• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#
2# Copyright 2008 Google Inc. All Rights Reserved.
3
4"""
5The host module contains the objects and method used to
6manage a host in Autotest.
7
8The valid actions are:
9create:  adds host(s)
10delete:  deletes host(s)
11list:    lists host(s)
12stat:    displays host(s) information
13mod:     modifies host(s)
14jobs:    lists all jobs that ran on host(s)
15
16The common options are:
17-M|--mlist:   file containing a list of machines
18
19
20See topic_common.py for a High Level Design and Algorithm.
21
22"""
23import re
24
25from autotest_lib.cli import action_common, topic_common
26from autotest_lib.client.common_lib import host_protections
27
28
29class host(topic_common.atest):
30    """Host class
31    atest host [create|delete|list|stat|mod|jobs] <options>"""
32    usage_action = '[create|delete|list|stat|mod|jobs]'
33    topic = msg_topic = 'host'
34    msg_items = '<hosts>'
35
36    protections = host_protections.Protection.names
37
38
39    def __init__(self):
40        """Add to the parser the options common to all the
41        host actions"""
42        super(host, self).__init__()
43
44        self.parser.add_option('-M', '--mlist',
45                               help='File listing the machines',
46                               type='string',
47                               default=None,
48                               metavar='MACHINE_FLIST')
49
50        self.topic_parse_info = topic_common.item_parse_info(
51            attribute_name='hosts',
52            filename_option='mlist',
53            use_leftover=True)
54
55
56    def _parse_lock_options(self, options):
57        if options.lock and options.unlock:
58            self.invalid_syntax('Only specify one of '
59                                '--lock and --unlock.')
60
61        if options.lock:
62            self.data['locked'] = True
63            self.messages.append('Locked host')
64        elif options.unlock:
65            self.data['locked'] = False
66            self.data['lock_reason'] = ''
67            self.messages.append('Unlocked host')
68
69        if options.lock and options.lock_reason:
70            self.data['lock_reason'] = options.lock_reason
71
72
73    def _cleanup_labels(self, labels, platform=None):
74        """Removes the platform label from the overall labels"""
75        if platform:
76            return [label for label in labels
77                    if label != platform]
78        else:
79            try:
80                return [label for label in labels
81                        if not label['platform']]
82            except TypeError:
83                # This is a hack - the server will soon
84                # do this, so all this code should be removed.
85                return labels
86
87
88    def get_items(self):
89        return self.hosts
90
91
92class host_help(host):
93    """Just here to get the atest logic working.
94    Usage is set by its parent"""
95    pass
96
97
98class host_list(action_common.atest_list, host):
99    """atest host list [--mlist <file>|<hosts>] [--label <label>]
100       [--status <status1,status2>] [--acl <ACL>] [--user <user>]"""
101
102    def __init__(self):
103        super(host_list, self).__init__()
104
105        self.parser.add_option('-b', '--label',
106                               default='',
107                               help='Only list hosts with all these labels '
108                               '(comma separated)')
109        self.parser.add_option('-s', '--status',
110                               default='',
111                               help='Only list hosts with any of these '
112                               'statuses (comma separated)')
113        self.parser.add_option('-a', '--acl',
114                               default='',
115                               help='Only list hosts within this ACL')
116        self.parser.add_option('-u', '--user',
117                               default='',
118                               help='Only list hosts available to this user')
119        self.parser.add_option('-N', '--hostnames-only', help='Only return '
120                               'hostnames for the machines queried.',
121                               action='store_true')
122        self.parser.add_option('--locked',
123                               default=False,
124                               help='Only list locked hosts',
125                               action='store_true')
126        self.parser.add_option('--unlocked',
127                               default=False,
128                               help='Only list unlocked hosts',
129                               action='store_true')
130
131
132
133    def parse(self):
134        """Consume the specific options"""
135        label_info = topic_common.item_parse_info(attribute_name='labels',
136                                                  inline_option='label')
137
138        (options, leftover) = super(host_list, self).parse([label_info])
139
140        self.status = options.status
141        self.acl = options.acl
142        self.user = options.user
143        self.hostnames_only = options.hostnames_only
144
145        if options.locked and options.unlocked:
146            self.invalid_syntax('--locked and --unlocked are '
147                                'mutually exclusive')
148        self.locked = options.locked
149        self.unlocked = options.unlocked
150        return (options, leftover)
151
152
153    def execute(self):
154        filters = {}
155        check_results = {}
156        if self.hosts:
157            filters['hostname__in'] = self.hosts
158            check_results['hostname__in'] = 'hostname'
159
160        if self.labels:
161            if len(self.labels) == 1:
162                # This is needed for labels with wildcards (x86*)
163                filters['labels__name__in'] = self.labels
164                check_results['labels__name__in'] = None
165            else:
166                filters['multiple_labels'] = self.labels
167                check_results['multiple_labels'] = None
168
169        if self.status:
170            statuses = self.status.split(',')
171            statuses = [status.strip() for status in statuses
172                        if status.strip()]
173
174            filters['status__in'] = statuses
175            check_results['status__in'] = None
176
177        if self.acl:
178            filters['aclgroup__name'] = self.acl
179            check_results['aclgroup__name'] = None
180        if self.user:
181            filters['aclgroup__users__login'] = self.user
182            check_results['aclgroup__users__login'] = None
183
184        if self.locked or self.unlocked:
185            filters['locked'] = self.locked
186            check_results['locked'] = None
187
188        return super(host_list, self).execute(op='get_hosts',
189                                              filters=filters,
190                                              check_results=check_results)
191
192
193    def output(self, results):
194        if results:
195            # Remove the platform from the labels.
196            for result in results:
197                result['labels'] = self._cleanup_labels(result['labels'],
198                                                        result['platform'])
199        if self.hostnames_only:
200            self.print_list(results, key='hostname')
201        else:
202            keys = ['hostname', 'status',
203                    'shard', 'locked', 'lock_reason', 'platform', 'labels']
204            super(host_list, self).output(results, keys=keys)
205
206
207class host_stat(host):
208    """atest host stat --mlist <file>|<hosts>"""
209    usage_action = 'stat'
210
211    def execute(self):
212        results = []
213        # Convert wildcards into real host stats.
214        existing_hosts = []
215        for host in self.hosts:
216            if host.endswith('*'):
217                stats = self.execute_rpc('get_hosts',
218                                         hostname__startswith=host.rstrip('*'))
219                if len(stats) == 0:
220                    self.failure('No hosts matching %s' % host, item=host,
221                                 what_failed='Failed to stat')
222                    continue
223            else:
224                stats = self.execute_rpc('get_hosts', hostname=host)
225                if len(stats) == 0:
226                    self.failure('Unknown host %s' % host, item=host,
227                                 what_failed='Failed to stat')
228                    continue
229            existing_hosts.extend(stats)
230
231        for stat in existing_hosts:
232            host = stat['hostname']
233            # The host exists, these should succeed
234            acls = self.execute_rpc('get_acl_groups', hosts__hostname=host)
235
236            labels = self.execute_rpc('get_labels', host__hostname=host)
237            results.append([[stat], acls, labels, stat['attributes']])
238        return results
239
240
241    def output(self, results):
242        for stats, acls, labels, attributes in results:
243            print '-'*5
244            self.print_fields(stats,
245                              keys=['hostname', 'platform',
246                                    'status', 'locked', 'locked_by',
247                                    'lock_time', 'lock_reason', 'protection',])
248            self.print_by_ids(acls, 'ACLs', line_before=True)
249            labels = self._cleanup_labels(labels)
250            self.print_by_ids(labels, 'Labels', line_before=True)
251            self.print_dict(attributes, 'Host Attributes', line_before=True)
252
253
254class host_jobs(host):
255    """atest host jobs [--max-query] --mlist <file>|<hosts>"""
256    usage_action = 'jobs'
257
258    def __init__(self):
259        super(host_jobs, self).__init__()
260        self.parser.add_option('-q', '--max-query',
261                               help='Limits the number of results '
262                               '(20 by default)',
263                               type='int', default=20)
264
265
266    def parse(self):
267        """Consume the specific options"""
268        (options, leftover) = super(host_jobs, self).parse()
269        self.max_queries = options.max_query
270        return (options, leftover)
271
272
273    def execute(self):
274        results = []
275        real_hosts = []
276        for host in self.hosts:
277            if host.endswith('*'):
278                stats = self.execute_rpc('get_hosts',
279                                         hostname__startswith=host.rstrip('*'))
280                if len(stats) == 0:
281                    self.failure('No host matching %s' % host, item=host,
282                                 what_failed='Failed to stat')
283                [real_hosts.append(stat['hostname']) for stat in stats]
284            else:
285                real_hosts.append(host)
286
287        for host in real_hosts:
288            queue_entries = self.execute_rpc('get_host_queue_entries',
289                                             host__hostname=host,
290                                             query_limit=self.max_queries,
291                                             sort_by=['-job__id'])
292            jobs = []
293            for entry in queue_entries:
294                job = {'job_id': entry['job']['id'],
295                       'job_owner': entry['job']['owner'],
296                       'job_name': entry['job']['name'],
297                       'status': entry['status']}
298                jobs.append(job)
299            results.append((host, jobs))
300        return results
301
302
303    def output(self, results):
304        for host, jobs in results:
305            print '-'*5
306            print 'Hostname: %s' % host
307            self.print_table(jobs, keys_header=['job_id',
308                                                'job_owner',
309                                                'job_name',
310                                                'status'])
311
312
313class host_mod(host):
314    """atest host mod --lock|--unlock|--force_modify_locking|--protection
315    --mlist <file>|<hosts>"""
316    usage_action = 'mod'
317    attribute_regex = r'^(?P<attribute>\w+)=(?P<value>.+)?'
318
319    def __init__(self):
320        """Add the options specific to the mod action"""
321        self.data = {}
322        self.messages = []
323        self.attribute = None
324        self.value = None
325        super(host_mod, self).__init__()
326        self.parser.add_option('-l', '--lock',
327                               help='Lock hosts',
328                               action='store_true')
329        self.parser.add_option('-u', '--unlock',
330                               help='Unlock hosts',
331                               action='store_true')
332        self.parser.add_option('-f', '--force_modify_locking',
333                               help='Forcefully lock\unlock a host',
334                               action='store_true')
335        self.parser.add_option('-r', '--lock_reason',
336                               help='Reason for locking hosts',
337                               default='')
338        self.parser.add_option('-p', '--protection', type='choice',
339                               help=('Set the protection level on a host.  '
340                                     'Must be one of: %s' %
341                                     ', '.join('"%s"' % p
342                                               for p in self.protections)),
343                               choices=self.protections)
344        self.parser.add_option('--attribute', '-a', default='',
345                               help=('Host attribute to add or change. Format '
346                                     'is <attribute>=<value>. Value can be '
347                                     'blank to delete attribute.'))
348
349
350    def parse(self):
351        """Consume the specific options"""
352        (options, leftover) = super(host_mod, self).parse()
353
354        self._parse_lock_options(options)
355        if options.force_modify_locking:
356             self.data['force_modify_locking'] = True
357
358        if options.protection:
359            self.data['protection'] = options.protection
360            self.messages.append('Protection set to "%s"' % options.protection)
361
362        if len(self.data) == 0 and not options.attribute:
363            self.invalid_syntax('No modification requested')
364
365        if options.attribute:
366            match = re.match(self.attribute_regex, options.attribute)
367            if not match:
368                self.invalid_syntax('Attributes must be in <attribute>=<value>'
369                                    ' syntax!')
370
371            self.attribute = match.group('attribute')
372            self.value = match.group('value')
373
374        return (options, leftover)
375
376
377    def execute(self):
378        successes = []
379        for host in self.hosts:
380            try:
381                res = self.execute_rpc('modify_host', item=host,
382                                       id=host, **self.data)
383                if self.attribute:
384                    self.execute_rpc('set_host_attribute',
385                                     attribute=self.attribute,
386                                     value=self.value, hostname=host)
387                # TODO: Make the AFE return True or False,
388                # especially for lock
389                successes.append(host)
390            except topic_common.CliError, full_error:
391                # Already logged by execute_rpc()
392                pass
393
394        return successes
395
396
397    def output(self, hosts):
398        for msg in self.messages:
399            self.print_wrapped(msg, hosts)
400
401
402class host_create(host):
403    """atest host create [--lock|--unlock --platform <arch>
404    --labels <labels>|--blist <label_file>
405    --acls <acls>|--alist <acl_file>
406    --protection <protection_type>
407    --mlist <mach_file>] <hosts>"""
408    usage_action = 'create'
409
410    def __init__(self):
411        self.messages = []
412        super(host_create, self).__init__()
413        self.parser.add_option('-l', '--lock',
414                               help='Create the hosts as locked',
415                               action='store_true', default=False)
416        self.parser.add_option('-u', '--unlock',
417                               help='Create the hosts as '
418                               'unlocked (default)',
419                               action='store_true')
420        self.parser.add_option('-r', '--lock_reason',
421                               help='Reason for locking hosts',
422                               default='')
423        self.parser.add_option('-t', '--platform',
424                               help='Sets the platform label')
425        self.parser.add_option('-b', '--labels',
426                               help='Comma separated list of labels')
427        self.parser.add_option('-B', '--blist',
428                               help='File listing the labels',
429                               type='string',
430                               metavar='LABEL_FLIST')
431        self.parser.add_option('-a', '--acls',
432                               help='Comma separated list of ACLs')
433        self.parser.add_option('-A', '--alist',
434                               help='File listing the acls',
435                               type='string',
436                               metavar='ACL_FLIST')
437        self.parser.add_option('-p', '--protection', type='choice',
438                               help=('Set the protection level on a host.  '
439                                     'Must be one of: %s' %
440                                     ', '.join('"%s"' % p
441                                               for p in self.protections)),
442                               choices=self.protections)
443        self.parser.add_option('-s', '--serials',
444                               help=('Comma separated list of adb-based device '
445                                     'serials'))
446
447
448    def parse(self):
449        label_info = topic_common.item_parse_info(attribute_name='labels',
450                                                 inline_option='labels',
451                                                 filename_option='blist')
452        acl_info = topic_common.item_parse_info(attribute_name='acls',
453                                                inline_option='acls',
454                                                filename_option='alist')
455
456        (options, leftover) = super(host_create, self).parse([label_info,
457                                                              acl_info],
458                                                             req_items='hosts')
459
460        self._parse_lock_options(options)
461        self.locked = options.lock
462        self.platform = getattr(options, 'platform', None)
463        self.serials = getattr(options, 'serials', None)
464        if self.serials:
465            if len(self.hosts) > 1:
466                raise topic_common.CliError('Can not specify serials with '
467                                            'multiple hosts')
468            self.serials = self.serials.split(',')
469        if options.protection:
470            self.data['protection'] = options.protection
471        return (options, leftover)
472
473
474    def _execute_add_one_host(self, host):
475        # Always add the hosts as locked to avoid the host
476        # being picked up by the scheduler before it's ACL'ed.
477        # We enforce lock reasons for each lock, so we
478        # provide a 'dummy' if we are intending to unlock after.
479        self.data['locked'] = True
480        if not self.locked:
481            self.data['lock_reason'] = 'Forced lock on device creation'
482        self.execute_rpc('add_host', hostname=host,
483                         status="Ready", **self.data)
484
485        # Now add the platform label
486        labels = self.labels[:]
487        if self.platform:
488            labels.append(self.platform)
489        if len (labels):
490            self.execute_rpc('host_add_labels', id=host, labels=labels)
491
492
493    def _execute_add_hosts(self):
494        successful_hosts = self.site_create_hosts_hook()
495
496        if successful_hosts:
497            for acl in self.acls:
498                self.execute_rpc('acl_group_add_hosts',
499                                 id=acl,
500                                 hosts=successful_hosts)
501
502            if not self.locked:
503                for host in successful_hosts:
504                    self.execute_rpc('modify_host', id=host, locked=False,
505                                     lock_reason='')
506        return successful_hosts
507
508
509    def execute(self):
510        # We need to check if these labels & ACLs exist,
511        # and create them if not.
512        if self.platform:
513            self.check_and_create_items('get_labels', 'add_label',
514                                        [self.platform],
515                                        platform=True)
516
517        if self.labels:
518            self.check_and_create_items('get_labels', 'add_label',
519                                        self.labels,
520                                        platform=False)
521
522        if self.acls:
523            self.check_and_create_items('get_acl_groups',
524                                        'add_acl_group',
525                                        self.acls)
526
527        return self._execute_add_hosts()
528
529
530    def site_create_hosts_hook(self):
531        successful_hosts = []
532        for host in self.hosts:
533            try:
534                self._execute_add_one_host(host)
535                successful_hosts.append(host)
536            except topic_common.CliError:
537                pass
538
539        return successful_hosts
540
541
542    def output(self, hosts):
543        self.print_wrapped('Added host', hosts)
544
545
546class host_delete(action_common.atest_delete, host):
547    """atest host delete [--mlist <mach_file>] <hosts>"""
548    pass
549