blob: bea1dcbf883d71e717d5d13a053979c3f9e8018c [file] [log] [blame]
Dan Shi784df0c2014-11-26 10:11:15 -08001# 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
Ningning Xia84190b82018-04-16 15:01:40 -070021import logging
22
Dan Shi784df0c2014-11-26 10:11:15 -080023import common
24
25from autotest_lib.cli import action_common
Ningning Xia84190b82018-04-16 15:01:40 -070026from autotest_lib.cli import skylab_utils
Dan Shi784df0c2014-11-26 10:11:15 -080027from autotest_lib.cli import topic_common
28from autotest_lib.client.common_lib import error
Shuqian Zhaodb205af2018-02-28 15:13:03 -080029from autotest_lib.client.common_lib import global_config
Ningning Xia84190b82018-04-16 15:01:40 -070030from autotest_lib.client.common_lib import revision_control
Dan Shib9144a42014-12-01 16:09:32 -080031# The django setup is moved here as test_that uses sqlite setup. If this line
32# is in server_manager, test_that unittest will fail.
Dan Shi56f1ba72014-12-03 19:16:53 -080033from autotest_lib.frontend import setup_django_environment
Dan Shi784df0c2014-11-26 10:11:15 -080034from autotest_lib.site_utils import server_manager
Dan Shi56f1ba72014-12-03 19:16:53 -080035from autotest_lib.site_utils import server_manager_utils
Ningning Xia84190b82018-04-16 15:01:40 -070036from skylab_inventory import text_manager
37from skylab_inventory import translation_utils
38from skylab_inventory.lib import server as skylab_server
39
40
41# TODO(nxia): add an option to set logging level.
42root = logging.getLogger()
43root.setLevel(logging.CRITICAL)
Dan Shi784df0c2014-11-26 10:11:15 -080044
Shuqian Zhaodb205af2018-02-28 15:13:03 -080045RESPECT_SKYLAB_SERVERDB = global_config.global_config.get_config_value(
46 'SKYLAB', 'respect_skylab_serverdb', type=bool, default=False)
47ATEST_DISABLE_MSG = ('Updating server_db via atest server command has been '
48 'disabled. Please use use go/cros-infra-inventory-tool '
49 'to update it in skylab inventory service.')
Ningning Xia84190b82018-04-16 15:01:40 -070050UPLOAD_CL_MSG = ('Please submit the CL uploaded in https://chrome-internal-'
51 'review.googlesource.com/dashboard/self to make the server '
52 'change effective.')
Shuqian Zhaodb205af2018-02-28 15:13:03 -080053
Dan Shi784df0c2014-11-26 10:11:15 -080054
55class server(topic_common.atest):
56 """Server class
57
58 atest server [list|create|delete|modify] <options>
59 """
60 usage_action = '[list|create|delete|modify]'
61 topic = msg_topic = 'server'
62 msg_items = '<server>'
63
64 def __init__(self, hostname_required=True):
65 """Add to the parser the options common to all the server actions.
66
67 @param hostname_required: True to require the command has hostname
68 specified. Default is True.
69 """
70 super(server, self).__init__()
71
72 self.parser.add_option('-r', '--role',
73 help='Name of a role',
74 type='string',
75 default=None,
76 metavar='ROLE')
Dan Shi56f1ba72014-12-03 19:16:53 -080077 self.parser.add_option('-x', '--action',
78 help=('Set to True to apply actions when role '
79 'or status is changed, e.g., restart '
Ningning Xia84190b82018-04-16 15:01:40 -070080 'scheduler when a drone is removed. Note: '
81 'This is NOT supported when --skylab is '
82 'enabled.'),
Dan Shi56f1ba72014-12-03 19:16:53 -080083 action='store_true',
84 default=False,
85 metavar='ACTION')
Dan Shi784df0c2014-11-26 10:11:15 -080086
Ningning Xia84190b82018-04-16 15:01:40 -070087 self.add_skylab_options()
88
Dan Shi784df0c2014-11-26 10:11:15 -080089 self.topic_parse_info = topic_common.item_parse_info(
90 attribute_name='hostname', use_leftover=True)
91
92 self.hostname_required = hostname_required
93
94
95 def parse(self):
96 """Parse command arguments.
97 """
98 role_info = topic_common.item_parse_info(attribute_name='role')
99 kwargs = {}
100 if self.hostname_required:
101 kwargs['req_items'] = 'hostname'
102 (options, leftover) = super(server, self).parse([role_info], **kwargs)
103 if options.web_server:
104 self.invalid_syntax('Server actions will access server database '
105 'defined in your local global config. It does '
106 'not rely on RPC, no autotest server needs to '
107 'be specified.')
108
109 # self.hostname is a list. Action on server only needs one hostname at
110 # most.
111 if ((not self.hostname and self.hostname_required) or
112 len(self.hostname) > 1):
113 self.invalid_syntax('`server` topic can only manipulate 1 server. '
114 'Use -h to see available options.')
115 if self.hostname:
116 # Override self.hostname with the first hostname in the list.
117 self.hostname = self.hostname[0]
118 self.role = options.role
Ningning Xia84190b82018-04-16 15:01:40 -0700119
120 if self.skylab and self.role:
121 translation_utils.validate_server_role(self.role)
122
Dan Shi784df0c2014-11-26 10:11:15 -0800123 return (options, leftover)
124
125
126 def output(self, results):
127 """Display output.
128
129 For most actions, the return is a string message, no formating needed.
130
131 @param results: return of the execute call.
132 """
133 print results
134
135
136class server_help(server):
137 """Just here to get the atest logic working. Usage is set by its parent.
138 """
139 pass
140
141
142class server_list(action_common.atest_list, server):
143 """atest server list [--role <role>]"""
144
145 def __init__(self):
146 """Initializer.
147 """
148 super(server_list, self).__init__(hostname_required=False)
Ningning Xia84190b82018-04-16 15:01:40 -0700149 warn_message_for_skylab = 'This is not supported with --skylab.'
150
Dan Shi784df0c2014-11-26 10:11:15 -0800151 self.parser.add_option('-t', '--table',
152 help=('List details of all servers in a table, '
153 'e.g., \tHostname | Status | Roles | '
154 'note\t\tserver1 | primary | scheduler | '
Ningning Xia84190b82018-04-16 15:01:40 -0700155 'lab. %s' % warn_message_for_skylab),
Dan Shi784df0c2014-11-26 10:11:15 -0800156 action='store_true',
Allen Lica17e7c2016-10-27 15:37:17 -0700157 default=False)
Dan Shi784df0c2014-11-26 10:11:15 -0800158 self.parser.add_option('-s', '--status',
Ningning Xia84190b82018-04-16 15:01:40 -0700159 help='Only show servers with given status.',
Dan Shi784df0c2014-11-26 10:11:15 -0800160 type='string',
161 default=None,
162 metavar='STATUS')
163 self.parser.add_option('-u', '--summary',
164 help=('Show the summary of roles and status '
165 'only, e.g.,\tscheduler: server1(primary) '
166 'server2(backup)\t\tdrone: server3(primary'
Ningning Xia84190b82018-04-16 15:01:40 -0700167 ') server4(backup). %s' %
168 warn_message_for_skylab),
Dan Shi784df0c2014-11-26 10:11:15 -0800169 action='store_true',
Allen Lica17e7c2016-10-27 15:37:17 -0700170 default=False)
171 self.parser.add_option('--json',
Ningning Xia84190b82018-04-16 15:01:40 -0700172 help=('Format output as JSON. %s' %
173 warn_message_for_skylab),
Allen Lica17e7c2016-10-27 15:37:17 -0700174 action='store_true',
175 default=False)
Aviv Keshete1729bb2017-05-31 13:27:09 -0700176 self.parser.add_option('-N', '--hostnames-only',
Ningning Xia84190b82018-04-16 15:01:40 -0700177 help=('Only return hostnames. %s' %
178 warn_message_for_skylab),
Aviv Keshete1729bb2017-05-31 13:27:09 -0700179 action='store_true',
180 default=False)
Dan Shi784df0c2014-11-26 10:11:15 -0800181
182
183 def parse(self):
184 """Parse command arguments.
185 """
186 (options, leftover) = super(server_list, self).parse()
Allen Lica17e7c2016-10-27 15:37:17 -0700187 self.json = options.json
Dan Shi784df0c2014-11-26 10:11:15 -0800188 self.table = options.table
189 self.status = options.status
190 self.summary = options.summary
Aviv Keshete1729bb2017-05-31 13:27:09 -0700191 self.namesonly = options.hostnames_only
Ningning Xia84190b82018-04-16 15:01:40 -0700192
193 # TODO(nxia): support all formats for skylab inventory.
194 if (self.skylab and (self.json or self.table or self.summary)):
195 self.invalid_syntax('The format (json|summary|json|hostnames-only)'
196 ' is not supported with --skylab.')
197
Aviv Keshete1729bb2017-05-31 13:27:09 -0700198 if sum([self.table, self.summary, self.json, self.namesonly]) > 1:
199 self.invalid_syntax('May only specify up to 1 output-format flag.')
Dan Shi784df0c2014-11-26 10:11:15 -0800200 return (options, leftover)
201
202
203 def execute(self):
204 """Execute the command.
205
206 @return: A list of servers matched given hostname and role.
207 """
Ningning Xia84190b82018-04-16 15:01:40 -0700208 if self.skylab:
209 try:
210 inventory_repo = skylab_utils.InventoryRepo(
211 self.inventory_repo_dir)
212 inventory_repo.initialize()
213 infrastructure = text_manager.load_infrastructure(
214 inventory_repo.get_data_dir(), {self.environment})
215
216 return skylab_server.get_servers(
217 infrastructure,
218 hostname=self.hostname,
219 role=self.role,
220 status=self.status)
221 except (skylab_server.SkylabServerActionError,
222 revision_control.GitError) as e:
223 self.failure(e, what_failed='Failed to list servers from skylab'
224 ' inventory.', item=self.hostname, fatal=True)
225 else:
226 try:
227 return server_manager_utils.get_servers(
228 hostname=self.hostname,
229 role=self.role,
230 status=self.status)
231 except (server_manager_utils.ServerActionError,
232 error.InvalidDataError) as e:
233 self.failure(e, what_failed='Failed to find servers',
234 item=self.hostname, fatal=True)
Dan Shi784df0c2014-11-26 10:11:15 -0800235
236
237 def output(self, results):
238 """Display output.
239
240 @param results: return of the execute call, a list of server object that
241 contains server information.
242 """
Allen Li90a84ea2016-10-27 15:07:42 -0700243 if results:
Allen Lica17e7c2016-10-27 15:37:17 -0700244 if self.json:
245 formatter = server_manager_utils.format_servers_json
246 elif self.table:
Allen Li90a84ea2016-10-27 15:07:42 -0700247 formatter = server_manager_utils.format_servers_table
248 elif self.summary:
249 formatter = server_manager_utils.format_servers_summary
Aviv Keshete1729bb2017-05-31 13:27:09 -0700250 elif self.namesonly:
251 formatter = server_manager_utils.format_servers_nameonly
Allen Li90a84ea2016-10-27 15:07:42 -0700252 else:
253 formatter = server_manager_utils.format_servers
254 print formatter(results)
255 else:
Dan Shi784df0c2014-11-26 10:11:15 -0800256 self.failure('No server is found.',
257 what_failed='Failed to find servers',
258 item=self.hostname, fatal=True)
Dan Shi784df0c2014-11-26 10:11:15 -0800259
260
261class server_create(server):
262 """atest server create hostname --role <role> --note <note>
263 """
264
265 def __init__(self):
266 """Initializer.
267 """
268 super(server_create, self).__init__()
269 self.parser.add_option('-n', '--note',
270 help='note of the server',
271 type='string',
272 default=None,
273 metavar='NOTE')
274
275
276 def parse(self):
277 """Parse command arguments.
278 """
279 (options, leftover) = super(server_create, self).parse()
280 self.note = options.note
281
282 if not self.role:
283 self.invalid_syntax('--role is required to create a server.')
284
285 return (options, leftover)
286
287
Ningning Xia84190b82018-04-16 15:01:40 -0700288 def execute_skylab(self):
289 """Execute the command for skylab inventory changes."""
290 inventory_repo = skylab_utils.InventoryRepo(
291 self.inventory_repo_dir)
292 inventory_repo.initialize()
293 data_dir = inventory_repo.get_data_dir()
294 infrastructure = text_manager.load_infrastructure(
295 data_dir, {self.environment})
296
297 new_server = skylab_server.create(
298 infrastructure,
299 self.hostname,
300 self.environment,
301 role=self.role,
302 note=self.note)
303 text_manager.dump_infrastructure(
304 data_dir, self.environment, infrastructure)
305
306 message = skylab_utils.construct_commit_message(
307 'Add new server: %s' % self.hostname)
308 inventory_repo.git_repo.commit(message)
309 inventory_repo.git_repo.upload_cl(
310 'origin', 'master', draft=self.draft,
311 dryrun=self.dryrun)
312
313 return new_server
314
315
Dan Shi784df0c2014-11-26 10:11:15 -0800316 def execute(self):
317 """Execute the command.
318
319 @return: A Server object if it is created successfully.
320 """
Shuqian Zhaodb205af2018-02-28 15:13:03 -0800321 if RESPECT_SKYLAB_SERVERDB:
322 self.failure(ATEST_DISABLE_MSG,
323 what_failed='Failed to create server',
324 item=self.hostname, fatal=True)
325
Ningning Xia84190b82018-04-16 15:01:40 -0700326 if self.skylab:
327 try:
328 return self.execute_skylab()
329 except (skylab_server.SkylabServerActionError,
330 revision_control.GitError) as e:
331 self.failure(e, what_failed='Failed to create server in skylab'
332 'inventory.', item=self.hostname, fatal=True)
333 else:
334 try:
335 return server_manager.create(
336 hostname=self.hostname,
337 role=self.role,
338 note=self.note)
339 except (server_manager_utils.ServerActionError,
340 error.InvalidDataError) as e:
341 self.failure(e, what_failed='Failed to create server',
342 item=self.hostname, fatal=True)
Dan Shi784df0c2014-11-26 10:11:15 -0800343
344
345 def output(self, results):
346 """Display output.
347
348 @param results: return of the execute call, a server object that
349 contains server information.
350 """
351 if results:
Ningning Xia84190b82018-04-16 15:01:40 -0700352 print 'Server %s is added.\n' % self.hostname
Dan Shi784df0c2014-11-26 10:11:15 -0800353 print results
354
Ningning Xia84190b82018-04-16 15:01:40 -0700355 if self.skylab and not self.dryrun:
356 print UPLOAD_CL_MSG
357
Dan Shi784df0c2014-11-26 10:11:15 -0800358
359class server_delete(server):
360 """atest server delete hostname"""
361
Ningning Xia84190b82018-04-16 15:01:40 -0700362 def execute_skylab(self):
363 """Execute the command for skylab inventory changes."""
364 inventory_repo = skylab_utils.InventoryRepo(
365 self.inventory_repo_dir)
366 inventory_repo.initialize()
367 data_dir = inventory_repo.get_data_dir()
368 infrastructure = text_manager.load_infrastructure(
369 data_dir, {self.environment})
370
371 skylab_server.delete(infrastructure, self.hostname)
372 text_manager.dump_infrastructure(
373 data_dir, self.environment, infrastructure)
374
375 message = skylab_utils.construct_commit_message(
376 'Delete server: %s' % self.hostname)
377 inventory_repo.git_repo.commit(message)
378 inventory_repo.git_repo.upload_cl(
379 'origin', 'master', draft=self.draft,
380 dryrun=self.dryrun)
381
382
Dan Shi784df0c2014-11-26 10:11:15 -0800383 def execute(self):
384 """Execute the command.
385
386 @return: True if server is deleted successfully.
387 """
Shuqian Zhaodb205af2018-02-28 15:13:03 -0800388 if RESPECT_SKYLAB_SERVERDB:
389 self.failure(ATEST_DISABLE_MSG,
390 what_failed='Failed to delete server',
391 item=self.hostname, fatal=True)
392
Ningning Xia84190b82018-04-16 15:01:40 -0700393 if self.skylab:
394 try:
395 self.execute_skylab()
396 return True
397 except (skylab_server.SkylabServerActionError,
398 revision_control.GitError) as e:
399 self.failure(e, what_failed='Failed to delete server from '
400 'skylab inventory.', item=self.hostname,
401 fatal=True)
402 else:
403 try:
404 server_manager.delete(hostname=self.hostname)
405 return True
406 except (server_manager_utils.ServerActionError,
407 error.InvalidDataError) as e:
408 self.failure(e, what_failed='Failed to delete server',
409 item=self.hostname, fatal=True)
Dan Shi784df0c2014-11-26 10:11:15 -0800410
411
412 def output(self, results):
413 """Display output.
414
415 @param results: return of the execute call.
416 """
417 if results:
Ningning Xia84190b82018-04-16 15:01:40 -0700418 print ('Server %s is deleted.\n' %
Dan Shi784df0c2014-11-26 10:11:15 -0800419 self.hostname)
420
Ningning Xia84190b82018-04-16 15:01:40 -0700421 if self.skylab and not self.dryrun:
422 print UPLOAD_CL_MSG
423
Dan Shi784df0c2014-11-26 10:11:15 -0800424
425class server_modify(server):
426 """atest server modify hostname
427
428 modify action can only change one input at a time. Available inputs are:
429 --status: Status of the server.
430 --note: Note of the server.
431 --role: New role to be added to the server.
432 --delete_role: Existing role to be deleted from the server.
433 """
434
435 def __init__(self):
436 """Initializer.
437 """
438 super(server_modify, self).__init__()
439 self.parser.add_option('-s', '--status',
440 help='Status of the server',
441 type='string',
442 metavar='STATUS')
443 self.parser.add_option('-n', '--note',
444 help='Note of the server',
445 type='string',
446 default=None,
447 metavar='NOTE')
448 self.parser.add_option('-d', '--delete',
449 help=('Set to True to delete given role.'),
450 action='store_true',
451 default=False,
452 metavar='DELETE')
453 self.parser.add_option('-a', '--attribute',
454 help='Name of the attribute of the server',
455 type='string',
456 default=None,
457 metavar='ATTRIBUTE')
458 self.parser.add_option('-e', '--value',
459 help='Value for the attribute of the server',
460 type='string',
461 default=None,
462 metavar='VALUE')
463
464
465 def parse(self):
466 """Parse command arguments.
467 """
468 (options, leftover) = super(server_modify, self).parse()
469 self.status = options.status
470 self.note = options.note
471 self.delete = options.delete
472 self.attribute = options.attribute
473 self.value = options.value
Dan Shi56f1ba72014-12-03 19:16:53 -0800474 self.action = options.action
Dan Shi784df0c2014-11-26 10:11:15 -0800475
476 # modify supports various options. However, it's safer to limit one
477 # option at a time so no complicated role-dependent logic is needed
478 # to handle scenario that both role and status are changed.
479 # self.parser is optparse, which does not have function in argparse like
480 # add_mutually_exclusive_group. That's why the count is used here.
481 flags = [self.status is not None, self.role is not None,
482 self.attribute is not None, self.note is not None]
483 if flags.count(True) != 1:
484 msg = ('Action modify only support one option at a time. You can '
485 'try one of following 5 options:\n'
486 '1. --status: Change server\'s status.\n'
487 '2. --note: Change server\'s note.\n'
488 '3. --role with optional -d: Add/delete role from server.\n'
489 '4. --attribute --value: Set/change the value of a '
490 'server\'s attribute.\n'
491 '5. --attribute -d: Delete the attribute from the '
492 'server.\n'
493 '\nUse option -h to see a complete list of options.')
494 self.invalid_syntax(msg)
495 if (self.status != None or self.note != None) and self.delete:
496 self.invalid_syntax('--delete does not apply to status or note.')
497 if self.attribute != None and not self.delete and self.value == None:
498 self.invalid_syntax('--attribute must be used with option --value '
499 'or --delete.')
Ningning Xia84190b82018-04-16 15:01:40 -0700500
501 # TODO(nxia): crbug.com/832964 support --action with --skylab
502 if self.skylab and self.action:
503 self.invalid_syntax('--action is currently not supported with'
504 ' --skylab.')
505
Dan Shi784df0c2014-11-26 10:11:15 -0800506 return (options, leftover)
507
508
Ningning Xia84190b82018-04-16 15:01:40 -0700509 def execute_skylab(self):
510 """Execute the command for skylab inventory changes."""
511 inventory_repo = skylab_utils.InventoryRepo(
512 self.inventory_repo_dir)
513 inventory_repo.initialize()
514 data_dir = inventory_repo.get_data_dir()
515 infrastructure = text_manager.load_infrastructure(
516 data_dir, {self.environment})
517
518 target_server = skylab_server.modify(
519 infrastructure,
520 self.hostname,
521 role=self.role,
522 status=self.status,
523 delete_role=self.delete,
524 note=self.note,
525 attribute=self.attribute,
526 value=self.value,
527 delete_attribute=self.delete)
528 text_manager.dump_infrastructure(
529 data_dir, self.environment, infrastructure)
530
531 status = inventory_repo.git_repo.status()
532 if not status:
533 print('Nothing is changed for server %s.' % self.hostname)
534 return
535
536 message = skylab_utils.construct_commit_message(
537 'Modify server: %s' % self.hostname)
538 inventory_repo.git_repo.commit(message)
539 inventory_repo.git_repo.upload_cl(
540 'origin', 'master', draft=self.draft,
541 dryrun=self.dryrun)
542
543 return target_server
544
545
Dan Shi784df0c2014-11-26 10:11:15 -0800546 def execute(self):
547 """Execute the command.
548
549 @return: The updated server object if it is modified successfully.
550 """
Shuqian Zhaodb205af2018-02-28 15:13:03 -0800551 if RESPECT_SKYLAB_SERVERDB:
552 self.failure(ATEST_DISABLE_MSG,
553 what_failed='Failed to modify server',
554 item=self.hostname, fatal=True)
555
Ningning Xia84190b82018-04-16 15:01:40 -0700556 if self.skylab:
557 try:
558 return self.execute_skylab()
559 except (skylab_server.SkylabServerActionError,
560 revision_control.GitError) as e:
561 self.failure(e, what_failed='Failed to modify server in skylab'
562 ' inventory.', item=self.hostname, fatal=True)
563 else:
564 try:
565 return server_manager.modify(
566 hostname=self.hostname, role=self.role,
567 status=self.status, delete=self.delete,
568 note=self.note, attribute=self.attribute,
569 value=self.value, action=self.action)
570 except (server_manager_utils.ServerActionError,
571 error.InvalidDataError) as e:
572 self.failure(e, what_failed='Failed to modify server',
573 item=self.hostname, fatal=True)
Dan Shi784df0c2014-11-26 10:11:15 -0800574
575
576 def output(self, results):
577 """Display output.
578
579 @param results: return of the execute call, which is the updated server
580 object.
581 """
582 if results:
Ningning Xia84190b82018-04-16 15:01:40 -0700583 print 'Server %s is modified.\n' % self.hostname
Dan Shi784df0c2014-11-26 10:11:15 -0800584 print results
Ningning Xia84190b82018-04-16 15:01:40 -0700585
586 if self.skylab and not self.dryrun:
587 print UPLOAD_CL_MSG