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