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