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