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