• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Copyright Martin J. Bligh, Google Inc 2008
2# Released under the GPL v2
3
4"""
5This class allows you to communicate with the frontend to submit jobs etc
6It is designed for writing more sophisiticated server-side control files that
7can recursively add and manage other jobs.
8
9We turn the JSON dictionaries into real objects that are more idiomatic
10
11For docs, see:
12    http://www.chromium.org/chromium-os/testing/afe-rpc-infrastructure
13    http://docs.djangoproject.com/en/dev/ref/models/querysets/#queryset-api
14"""
15
16#pylint: disable=missing-docstring
17
18import getpass
19import os
20import re
21
22import common
23
24from autotest_lib.frontend.afe import rpc_client_lib
25from autotest_lib.client.common_lib import control_data
26from autotest_lib.client.common_lib import global_config
27from autotest_lib.client.common_lib import host_states
28from autotest_lib.client.common_lib import priorities
29from autotest_lib.client.common_lib import utils
30from autotest_lib.tko import db
31
32try:
33    from chromite.lib import metrics
34except ImportError:
35    metrics = utils.metrics_mock
36
37try:
38    from autotest_lib.server.site_common import site_utils as server_utils
39except:
40    from autotest_lib.server import utils as server_utils
41form_ntuples_from_machines = server_utils.form_ntuples_from_machines
42
43GLOBAL_CONFIG = global_config.global_config
44DEFAULT_SERVER = 'autotest'
45
46
47def dump_object(header, obj):
48    """
49    Standard way to print out the frontend objects (eg job, host, acl, label)
50    in a human-readable fashion for debugging
51    """
52    result = header + '\n'
53    for key in obj.hash:
54        if key == 'afe' or key == 'hash':
55            continue
56        result += '%20s: %s\n' % (key, obj.hash[key])
57    return result
58
59
60class RpcClient(object):
61    """
62    Abstract RPC class for communicating with the autotest frontend
63    Inherited for both TKO and AFE uses.
64
65    All the constructors go in the afe / tko class.
66    Manipulating methods go in the object classes themselves
67    """
68    def __init__(self, path, user, server, print_log, debug, reply_debug):
69        """
70        Create a cached instance of a connection to the frontend
71
72            user: username to connect as
73            server: frontend server to connect to
74            print_log: pring a logging message to stdout on every operation
75            debug: print out all RPC traffic
76        """
77        if not user and utils.is_in_container():
78            user = GLOBAL_CONFIG.get_config_value('SSP', 'user', default=None)
79        if not user:
80            user = getpass.getuser()
81        if not server:
82            if 'AUTOTEST_WEB' in os.environ:
83                server = os.environ['AUTOTEST_WEB']
84            else:
85                server = GLOBAL_CONFIG.get_config_value('SERVER', 'hostname',
86                                                        default=DEFAULT_SERVER)
87        self.server = server
88        self.user = user
89        self.print_log = print_log
90        self.debug = debug
91        self.reply_debug = reply_debug
92        headers = {'AUTHORIZATION': self.user}
93        rpc_server = rpc_client_lib.add_protocol(server) + path
94        if debug:
95            print 'SERVER: %s' % rpc_server
96            print 'HEADERS: %s' % headers
97        self.proxy = rpc_client_lib.get_proxy(rpc_server, headers=headers)
98
99
100    def run(self, call, **dargs):
101        """
102        Make a RPC call to the AFE server
103        """
104        rpc_call = getattr(self.proxy, call)
105        if self.debug:
106            print 'DEBUG: %s %s' % (call, dargs)
107        try:
108            result = utils.strip_unicode(rpc_call(**dargs))
109            if self.reply_debug:
110                print result
111            return result
112        except Exception:
113            raise
114
115
116    def log(self, message):
117        if self.print_log:
118            print message
119
120
121class TKO(RpcClient):
122    def __init__(self, user=None, server=None, print_log=True, debug=False,
123                 reply_debug=False):
124        super(TKO, self).__init__(path='/new_tko/server/noauth/rpc/',
125                                  user=user,
126                                  server=server,
127                                  print_log=print_log,
128                                  debug=debug,
129                                  reply_debug=reply_debug)
130        self._db = None
131
132
133    @metrics.SecondsTimerDecorator(
134            'chromeos/autotest/tko/get_job_status_duration')
135    def get_job_test_statuses_from_db(self, job_id):
136        """Get job test statuses from the database.
137
138        Retrieve a set of fields from a job that reflect the status of each test
139        run within a job.
140        fields retrieved: status, test_name, reason, test_started_time,
141                          test_finished_time, afe_job_id, job_owner, hostname.
142
143        @param job_id: The afe job id to look up.
144        @returns a TestStatus object of the resulting information.
145        """
146        if self._db is None:
147            self._db = db.db()
148        fields = ['status', 'test_name', 'subdir', 'reason',
149                  'test_started_time', 'test_finished_time', 'afe_job_id',
150                  'job_owner', 'hostname', 'job_tag']
151        table = 'tko_test_view_2'
152        where = 'job_tag like "%s-%%"' % job_id
153        test_status = []
154        # Run commit before we query to ensure that we are pulling the latest
155        # results.
156        self._db.commit()
157        for entry in self._db.select(','.join(fields), table, (where, None)):
158            status_dict = {}
159            for key,value in zip(fields, entry):
160                # All callers expect values to be a str object.
161                status_dict[key] = str(value)
162            # id is used by TestStatus to uniquely identify each Test Status
163            # obj.
164            status_dict['id'] = [status_dict['reason'], status_dict['hostname'],
165                                 status_dict['test_name']]
166            test_status.append(status_dict)
167
168        return [TestStatus(self, e) for e in test_status]
169
170
171    def get_status_counts(self, job, **data):
172        entries = self.run('get_status_counts',
173                           group_by=['hostname', 'test_name', 'reason'],
174                           job_tag__startswith='%s-' % job, **data)
175        return [TestStatus(self, e) for e in entries['groups']]
176
177
178class _StableVersionMap(object):
179    """
180    A mapping from board names to strings naming software versions.
181
182    The mapping is meant to allow finding a nominally "stable" version
183    of software associated with a given board.  The mapping identifies
184    specific versions of software that should be installed during
185    operations such as repair.
186
187    Conceptually, there are multiple version maps, each handling
188    different types of image.  For instance, a single board may have
189    both a stable OS image (e.g. for CrOS), and a separate stable
190    firmware image.
191
192    Each different type of image requires a certain amount of special
193    handling, implemented by a subclass of `StableVersionMap`.  The
194    subclasses take care of pre-processing of arguments, delegating
195    actual RPC calls to this superclass.
196
197    @property _afe      AFE object through which to make the actual RPC
198                        calls.
199    @property _android  Value of the `android` parameter to be passed
200                        when calling the `get_stable_version` RPC.
201    """
202
203    def __init__(self, afe):
204        self._afe = afe
205
206
207    def get_all_versions(self):
208        """
209        Get all mappings in the stable versions table.
210
211        Extracts the full content of the `stable_version` table
212        in the AFE database, and returns it as a dictionary
213        mapping board names to version strings.
214
215        @return A dictionary mapping board names to version strings.
216        """
217        return self._afe.run('get_all_stable_versions')
218
219
220    def get_version(self, board):
221        """
222        Get the mapping of one board in the stable versions table.
223
224        Look up and return the version mapped to the given board in the
225        `stable_versions` table in the AFE database.
226
227        @param board  The board to be looked up.
228
229        @return The version mapped for the given board.
230        """
231        return self._afe.run('get_stable_version', board=board)
232
233
234    def set_version(self, board, version):
235        """
236        Change the mapping of one board in the stable versions table.
237
238        Set the mapping in the `stable_versions` table in the AFE
239        database for the given board to the given version.
240
241        @param board    The board to be updated.
242        @param version  The new version to be assigned to the board.
243        """
244        self._afe.run('set_stable_version',
245                      version=version, board=board)
246
247
248    def delete_version(self, board):
249        """
250        Remove the mapping of one board in the stable versions table.
251
252        Remove the mapping in the `stable_versions` table in the AFE
253        database for the given board.
254
255        @param board    The board to be updated.
256        """
257        self._afe.run('delete_stable_version', board=board)
258
259
260class _OSVersionMap(_StableVersionMap):
261    """
262    Abstract stable version mapping for full OS images of various types.
263    """
264
265    def _version_is_valid(self, version):
266        return True
267
268    def get_all_versions(self):
269        versions = super(_OSVersionMap, self).get_all_versions()
270        for board in versions.keys():
271            if ('/' in board
272                    or not self._version_is_valid(versions[board])):
273                del versions[board]
274        return versions
275
276    def get_version(self, board):
277        version = super(_OSVersionMap, self).get_version(board)
278        return version if self._version_is_valid(version) else None
279
280
281def format_cros_image_name(board, version):
282    """
283    Return an image name for a given `board` and `version`.
284
285    This formats `board` and `version` into a string identifying an
286    image file.  The string represents part of a URL for access to
287    the image.
288
289    The returned image name is typically of a form like
290    "falco-release/R55-8872.44.0".
291    """
292    build_pattern = GLOBAL_CONFIG.get_config_value(
293            'CROS', 'stable_build_pattern')
294    return build_pattern % (board, version)
295
296
297class _CrosVersionMap(_OSVersionMap):
298    """
299    Stable version mapping for Chrome OS release images.
300
301    This class manages a mapping of Chrome OS board names to known-good
302    release (or canary) images.  The images selected can be installed on
303    DUTs during repair tasks, as a way of getting a DUT into a known
304    working state.
305    """
306
307    def _version_is_valid(self, version):
308        return version is not None and '/' not in version
309
310    def get_image_name(self, board):
311        """
312        Return the full image name of the stable version for `board`.
313
314        This finds the stable version for `board`, and returns a string
315        identifying the associated image as for `format_image_name()`,
316        above.
317
318        @return A string identifying the image file for the stable
319                image for `board`.
320        """
321        return format_cros_image_name(board, self.get_version(board))
322
323
324class _SuffixHackVersionMap(_StableVersionMap):
325    """
326    Abstract super class for mappings using a pseudo-board name.
327
328    For non-OS image type mappings, we look them up in the
329    `stable_versions` table by constructing a "pseudo-board" from the
330    real board name plus a suffix string that identifies the image type.
331    So, for instance the name "lulu/firmware" is used to look up the
332    FAFT firmware version for lulu boards.
333    """
334
335    # _SUFFIX - The suffix used in constructing the "pseudo-board"
336    # lookup key.  Each subclass must define this value for itself.
337    #
338    _SUFFIX = None
339
340    def get_all_versions(self):
341        # Get all the mappings from the AFE, extract just the mappings
342        # with our suffix, and replace the pseudo-board name keys with
343        # the real board names.
344        #
345        all_versions = super(
346                _SuffixHackVersionMap, self).get_all_versions()
347        return {
348            board[0 : -len(self._SUFFIX)]: all_versions[board]
349                for board in all_versions.keys()
350                    if board.endswith(self._SUFFIX)
351        }
352
353
354    def get_version(self, board):
355        board += self._SUFFIX
356        return super(_SuffixHackVersionMap, self).get_version(board)
357
358
359    def set_version(self, board, version):
360        board += self._SUFFIX
361        super(_SuffixHackVersionMap, self).set_version(board, version)
362
363
364    def delete_version(self, board):
365        board += self._SUFFIX
366        super(_SuffixHackVersionMap, self).delete_version(board)
367
368
369class _FAFTVersionMap(_SuffixHackVersionMap):
370    """
371    Stable version mapping for firmware versions used in FAFT repair.
372
373    When DUTs used for FAFT fail repair, stable firmware may need to be
374    flashed directly from original tarballs.  The FAFT firmware version
375    mapping finds the appropriate tarball for a given board.
376    """
377
378    _SUFFIX = '/firmware'
379
380    def get_version(self, board):
381        # If there's no mapping for `board`, the lookup will return the
382        # default CrOS version mapping.  To eliminate that case, we
383        # require a '/' character in the version, since CrOS versions
384        # won't match that.
385        #
386        # TODO(jrbarnette):  This is, of course, a hack.  Ultimately,
387        # the right fix is to move handling to the RPC server side.
388        #
389        version = super(_FAFTVersionMap, self).get_version(board)
390        return version if '/' in version else None
391
392
393class _FirmwareVersionMap(_SuffixHackVersionMap):
394    """
395    Stable version mapping for firmware supplied in Chrome OS images.
396
397    A Chrome OS image bundles a version of the firmware that the
398    device should update to when the OS version is installed during
399    AU.
400
401    Test images suppress the firmware update during AU.  Instead, during
402    repair and verify we check installed firmware on a DUT, compare it
403    against the stable version mapping for the board, and update when
404    the DUT is out-of-date.
405    """
406
407    _SUFFIX = '/rwfw'
408
409    def get_version(self, board):
410        # If there's no mapping for `board`, the lookup will return the
411        # default CrOS version mapping.  To eliminate that case, we
412        # require the version start with "Google_", since CrOS versions
413        # won't match that.
414        #
415        # TODO(jrbarnette):  This is, of course, a hack.  Ultimately,
416        # the right fix is to move handling to the RPC server side.
417        #
418        version = super(_FirmwareVersionMap, self).get_version(board)
419        return version if version.startswith('Google_') else None
420
421
422class AFE(RpcClient):
423
424    # Known image types for stable version mapping objects.
425    # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
426    # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
427    # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
428    #
429    CROS_IMAGE_TYPE = 'cros'
430    FAFT_IMAGE_TYPE = 'faft'
431    FIRMWARE_IMAGE_TYPE = 'firmware'
432
433    _IMAGE_MAPPING_CLASSES = {
434        CROS_IMAGE_TYPE: _CrosVersionMap,
435        FAFT_IMAGE_TYPE: _FAFTVersionMap,
436        FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
437    }
438
439
440    def __init__(self, user=None, server=None, print_log=True, debug=False,
441                 reply_debug=False, job=None):
442        self.job = job
443        super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
444                                  user=user,
445                                  server=server,
446                                  print_log=print_log,
447                                  debug=debug,
448                                  reply_debug=reply_debug)
449
450
451    def get_stable_version_map(self, image_type):
452        """
453        Return a stable version mapping for the given image type.
454
455        @return An object mapping board names to version strings for
456                software of the given image type.
457        """
458        return self._IMAGE_MAPPING_CLASSES[image_type](self)
459
460
461    def host_statuses(self, live=None):
462        dead_statuses = ['Repair Failed', 'Repairing']
463        statuses = self.run('get_static_data')['host_statuses']
464        if live == True:
465            return list(set(statuses) - set(dead_statuses))
466        if live == False:
467            return dead_statuses
468        else:
469            return statuses
470
471
472    @staticmethod
473    def _dict_for_host_query(hostnames=(), status=None, label=None):
474        query_args = {}
475        if hostnames:
476            query_args['hostname__in'] = hostnames
477        if status:
478            query_args['status'] = status
479        if label:
480            query_args['labels__name'] = label
481        return query_args
482
483
484    def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
485        query_args = dict(dargs)
486        query_args.update(self._dict_for_host_query(hostnames=hostnames,
487                                                    status=status,
488                                                    label=label))
489        hosts = self.run('get_hosts', **query_args)
490        return [Host(self, h) for h in hosts]
491
492
493    def get_hostnames(self, status=None, label=None, **dargs):
494        """Like get_hosts() but returns hostnames instead of Host objects."""
495        # This implementation can be replaced with a more efficient one
496        # that does not query for entire host objects in the future.
497        return [host_obj.hostname for host_obj in
498                self.get_hosts(status=status, label=label, **dargs)]
499
500
501    def reverify_hosts(self, hostnames=(), status=None, label=None):
502        query_args = dict(locked=False,
503                          aclgroup__users__login=self.user)
504        query_args.update(self._dict_for_host_query(hostnames=hostnames,
505                                                    status=status,
506                                                    label=label))
507        return self.run('reverify_hosts', **query_args)
508
509
510    def repair_hosts(self, hostnames=(), status=None, label=None):
511        query_args = dict(locked=False,
512                          aclgroup__users__login=self.user)
513        query_args.update(self._dict_for_host_query(hostnames=hostnames,
514                                                    status=status,
515                                                    label=label))
516        return self.run('repair_hosts', **query_args)
517
518
519    def create_host(self, hostname, **dargs):
520        id = self.run('add_host', hostname=hostname, **dargs)
521        return self.get_hosts(id=id)[0]
522
523
524    def get_host_attribute(self, attr, **dargs):
525        host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
526        return [HostAttribute(self, a) for a in host_attrs]
527
528
529    def set_host_attribute(self, attr, val, **dargs):
530        self.run('set_host_attribute', attribute=attr, value=val, **dargs)
531
532
533    def get_labels(self, **dargs):
534        labels = self.run('get_labels', **dargs)
535        return [Label(self, l) for l in labels]
536
537
538    def create_label(self, name, **dargs):
539        id = self.run('add_label', name=name, **dargs)
540        return self.get_labels(id=id)[0]
541
542
543    def get_acls(self, **dargs):
544        acls = self.run('get_acl_groups', **dargs)
545        return [Acl(self, a) for a in acls]
546
547
548    def create_acl(self, name, **dargs):
549        id = self.run('add_acl_group', name=name, **dargs)
550        return self.get_acls(id=id)[0]
551
552
553    def get_users(self, **dargs):
554        users = self.run('get_users', **dargs)
555        return [User(self, u) for u in users]
556
557
558    def generate_control_file(self, tests, **dargs):
559        ret = self.run('generate_control_file', tests=tests, **dargs)
560        return ControlFile(self, ret)
561
562
563    def get_jobs(self, summary=False, **dargs):
564        if summary:
565            jobs_data = self.run('get_jobs_summary', **dargs)
566        else:
567            jobs_data = self.run('get_jobs', **dargs)
568        jobs = []
569        for j in jobs_data:
570            job = Job(self, j)
571            # Set up some extra information defaults
572            job.testname = re.sub('\s.*', '', job.name) # arbitrary default
573            job.platform_results = {}
574            job.platform_reasons = {}
575            jobs.append(job)
576        return jobs
577
578
579    def get_host_queue_entries(self, **kwargs):
580        """Find JobStatus objects matching some constraints.
581
582        @param **kwargs: Arguments to pass to the RPC
583        """
584        entries = self.run('get_host_queue_entries', **kwargs)
585        return self._entries_to_statuses(entries)
586
587
588    def get_host_queue_entries_by_insert_time(self, **kwargs):
589        """Like get_host_queue_entries, but using the insert index table.
590
591        @param **kwargs: Arguments to pass to the RPC
592        """
593        entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
594        return self._entries_to_statuses(entries)
595
596
597    def _entries_to_statuses(self, entries):
598        """Converts HQEs to JobStatuses
599
600        Sadly, get_host_queue_entries doesn't return platforms, we have
601        to get those back from an explicit get_hosts queury, then patch
602        the new host objects back into the host list.
603
604        :param entries: A list of HQEs from get_host_queue_entries or
605          get_host_queue_entries_by_insert_time.
606        """
607        job_statuses = [JobStatus(self, e) for e in entries]
608        hostnames = [s.host.hostname for s in job_statuses if s.host]
609        hosts = {}
610        for host in self.get_hosts(hostname__in=hostnames):
611            hosts[host.hostname] = host
612        for status in job_statuses:
613            if status.host:
614                status.host = hosts.get(status.host.hostname)
615        # filter job statuses that have either host or meta_host
616        return [status for status in job_statuses if (status.host or
617                                                      status.meta_host)]
618
619
620    def get_special_tasks(self, **data):
621        tasks = self.run('get_special_tasks', **data)
622        return [SpecialTask(self, t) for t in tasks]
623
624
625    def get_host_special_tasks(self, host_id, **data):
626        tasks = self.run('get_host_special_tasks',
627                         host_id=host_id, **data)
628        return [SpecialTask(self, t) for t in tasks]
629
630
631    def get_host_status_task(self, host_id, end_time):
632        task = self.run('get_host_status_task',
633                        host_id=host_id, end_time=end_time)
634        return SpecialTask(self, task) if task else None
635
636
637    def get_host_diagnosis_interval(self, host_id, end_time, success):
638        return self.run('get_host_diagnosis_interval',
639                        host_id=host_id, end_time=end_time,
640                        success=success)
641
642
643    def create_job(self, control_file, name=' ',
644                   priority=priorities.Priority.DEFAULT,
645                   control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
646                   **dargs):
647        id = self.run('create_job', name=name, priority=priority,
648                 control_file=control_file, control_type=control_type, **dargs)
649        return self.get_jobs(id=id)[0]
650
651
652    def abort_jobs(self, jobs):
653        """Abort a list of jobs.
654
655        Already completed jobs will not be affected.
656
657        @param jobs: List of job ids to abort.
658        """
659        for job in jobs:
660            self.run('abort_host_queue_entries', job_id=job)
661
662
663    def get_hosts_by_attribute(self, attribute, value):
664        """
665        Get the list of hosts that share the same host attribute value.
666
667        @param attribute: String of the host attribute to check.
668        @param value: String of the value that is shared between hosts.
669
670        @returns List of hostnames that all have the same host attribute and
671                 value.
672        """
673        return self.run('get_hosts_by_attribute',
674                        attribute=attribute, value=value)
675
676
677    def lock_host(self, host, lock_reason, fail_if_locked=False):
678        """
679        Lock the given host with the given lock reason.
680
681        Locking a host that's already locked using the 'modify_hosts' rpc
682        will raise an exception. That's why fail_if_locked exists so the
683        caller can determine if the lock succeeded or failed.  This will
684        save every caller from wrapping lock_host in a try-except.
685
686        @param host: hostname of host to lock.
687        @param lock_reason: Reason for locking host.
688        @param fail_if_locked: Return False if host is already locked.
689
690        @returns Boolean, True if lock was successful, False otherwise.
691        """
692        try:
693            self.run('modify_hosts',
694                     host_filter_data={'hostname': host},
695                     update_data={'locked': True,
696                                  'lock_reason': lock_reason})
697        except Exception:
698            return not fail_if_locked
699        return True
700
701
702    def unlock_hosts(self, locked_hosts):
703        """
704        Unlock the hosts.
705
706        Unlocking a host that's already unlocked will do nothing so we don't
707        need any special try-except clause here.
708
709        @param locked_hosts: List of hostnames of hosts to unlock.
710        """
711        self.run('modify_hosts',
712                 host_filter_data={'hostname__in': locked_hosts},
713                 update_data={'locked': False,
714                              'lock_reason': ''})
715
716
717class TestResults(object):
718    """
719    Container class used to hold the results of the tests for a job
720    """
721    def __init__(self):
722        self.good = []
723        self.fail = []
724        self.pending = []
725
726
727    def add(self, result):
728        if result.complete_count > result.pass_count:
729            self.fail.append(result)
730        elif result.incomplete_count > 0:
731            self.pending.append(result)
732        else:
733            self.good.append(result)
734
735
736class RpcObject(object):
737    """
738    Generic object used to construct python objects from rpc calls
739    """
740    def __init__(self, afe, hash):
741        self.afe = afe
742        self.hash = hash
743        self.__dict__.update(hash)
744
745
746    def __str__(self):
747        return dump_object(self.__repr__(), self)
748
749
750class ControlFile(RpcObject):
751    """
752    AFE control file object
753
754    Fields: synch_count, dependencies, control_file, is_server
755    """
756    def __repr__(self):
757        return 'CONTROL FILE: %s' % self.control_file
758
759
760class Label(RpcObject):
761    """
762    AFE label object
763
764    Fields:
765        name, invalid, platform, kernel_config, id, only_if_needed
766    """
767    def __repr__(self):
768        return 'LABEL: %s' % self.name
769
770
771    def add_hosts(self, hosts):
772        # We must use the label's name instead of the id because label ids are
773        # not consistent across master-shard.
774        return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
775
776
777    def remove_hosts(self, hosts):
778        # We must use the label's name instead of the id because label ids are
779        # not consistent across master-shard.
780        return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
781
782
783class Acl(RpcObject):
784    """
785    AFE acl object
786
787    Fields:
788        users, hosts, description, name, id
789    """
790    def __repr__(self):
791        return 'ACL: %s' % self.name
792
793
794    def add_hosts(self, hosts):
795        self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
796        return self.afe.run('acl_group_add_hosts', self.id, hosts)
797
798
799    def remove_hosts(self, hosts):
800        self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
801        return self.afe.run('acl_group_remove_hosts', self.id, hosts)
802
803
804    def add_users(self, users):
805        self.afe.log('Adding users %s to ACL %s' % (users, self.name))
806        return self.afe.run('acl_group_add_users', id=self.name, users=users)
807
808
809class Job(RpcObject):
810    """
811    AFE job object
812
813    Fields:
814        name, control_file, control_type, synch_count, reboot_before,
815        run_verify, priority, email_list, created_on, dependencies,
816        timeout, owner, reboot_after, id
817    """
818    def __repr__(self):
819        return 'JOB: %s' % self.id
820
821
822class JobStatus(RpcObject):
823    """
824    AFE job_status object
825
826    Fields:
827        status, complete, deleted, meta_host, host, active, execution_subdir, id
828    """
829    def __init__(self, afe, hash):
830        super(JobStatus, self).__init__(afe, hash)
831        self.job = Job(afe, self.job)
832        if getattr(self, 'host'):
833            self.host = Host(afe, self.host)
834
835
836    def __repr__(self):
837        if self.host and self.host.hostname:
838            hostname = self.host.hostname
839        else:
840            hostname = 'None'
841        return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
842
843
844class SpecialTask(RpcObject):
845    """
846    AFE special task object
847    """
848    def __init__(self, afe, hash):
849        super(SpecialTask, self).__init__(afe, hash)
850        self.host = Host(afe, self.host)
851
852
853    def __repr__(self):
854        return 'SPECIAL TASK: %s' % self.id
855
856
857class Host(RpcObject):
858    """
859    AFE host object
860
861    Fields:
862        status, lock_time, locked_by, locked, hostname, invalid,
863        labels, platform, protection, dirty, id
864    """
865    def __repr__(self):
866        return 'HOST OBJECT: %s' % self.hostname
867
868
869    def show(self):
870        labels = list(set(self.labels) - set([self.platform]))
871        print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
872                                           self.locked, self.platform,
873                                           ', '.join(labels))
874
875
876    def delete(self):
877        return self.afe.run('delete_host', id=self.id)
878
879
880    def modify(self, **dargs):
881        return self.afe.run('modify_host', id=self.id, **dargs)
882
883
884    def get_acls(self):
885        return self.afe.get_acls(hosts__hostname=self.hostname)
886
887
888    def add_acl(self, acl_name):
889        self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
890        return self.afe.run('acl_group_add_hosts', id=acl_name,
891                            hosts=[self.hostname])
892
893
894    def remove_acl(self, acl_name):
895        self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
896        return self.afe.run('acl_group_remove_hosts', id=acl_name,
897                            hosts=[self.hostname])
898
899
900    def get_labels(self):
901        return self.afe.get_labels(host__hostname__in=[self.hostname])
902
903
904    def add_labels(self, labels):
905        self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
906        return self.afe.run('host_add_labels', id=self.id, labels=labels)
907
908
909    def remove_labels(self, labels):
910        self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
911        return self.afe.run('host_remove_labels', id=self.id, labels=labels)
912
913
914    def is_available(self):
915        """Check whether DUT host is available.
916
917        @return: bool
918        """
919        return not (self.locked
920                    or self.status in host_states.UNAVAILABLE_STATES)
921
922
923class User(RpcObject):
924    def __repr__(self):
925        return 'USER: %s' % self.login
926
927
928class TestStatus(RpcObject):
929    """
930    TKO test status object
931
932    Fields:
933        test_idx, hostname, testname, id
934        complete_count, incomplete_count, group_count, pass_count
935    """
936    def __repr__(self):
937        return 'TEST STATUS: %s' % self.id
938
939
940class HostAttribute(RpcObject):
941    """
942    AFE host attribute object
943
944    Fields:
945        id, host, attribute, value
946    """
947    def __repr__(self):
948        return 'HOST ATTRIBUTE %d' % self.id
949