1# Copyright 2014 The Chromium OS Authors. All rights reserved. 2# Use of this source code is governed by a BSD-style license that can be 3# found in the LICENSE file. 4 5""" 6The server module contains the objects and methods used to manage servers in 7Autotest. 8 9The valid actions are: 10list: list all servers in the database 11create: create a server 12delete: deletes a server 13modify: modify a server's role or status. 14 15The common options are: 16--role / -r: role that's related to server actions. 17 18See topic_common.py for a High Level Design and Algorithm. 19""" 20 21import common 22 23from autotest_lib.cli import action_common 24from autotest_lib.cli import topic_common 25from autotest_lib.client.common_lib import error 26from autotest_lib.client.common_lib import global_config 27# The django setup is moved here as test_that uses sqlite setup. If this line 28# is in server_manager, test_that unittest will fail. 29from autotest_lib.frontend import setup_django_environment 30from autotest_lib.site_utils import server_manager 31from autotest_lib.site_utils import server_manager_utils 32 33RESPECT_SKYLAB_SERVERDB = global_config.global_config.get_config_value( 34 'SKYLAB', 'respect_skylab_serverdb', type=bool, default=False) 35ATEST_DISABLE_MSG = ('Updating server_db via atest server command has been ' 36 'disabled. Please use use go/cros-infra-inventory-tool ' 37 'to update it in skylab inventory service.') 38 39 40class server(topic_common.atest): 41 """Server class 42 43 atest server [list|create|delete|modify] <options> 44 """ 45 usage_action = '[list|create|delete|modify]' 46 topic = msg_topic = 'server' 47 msg_items = '<server>' 48 49 def __init__(self, hostname_required=True): 50 """Add to the parser the options common to all the server actions. 51 52 @param hostname_required: True to require the command has hostname 53 specified. Default is True. 54 """ 55 super(server, self).__init__() 56 57 self.parser.add_option('-r', '--role', 58 help='Name of a role', 59 type='string', 60 default=None, 61 metavar='ROLE') 62 self.parser.add_option('-x', '--action', 63 help=('Set to True to apply actions when role ' 64 'or status is changed, e.g., restart ' 65 'scheduler when a drone is removed.'), 66 action='store_true', 67 default=False, 68 metavar='ACTION') 69 70 self.topic_parse_info = topic_common.item_parse_info( 71 attribute_name='hostname', use_leftover=True) 72 73 self.hostname_required = hostname_required 74 75 76 def parse(self): 77 """Parse command arguments. 78 """ 79 role_info = topic_common.item_parse_info(attribute_name='role') 80 kwargs = {} 81 if self.hostname_required: 82 kwargs['req_items'] = 'hostname' 83 (options, leftover) = super(server, self).parse([role_info], **kwargs) 84 if options.web_server: 85 self.invalid_syntax('Server actions will access server database ' 86 'defined in your local global config. It does ' 87 'not rely on RPC, no autotest server needs to ' 88 'be specified.') 89 90 # self.hostname is a list. Action on server only needs one hostname at 91 # most. 92 if ((not self.hostname and self.hostname_required) or 93 len(self.hostname) > 1): 94 self.invalid_syntax('`server` topic can only manipulate 1 server. ' 95 'Use -h to see available options.') 96 if self.hostname: 97 # Override self.hostname with the first hostname in the list. 98 self.hostname = self.hostname[0] 99 self.role = options.role 100 return (options, leftover) 101 102 103 def output(self, results): 104 """Display output. 105 106 For most actions, the return is a string message, no formating needed. 107 108 @param results: return of the execute call. 109 """ 110 print results 111 112 113class server_help(server): 114 """Just here to get the atest logic working. Usage is set by its parent. 115 """ 116 pass 117 118 119class server_list(action_common.atest_list, server): 120 """atest server list [--role <role>]""" 121 122 def __init__(self): 123 """Initializer. 124 """ 125 super(server_list, self).__init__(hostname_required=False) 126 self.parser.add_option('-t', '--table', 127 help=('List details of all servers in a table, ' 128 'e.g., \tHostname | Status | Roles | ' 129 'note\t\tserver1 | primary | scheduler | ' 130 'lab'), 131 action='store_true', 132 default=False) 133 self.parser.add_option('-s', '--status', 134 help='Only show servers with given status', 135 type='string', 136 default=None, 137 metavar='STATUS') 138 self.parser.add_option('-u', '--summary', 139 help=('Show the summary of roles and status ' 140 'only, e.g.,\tscheduler: server1(primary) ' 141 'server2(backup)\t\tdrone: server3(primary' 142 ') server4(backup)'), 143 action='store_true', 144 default=False) 145 self.parser.add_option('--json', 146 help='Format output as JSON.', 147 action='store_true', 148 default=False) 149 self.parser.add_option('-N', '--hostnames-only', 150 help='Only return hostnames.', 151 action='store_true', 152 default=False) 153 154 155 def parse(self): 156 """Parse command arguments. 157 """ 158 (options, leftover) = super(server_list, self).parse() 159 self.json = options.json 160 self.table = options.table 161 self.status = options.status 162 self.summary = options.summary 163 self.namesonly = options.hostnames_only 164 if sum([self.table, self.summary, self.json, self.namesonly]) > 1: 165 self.invalid_syntax('May only specify up to 1 output-format flag.') 166 return (options, leftover) 167 168 169 def execute(self): 170 """Execute the command. 171 172 @return: A list of servers matched given hostname and role. 173 """ 174 try: 175 return server_manager_utils.get_servers(hostname=self.hostname, 176 role=self.role, 177 status=self.status) 178 except (server_manager_utils.ServerActionError, 179 error.InvalidDataError) as e: 180 self.failure(e, what_failed='Failed to find servers', 181 item=self.hostname, fatal=True) 182 183 184 def output(self, results): 185 """Display output. 186 187 @param results: return of the execute call, a list of server object that 188 contains server information. 189 """ 190 if results: 191 if self.json: 192 formatter = server_manager_utils.format_servers_json 193 elif self.table: 194 formatter = server_manager_utils.format_servers_table 195 elif self.summary: 196 formatter = server_manager_utils.format_servers_summary 197 elif self.namesonly: 198 formatter = server_manager_utils.format_servers_nameonly 199 else: 200 formatter = server_manager_utils.format_servers 201 print formatter(results) 202 else: 203 self.failure('No server is found.', 204 what_failed='Failed to find servers', 205 item=self.hostname, fatal=True) 206 207 208class server_create(server): 209 """atest server create hostname --role <role> --note <note> 210 """ 211 212 def __init__(self): 213 """Initializer. 214 """ 215 super(server_create, self).__init__() 216 self.parser.add_option('-n', '--note', 217 help='note of the server', 218 type='string', 219 default=None, 220 metavar='NOTE') 221 222 223 def parse(self): 224 """Parse command arguments. 225 """ 226 (options, leftover) = super(server_create, self).parse() 227 self.note = options.note 228 229 if not self.role: 230 self.invalid_syntax('--role is required to create a server.') 231 232 return (options, leftover) 233 234 235 def execute(self): 236 """Execute the command. 237 238 @return: A Server object if it is created successfully. 239 """ 240 if RESPECT_SKYLAB_SERVERDB: 241 self.failure(ATEST_DISABLE_MSG, 242 what_failed='Failed to create server', 243 item=self.hostname, fatal=True) 244 245 try: 246 return server_manager.create(hostname=self.hostname, role=self.role, 247 note=self.note) 248 except (server_manager_utils.ServerActionError, 249 error.InvalidDataError) as e: 250 self.failure(e, what_failed='Failed to create server', 251 item=self.hostname, fatal=True) 252 253 254 def output(self, results): 255 """Display output. 256 257 @param results: return of the execute call, a server object that 258 contains server information. 259 """ 260 if results: 261 print 'Server %s is added to server database:\n' % self.hostname 262 print results 263 264 265class server_delete(server): 266 """atest server delete hostname""" 267 268 def execute(self): 269 """Execute the command. 270 271 @return: True if server is deleted successfully. 272 """ 273 if RESPECT_SKYLAB_SERVERDB: 274 self.failure(ATEST_DISABLE_MSG, 275 what_failed='Failed to delete server', 276 item=self.hostname, fatal=True) 277 278 try: 279 server_manager.delete(hostname=self.hostname) 280 return True 281 except (server_manager_utils.ServerActionError, 282 error.InvalidDataError) as e: 283 self.failure(e, what_failed='Failed to delete server', 284 item=self.hostname, fatal=True) 285 286 287 def output(self, results): 288 """Display output. 289 290 @param results: return of the execute call. 291 """ 292 if results: 293 print ('Server %s is deleted from server database successfully.' % 294 self.hostname) 295 296 297class server_modify(server): 298 """atest server modify hostname 299 300 modify action can only change one input at a time. Available inputs are: 301 --status: Status of the server. 302 --note: Note of the server. 303 --role: New role to be added to the server. 304 --delete_role: Existing role to be deleted from the server. 305 """ 306 307 def __init__(self): 308 """Initializer. 309 """ 310 super(server_modify, self).__init__() 311 self.parser.add_option('-s', '--status', 312 help='Status of the server', 313 type='string', 314 metavar='STATUS') 315 self.parser.add_option('-n', '--note', 316 help='Note of the server', 317 type='string', 318 default=None, 319 metavar='NOTE') 320 self.parser.add_option('-d', '--delete', 321 help=('Set to True to delete given role.'), 322 action='store_true', 323 default=False, 324 metavar='DELETE') 325 self.parser.add_option('-a', '--attribute', 326 help='Name of the attribute of the server', 327 type='string', 328 default=None, 329 metavar='ATTRIBUTE') 330 self.parser.add_option('-e', '--value', 331 help='Value for the attribute of the server', 332 type='string', 333 default=None, 334 metavar='VALUE') 335 336 337 def parse(self): 338 """Parse command arguments. 339 """ 340 (options, leftover) = super(server_modify, self).parse() 341 self.status = options.status 342 self.note = options.note 343 self.delete = options.delete 344 self.attribute = options.attribute 345 self.value = options.value 346 self.action = options.action 347 348 # modify supports various options. However, it's safer to limit one 349 # option at a time so no complicated role-dependent logic is needed 350 # to handle scenario that both role and status are changed. 351 # self.parser is optparse, which does not have function in argparse like 352 # add_mutually_exclusive_group. That's why the count is used here. 353 flags = [self.status is not None, self.role is not None, 354 self.attribute is not None, self.note is not None] 355 if flags.count(True) != 1: 356 msg = ('Action modify only support one option at a time. You can ' 357 'try one of following 5 options:\n' 358 '1. --status: Change server\'s status.\n' 359 '2. --note: Change server\'s note.\n' 360 '3. --role with optional -d: Add/delete role from server.\n' 361 '4. --attribute --value: Set/change the value of a ' 362 'server\'s attribute.\n' 363 '5. --attribute -d: Delete the attribute from the ' 364 'server.\n' 365 '\nUse option -h to see a complete list of options.') 366 self.invalid_syntax(msg) 367 if (self.status != None or self.note != None) and self.delete: 368 self.invalid_syntax('--delete does not apply to status or note.') 369 if self.attribute != None and not self.delete and self.value == None: 370 self.invalid_syntax('--attribute must be used with option --value ' 371 'or --delete.') 372 return (options, leftover) 373 374 375 def execute(self): 376 """Execute the command. 377 378 @return: The updated server object if it is modified successfully. 379 """ 380 if RESPECT_SKYLAB_SERVERDB: 381 self.failure(ATEST_DISABLE_MSG, 382 what_failed='Failed to modify server', 383 item=self.hostname, fatal=True) 384 385 try: 386 return server_manager.modify(hostname=self.hostname, role=self.role, 387 status=self.status, delete=self.delete, 388 note=self.note, 389 attribute=self.attribute, 390 value=self.value, action=self.action) 391 except (server_manager_utils.ServerActionError, 392 error.InvalidDataError) as e: 393 self.failure(e, what_failed='Failed to modify server', 394 item=self.hostname, fatal=True) 395 396 397 def output(self, results): 398 """Display output. 399 400 @param results: return of the execute call, which is the updated server 401 object. 402 """ 403 if results: 404 print 'Server %s is modified successfully.' % self.hostname 405 print results 406