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