• 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        raise RuntimeError("server.frontend._StableVersionMap::set_version is intentionally deleted")
245
246
247    def delete_version(self, board):
248        """
249        Remove the mapping of one board in the stable versions table.
250
251        Remove the mapping in the `stable_versions` table in the AFE
252        database for the given board.
253
254        @param board    The board to be updated.
255        """
256        raise RuntimeError("server.frontend._StableVersionMap::delete_version is intentionally deleted")
257
258
259class _OSVersionMap(_StableVersionMap):
260    """
261    Abstract stable version mapping for full OS images of various types.
262    """
263
264    def _version_is_valid(self, version):
265        return True
266
267    def get_all_versions(self):
268        versions = super(_OSVersionMap, self).get_all_versions()
269        for board in versions.keys():
270            if ('/' in board
271                    or not self._version_is_valid(versions[board])):
272                del versions[board]
273        return versions
274
275    def get_version(self, board):
276        version = super(_OSVersionMap, self).get_version(board)
277        return version if self._version_is_valid(version) else None
278
279
280def format_cros_image_name(board, version):
281    """
282    Return an image name for a given `board` and `version`.
283
284    This formats `board` and `version` into a string identifying an
285    image file.  The string represents part of a URL for access to
286    the image.
287
288    The returned image name is typically of a form like
289    "falco-release/R55-8872.44.0".
290    """
291    build_pattern = GLOBAL_CONFIG.get_config_value(
292            'CROS', 'stable_build_pattern')
293    return build_pattern % (board, version)
294
295
296class _CrosVersionMap(_OSVersionMap):
297    """
298    Stable version mapping for Chrome OS release images.
299
300    This class manages a mapping of Chrome OS board names to known-good
301    release (or canary) images.  The images selected can be installed on
302    DUTs during repair tasks, as a way of getting a DUT into a known
303    working state.
304    """
305
306    def _version_is_valid(self, version):
307        return version is not None and '/' not in version
308
309    def get_image_name(self, board):
310        """
311        Return the full image name of the stable version for `board`.
312
313        This finds the stable version for `board`, and returns a string
314        identifying the associated image as for `format_image_name()`,
315        above.
316
317        @return A string identifying the image file for the stable
318                image for `board`.
319        """
320        return format_cros_image_name(board, self.get_version(board))
321
322
323class _SuffixHackVersionMap(_StableVersionMap):
324    """
325    Abstract super class for mappings using a pseudo-board name.
326
327    For non-OS image type mappings, we look them up in the
328    `stable_versions` table by constructing a "pseudo-board" from the
329    real board name plus a suffix string that identifies the image type.
330    So, for instance the name "lulu/firmware" is used to look up the
331    FAFT firmware version for lulu boards.
332    """
333
334    # _SUFFIX - The suffix used in constructing the "pseudo-board"
335    # lookup key.  Each subclass must define this value for itself.
336    #
337    _SUFFIX = None
338
339    def get_all_versions(self):
340        # Get all the mappings from the AFE, extract just the mappings
341        # with our suffix, and replace the pseudo-board name keys with
342        # the real board names.
343        #
344        all_versions = super(
345                _SuffixHackVersionMap, self).get_all_versions()
346        return {
347            board[0 : -len(self._SUFFIX)]: all_versions[board]
348                for board in all_versions.keys()
349                    if board.endswith(self._SUFFIX)
350        }
351
352
353    def get_version(self, board):
354        board += self._SUFFIX
355        return super(_SuffixHackVersionMap, self).get_version(board)
356
357
358    def set_version(self, board, version):
359        board += self._SUFFIX
360        super(_SuffixHackVersionMap, self).set_version(board, version)
361
362
363    def delete_version(self, board):
364        board += self._SUFFIX
365        super(_SuffixHackVersionMap, self).delete_version(board)
366
367
368class _FAFTVersionMap(_SuffixHackVersionMap):
369    """
370    Stable version mapping for firmware versions used in FAFT repair.
371
372    When DUTs used for FAFT fail repair, stable firmware may need to be
373    flashed directly from original tarballs.  The FAFT firmware version
374    mapping finds the appropriate tarball for a given board.
375    """
376
377    _SUFFIX = '/firmware'
378
379    def get_version(self, board):
380        # If there's no mapping for `board`, the lookup will return the
381        # default CrOS version mapping.  To eliminate that case, we
382        # require a '/' character in the version, since CrOS versions
383        # won't match that.
384        #
385        # TODO(jrbarnette):  This is, of course, a hack.  Ultimately,
386        # the right fix is to move handling to the RPC server side.
387        #
388        version = super(_FAFTVersionMap, self).get_version(board)
389        return version if '/' in version else None
390
391
392class _FirmwareVersionMap(_SuffixHackVersionMap):
393    """
394    Stable version mapping for firmware supplied in Chrome OS images.
395
396    A Chrome OS image bundles a version of the firmware that the
397    device should update to when the OS version is installed during
398    AU.
399
400    Test images suppress the firmware update during AU.  Instead, during
401    repair and verify we check installed firmware on a DUT, compare it
402    against the stable version mapping for the board, and update when
403    the DUT is out-of-date.
404    """
405
406    _SUFFIX = '/rwfw'
407
408    def get_version(self, board):
409        # If there's no mapping for `board`, the lookup will return the
410        # default CrOS version mapping.  To eliminate that case, we
411        # require the version start with "Google_", since CrOS versions
412        # won't match that.
413        #
414        # TODO(jrbarnette):  This is, of course, a hack.  Ultimately,
415        # the right fix is to move handling to the RPC server side.
416        #
417        version = super(_FirmwareVersionMap, self).get_version(board)
418        return version if version.startswith('Google_') else None
419
420
421class AFE(RpcClient):
422
423    # Known image types for stable version mapping objects.
424    # CROS_IMAGE_TYPE - Mappings for Chrome OS images.
425    # FAFT_IMAGE_TYPE - Mappings for Firmware images for FAFT repair.
426    # FIRMWARE_IMAGE_TYPE - Mappings for released RW Firmware images.
427    #
428    CROS_IMAGE_TYPE = 'cros'
429    FAFT_IMAGE_TYPE = 'faft'
430    FIRMWARE_IMAGE_TYPE = 'firmware'
431
432    _IMAGE_MAPPING_CLASSES = {
433        CROS_IMAGE_TYPE: _CrosVersionMap,
434        FAFT_IMAGE_TYPE: _FAFTVersionMap,
435        FIRMWARE_IMAGE_TYPE: _FirmwareVersionMap,
436    }
437
438
439    def __init__(self, user=None, server=None, print_log=True, debug=False,
440                 reply_debug=False, job=None):
441        self.job = job
442        super(AFE, self).__init__(path='/afe/server/noauth/rpc/',
443                                  user=user,
444                                  server=server,
445                                  print_log=print_log,
446                                  debug=debug,
447                                  reply_debug=reply_debug)
448
449
450    def get_stable_version_map(self, image_type):
451        """
452        Return a stable version mapping for the given image type.
453
454        @return An object mapping board names to version strings for
455                software of the given image type.
456        """
457        return self._IMAGE_MAPPING_CLASSES[image_type](self)
458
459
460    def host_statuses(self, live=None):
461        dead_statuses = ['Repair Failed', 'Repairing']
462        statuses = self.run('get_static_data')['host_statuses']
463        if live == True:
464            return list(set(statuses) - set(dead_statuses))
465        if live == False:
466            return dead_statuses
467        else:
468            return statuses
469
470
471    @staticmethod
472    def _dict_for_host_query(hostnames=(), status=None, label=None):
473        query_args = {}
474        if hostnames:
475            query_args['hostname__in'] = hostnames
476        if status:
477            query_args['status'] = status
478        if label:
479            query_args['labels__name'] = label
480        return query_args
481
482
483    def get_hosts(self, hostnames=(), status=None, label=None, **dargs):
484        query_args = dict(dargs)
485        query_args.update(self._dict_for_host_query(hostnames=hostnames,
486                                                    status=status,
487                                                    label=label))
488        hosts = self.run('get_hosts', **query_args)
489        return [Host(self, h) for h in hosts]
490
491
492    def get_hostnames(self, status=None, label=None, **dargs):
493        """Like get_hosts() but returns hostnames instead of Host objects."""
494        # This implementation can be replaced with a more efficient one
495        # that does not query for entire host objects in the future.
496        return [host_obj.hostname for host_obj in
497                self.get_hosts(status=status, label=label, **dargs)]
498
499
500    def reverify_hosts(self, hostnames=(), status=None, label=None):
501        query_args = dict(locked=False,
502                          aclgroup__users__login=self.user)
503        query_args.update(self._dict_for_host_query(hostnames=hostnames,
504                                                    status=status,
505                                                    label=label))
506        return self.run('reverify_hosts', **query_args)
507
508
509    def repair_hosts(self, hostnames=(), status=None, label=None):
510        query_args = dict(locked=False,
511                          aclgroup__users__login=self.user)
512        query_args.update(self._dict_for_host_query(hostnames=hostnames,
513                                                    status=status,
514                                                    label=label))
515        return self.run('repair_hosts', **query_args)
516
517
518    def create_host(self, hostname, **dargs):
519        id = self.run('add_host', hostname=hostname, **dargs)
520        return self.get_hosts(id=id)[0]
521
522
523    def get_host_attribute(self, attr, **dargs):
524        host_attrs = self.run('get_host_attribute', attribute=attr, **dargs)
525        return [HostAttribute(self, a) for a in host_attrs]
526
527
528    def set_host_attribute(self, attr, val, **dargs):
529        self.run('set_host_attribute', attribute=attr, value=val, **dargs)
530
531
532    def get_labels(self, **dargs):
533        labels = self.run('get_labels', **dargs)
534        return [Label(self, l) for l in labels]
535
536
537    def create_label(self, name, **dargs):
538        id = self.run('add_label', name=name, **dargs)
539        return self.get_labels(id=id)[0]
540
541
542    def get_acls(self, **dargs):
543        acls = self.run('get_acl_groups', **dargs)
544        return [Acl(self, a) for a in acls]
545
546
547    def create_acl(self, name, **dargs):
548        id = self.run('add_acl_group', name=name, **dargs)
549        return self.get_acls(id=id)[0]
550
551
552    def get_users(self, **dargs):
553        users = self.run('get_users', **dargs)
554        return [User(self, u) for u in users]
555
556
557    def generate_control_file(self, tests, **dargs):
558        ret = self.run('generate_control_file', tests=tests, **dargs)
559        return ControlFile(self, ret)
560
561
562    def get_jobs(self, summary=False, **dargs):
563        if summary:
564            jobs_data = self.run('get_jobs_summary', **dargs)
565        else:
566            jobs_data = self.run('get_jobs', **dargs)
567        jobs = []
568        for j in jobs_data:
569            job = Job(self, j)
570            # Set up some extra information defaults
571            job.testname = re.sub('\s.*', '', job.name) # arbitrary default
572            job.platform_results = {}
573            job.platform_reasons = {}
574            jobs.append(job)
575        return jobs
576
577
578    def get_host_queue_entries(self, **kwargs):
579        """Find JobStatus objects matching some constraints.
580
581        @param **kwargs: Arguments to pass to the RPC
582        """
583        entries = self.run('get_host_queue_entries', **kwargs)
584        return self._entries_to_statuses(entries)
585
586
587    def get_host_queue_entries_by_insert_time(self, **kwargs):
588        """Like get_host_queue_entries, but using the insert index table.
589
590        @param **kwargs: Arguments to pass to the RPC
591        """
592        entries = self.run('get_host_queue_entries_by_insert_time', **kwargs)
593        return self._entries_to_statuses(entries)
594
595
596    def _entries_to_statuses(self, entries):
597        """Converts HQEs to JobStatuses
598
599        Sadly, get_host_queue_entries doesn't return platforms, we have
600        to get those back from an explicit get_hosts queury, then patch
601        the new host objects back into the host list.
602
603        :param entries: A list of HQEs from get_host_queue_entries or
604          get_host_queue_entries_by_insert_time.
605        """
606        job_statuses = [JobStatus(self, e) for e in entries]
607        hostnames = [s.host.hostname for s in job_statuses if s.host]
608        hosts = {}
609        for host in self.get_hosts(hostname__in=hostnames):
610            hosts[host.hostname] = host
611        for status in job_statuses:
612            if status.host:
613                status.host = hosts.get(status.host.hostname)
614        # filter job statuses that have either host or meta_host
615        return [status for status in job_statuses if (status.host or
616                                                      status.meta_host)]
617
618
619    def get_special_tasks(self, **data):
620        tasks = self.run('get_special_tasks', **data)
621        return [SpecialTask(self, t) for t in tasks]
622
623
624    def get_host_special_tasks(self, host_id, **data):
625        tasks = self.run('get_host_special_tasks',
626                         host_id=host_id, **data)
627        return [SpecialTask(self, t) for t in tasks]
628
629
630    def get_host_status_task(self, host_id, end_time):
631        task = self.run('get_host_status_task',
632                        host_id=host_id, end_time=end_time)
633        return SpecialTask(self, task) if task else None
634
635
636    def get_host_diagnosis_interval(self, host_id, end_time, success):
637        return self.run('get_host_diagnosis_interval',
638                        host_id=host_id, end_time=end_time,
639                        success=success)
640
641
642    def create_job(self, control_file, name=' ',
643                   priority=priorities.Priority.DEFAULT,
644                   control_type=control_data.CONTROL_TYPE_NAMES.CLIENT,
645                   **dargs):
646        id = self.run('create_job', name=name, priority=priority,
647                 control_file=control_file, control_type=control_type, **dargs)
648        return self.get_jobs(id=id)[0]
649
650
651    def abort_jobs(self, jobs):
652        """Abort a list of jobs.
653
654        Already completed jobs will not be affected.
655
656        @param jobs: List of job ids to abort.
657        """
658        for job in jobs:
659            self.run('abort_host_queue_entries', job_id=job)
660
661
662    def get_hosts_by_attribute(self, attribute, value):
663        """
664        Get the list of hosts that share the same host attribute value.
665
666        @param attribute: String of the host attribute to check.
667        @param value: String of the value that is shared between hosts.
668
669        @returns List of hostnames that all have the same host attribute and
670                 value.
671        """
672        return self.run('get_hosts_by_attribute',
673                        attribute=attribute, value=value)
674
675
676    def lock_host(self, host, lock_reason, fail_if_locked=False):
677        """
678        Lock the given host with the given lock reason.
679
680        Locking a host that's already locked using the 'modify_hosts' rpc
681        will raise an exception. That's why fail_if_locked exists so the
682        caller can determine if the lock succeeded or failed.  This will
683        save every caller from wrapping lock_host in a try-except.
684
685        @param host: hostname of host to lock.
686        @param lock_reason: Reason for locking host.
687        @param fail_if_locked: Return False if host is already locked.
688
689        @returns Boolean, True if lock was successful, False otherwise.
690        """
691        try:
692            self.run('modify_hosts',
693                     host_filter_data={'hostname': host},
694                     update_data={'locked': True,
695                                  'lock_reason': lock_reason})
696        except Exception:
697            return not fail_if_locked
698        return True
699
700
701    def unlock_hosts(self, locked_hosts):
702        """
703        Unlock the hosts.
704
705        Unlocking a host that's already unlocked will do nothing so we don't
706        need any special try-except clause here.
707
708        @param locked_hosts: List of hostnames of hosts to unlock.
709        """
710        self.run('modify_hosts',
711                 host_filter_data={'hostname__in': locked_hosts},
712                 update_data={'locked': False,
713                              'lock_reason': ''})
714
715
716class TestResults(object):
717    """
718    Container class used to hold the results of the tests for a job
719    """
720    def __init__(self):
721        self.good = []
722        self.fail = []
723        self.pending = []
724
725
726    def add(self, result):
727        if result.complete_count > result.pass_count:
728            self.fail.append(result)
729        elif result.incomplete_count > 0:
730            self.pending.append(result)
731        else:
732            self.good.append(result)
733
734
735class RpcObject(object):
736    """
737    Generic object used to construct python objects from rpc calls
738    """
739    def __init__(self, afe, hash):
740        self.afe = afe
741        self.hash = hash
742        self.__dict__.update(hash)
743
744
745    def __str__(self):
746        return dump_object(self.__repr__(), self)
747
748
749class ControlFile(RpcObject):
750    """
751    AFE control file object
752
753    Fields: synch_count, dependencies, control_file, is_server
754    """
755    def __repr__(self):
756        return 'CONTROL FILE: %s' % self.control_file
757
758
759class Label(RpcObject):
760    """
761    AFE label object
762
763    Fields:
764        name, invalid, platform, kernel_config, id, only_if_needed
765    """
766    def __repr__(self):
767        return 'LABEL: %s' % self.name
768
769
770    def add_hosts(self, hosts):
771        # We must use the label's name instead of the id because label ids are
772        # not consistent across master-shard.
773        return self.afe.run('label_add_hosts', id=self.name, hosts=hosts)
774
775
776    def remove_hosts(self, hosts):
777        # We must use the label's name instead of the id because label ids are
778        # not consistent across master-shard.
779        return self.afe.run('label_remove_hosts', id=self.name, hosts=hosts)
780
781
782class Acl(RpcObject):
783    """
784    AFE acl object
785
786    Fields:
787        users, hosts, description, name, id
788    """
789    def __repr__(self):
790        return 'ACL: %s' % self.name
791
792
793    def add_hosts(self, hosts):
794        self.afe.log('Adding hosts %s to ACL %s' % (hosts, self.name))
795        return self.afe.run('acl_group_add_hosts', self.id, hosts)
796
797
798    def remove_hosts(self, hosts):
799        self.afe.log('Removing hosts %s from ACL %s' % (hosts, self.name))
800        return self.afe.run('acl_group_remove_hosts', self.id, hosts)
801
802
803    def add_users(self, users):
804        self.afe.log('Adding users %s to ACL %s' % (users, self.name))
805        return self.afe.run('acl_group_add_users', id=self.name, users=users)
806
807
808class Job(RpcObject):
809    """
810    AFE job object
811
812    Fields:
813        name, control_file, control_type, synch_count, reboot_before,
814        run_verify, priority, email_list, created_on, dependencies,
815        timeout, owner, reboot_after, id
816    """
817    def __repr__(self):
818        return 'JOB: %s' % self.id
819
820
821class JobStatus(RpcObject):
822    """
823    AFE job_status object
824
825    Fields:
826        status, complete, deleted, meta_host, host, active, execution_subdir, id
827    """
828    def __init__(self, afe, hash):
829        super(JobStatus, self).__init__(afe, hash)
830        self.job = Job(afe, self.job)
831        if getattr(self, 'host'):
832            self.host = Host(afe, self.host)
833
834
835    def __repr__(self):
836        if self.host and self.host.hostname:
837            hostname = self.host.hostname
838        else:
839            hostname = 'None'
840        return 'JOB STATUS: %s-%s' % (self.job.id, hostname)
841
842
843class SpecialTask(RpcObject):
844    """
845    AFE special task object
846    """
847    def __init__(self, afe, hash):
848        super(SpecialTask, self).__init__(afe, hash)
849        self.host = Host(afe, self.host)
850
851
852    def __repr__(self):
853        return 'SPECIAL TASK: %s' % self.id
854
855
856class Host(RpcObject):
857    """
858    AFE host object
859
860    Fields:
861        status, lock_time, locked_by, locked, hostname, invalid,
862        labels, platform, protection, dirty, id
863    """
864    def __repr__(self):
865        return 'HOST OBJECT: %s' % self.hostname
866
867
868    def show(self):
869        labels = list(set(self.labels) - set([self.platform]))
870        print '%-6s %-7s %-7s %-16s %s' % (self.hostname, self.status,
871                                           self.locked, self.platform,
872                                           ', '.join(labels))
873
874
875    def delete(self):
876        return self.afe.run('delete_host', id=self.id)
877
878
879    def modify(self, **dargs):
880        return self.afe.run('modify_host', id=self.id, **dargs)
881
882
883    def get_acls(self):
884        return self.afe.get_acls(hosts__hostname=self.hostname)
885
886
887    def add_acl(self, acl_name):
888        self.afe.log('Adding ACL %s to host %s' % (acl_name, self.hostname))
889        return self.afe.run('acl_group_add_hosts', id=acl_name,
890                            hosts=[self.hostname])
891
892
893    def remove_acl(self, acl_name):
894        self.afe.log('Removing ACL %s from host %s' % (acl_name, self.hostname))
895        return self.afe.run('acl_group_remove_hosts', id=acl_name,
896                            hosts=[self.hostname])
897
898
899    def get_labels(self):
900        return self.afe.get_labels(host__hostname__in=[self.hostname])
901
902
903    def add_labels(self, labels):
904        self.afe.log('Adding labels %s to host %s' % (labels, self.hostname))
905        return self.afe.run('host_add_labels', id=self.id, labels=labels)
906
907
908    def remove_labels(self, labels):
909        self.afe.log('Removing labels %s from host %s' % (labels,self.hostname))
910        return self.afe.run('host_remove_labels', id=self.id, labels=labels)
911
912
913    def is_available(self):
914        """Check whether DUT host is available.
915
916        @return: bool
917        """
918        return not (self.locked
919                    or self.status in host_states.UNAVAILABLE_STATES)
920
921
922class User(RpcObject):
923    def __repr__(self):
924        return 'USER: %s' % self.login
925
926
927class TestStatus(RpcObject):
928    """
929    TKO test status object
930
931    Fields:
932        test_idx, hostname, testname, id
933        complete_count, incomplete_count, group_count, pass_count
934    """
935    def __repr__(self):
936        return 'TEST STATUS: %s' % self.id
937
938
939class HostAttribute(RpcObject):
940    """
941    AFE host attribute object
942
943    Fields:
944        id, host, attribute, value
945    """
946    def __repr__(self):
947        return 'HOST ATTRIBUTE %d' % self.id
948