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