xref: /aosp_15_r20/external/autotest/cli/server.py (revision 9c5db1993ded3edbeafc8092d69fe5de2ee02df7)
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
21from __future__ import print_function
22
23import common
24
25from autotest_lib.cli import action_common
26from autotest_lib.cli import skylab_utils
27from autotest_lib.cli import topic_common
28from autotest_lib.client.common_lib import error
29from autotest_lib.client.common_lib import revision_control
30# The django setup is moved here as test_that uses sqlite setup. If this line
31# is in server_manager, test_that unittest will fail.
32from autotest_lib.frontend import setup_django_environment
33
34try:
35    from skylab_inventory import text_manager
36    from skylab_inventory import translation_utils
37    from skylab_inventory.lib import server as skylab_server
38except ImportError:
39    pass
40
41
42ATEST_DISABLE_MSG = ('Updating server_db via atest server command has been '
43                     'disabled. Please use use go/cros-infra-inventory-tool '
44                     'to update it in skylab inventory service.')
45
46
47class server(topic_common.atest):
48    """Server class
49
50    atest server [list|create|delete|modify] <options>
51    """
52    usage_action = '[list|create|delete|modify]'
53    topic = msg_topic = 'server'
54    msg_items = '<server>'
55
56    def __init__(self, hostname_required=True, allow_multiple_hostname=False):
57        """Add to the parser the options common to all the server actions.
58
59        @param hostname_required: True to require the command has hostname
60                                  specified. Default is True.
61        """
62        super(server, self).__init__()
63
64        self.parser.add_option('-r', '--role',
65                               help='Name of a role',
66                               type='string',
67                               default=None,
68                               metavar='ROLE')
69        self.parser.add_option('-x', '--action',
70                               help=('Set to True to apply actions when role '
71                                     'or status is changed, e.g., restart '
72                                     'scheduler when a drone is removed. %s' %
73                                     skylab_utils.MSG_INVALID_IN_SKYLAB),
74                               action='store_true',
75                               default=False,
76                               metavar='ACTION')
77
78        self.add_skylab_options(enforce_skylab=True)
79
80        self.topic_parse_info = topic_common.item_parse_info(
81                attribute_name='hostname', use_leftover=True)
82
83        self.hostname_required = hostname_required
84        self.allow_multiple_hostname = allow_multiple_hostname
85
86
87    def parse(self):
88        """Parse command arguments.
89        """
90        role_info = topic_common.item_parse_info(attribute_name='role')
91        kwargs = {}
92        if self.hostname_required:
93            kwargs['req_items'] = 'hostname'
94        (options, leftover) = super(server, self).parse([role_info], **kwargs)
95        if options.web_server:
96            self.invalid_syntax('Server actions will access server database '
97                                'defined in your local global config. It does '
98                                'not rely on RPC, no autotest server needs to '
99                                'be specified.')
100
101        # self.hostname is a list. Action on server only needs one hostname at
102        # most.
103        if (not self.hostname and self.hostname_required):
104            self.invalid_syntax('`server` topic requires hostname. '
105                                'Use -h to see available options.')
106
107        if (self.hostname_required and not self.allow_multiple_hostname and
108            len(self.hostname) > 1):
109            self.invalid_syntax('`server` topic can only manipulate 1 server. '
110                                'Use -h to see available options.')
111
112        if self.hostname:
113            if not self.allow_multiple_hostname or not self.skylab:
114                # Only support create multiple servers in skylab.
115                # Override self.hostname with the first hostname in the list.
116                self.hostname = self.hostname[0]
117
118        self.role = options.role
119
120        if self.skylab and self.role:
121            translation_utils.validate_server_role(self.role)
122
123        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)
149
150        self.parser.add_option('-s', '--status',
151                               help='Only show servers with given status.',
152                               type='string',
153                               default=None,
154                               metavar='STATUS')
155        self.parser.add_option('--json',
156                               help=('Format output as JSON.'),
157                               action='store_true',
158                               default=False)
159        self.parser.add_option('-N', '--hostnames-only',
160                               help=('Only return hostnames.'),
161                               action='store_true',
162                               default=False)
163        # TODO(crbug.com/850344): support '--table' and '--summary' formats.
164
165
166    def parse(self):
167        """Parse command arguments.
168        """
169        (options, leftover) = super(server_list, self).parse()
170        self.json = options.json
171        self.status = options.status
172        self.namesonly = options.hostnames_only
173
174        if sum([self.json, self.namesonly]) > 1:
175            self.invalid_syntax('May only specify up to 1 output-format flag.')
176        return (options, leftover)
177
178
179    def execute_skylab(self):
180        """Execute 'atest server list --skylab'
181
182        @return: A list of servers matched the given hostname and role.
183        """
184        inventory_repo = skylab_utils.InventoryRepo(
185                        self.inventory_repo_dir)
186        inventory_repo.initialize()
187        infrastructure = text_manager.load_infrastructure(
188                inventory_repo.get_data_dir())
189
190        return skylab_server.get_servers(
191                infrastructure,
192                self.environment,
193                hostname=self.hostname,
194                role=self.role,
195                status=self.status)
196
197
198    def execute(self):
199        """Execute the command.
200
201        @return: A list of servers matched given hostname and role.
202        """
203        if self.skylab:
204            try:
205                return self.execute_skylab()
206            except (skylab_server.SkylabServerActionError,
207                    revision_control.GitError,
208                    skylab_utils.InventoryRepoDirNotClean) as e:
209                self.failure(e, what_failed='Failed to list servers from skylab'
210                             ' inventory.', item=self.hostname, fatal=True)
211        else:
212            return None
213
214
215    def output(self, results):
216        """Display output.
217
218        @param results: return of the execute call, a list of server object that
219                        contains server information.
220        """
221        if results:
222            if self.json:
223                if self.skylab:
224                    formatter = skylab_server.format_servers_json
225                else:
226                    return None
227            elif self.namesonly:
228                return None
229            else:
230                return None
231            print(formatter(results))
232        else:
233            self.failure('No server is found.',
234                         what_failed='Failed to find servers',
235                         item=self.hostname, fatal=True)
236
237
238class server_create(server):
239    """atest server create hostname --role <role> --note <note>
240    """
241
242    def __init__(self):
243        """Initializer.
244        """
245        super(server_create, self).__init__(allow_multiple_hostname=True)
246        self.parser.add_option('-n', '--note',
247                               help='note of the server',
248                               type='string',
249                               default=None,
250                               metavar='NOTE')
251
252
253    def parse(self):
254        """Parse command arguments.
255        """
256        (options, leftover) = super(server_create, self).parse()
257        self.note = options.note
258
259        if not self.role:
260            self.invalid_syntax('--role is required to create a server.')
261
262        return (options, leftover)
263
264
265    def execute_skylab(self):
266        """Execute the command for skylab inventory changes."""
267        inventory_repo = skylab_utils.InventoryRepo(
268                self.inventory_repo_dir)
269        inventory_repo.initialize()
270        data_dir = inventory_repo.get_data_dir()
271        infrastructure = text_manager.load_infrastructure(data_dir)
272
273        new_servers = []
274        for hostname in self.hostname:
275            new_servers.append(skylab_server.create(
276                    infrastructure,
277                    hostname,
278                    self.environment,
279                    role=self.role,
280                    note=self.note))
281        text_manager.dump_infrastructure(data_dir, infrastructure)
282
283        message = skylab_utils.construct_commit_message(
284                'Add new server: %s' % self.hostname)
285        self.change_number = inventory_repo.upload_change(
286                message, draft=self.draft, dryrun=self.dryrun,
287                submit=self.submit)
288
289        return new_servers
290
291
292    def execute(self):
293        """Execute the command.
294
295        @return: A Server object if it is created successfully.
296        """
297        self.failure(ATEST_DISABLE_MSG,
298                     what_failed='Failed to create server',
299                     item=self.hostname,
300                     fatal=True)
301
302
303    def output(self, results):
304        """Display output.
305
306        @param results: return of the execute call, a server object that
307                        contains server information.
308        """
309        if results:
310            print('Server %s is added.\n' % self.hostname)
311            print(results)
312
313            if self.skylab and not self.dryrun and not self.submit:
314                print(skylab_utils.get_cl_message(self.change_number))
315
316
317
318class server_delete(server):
319    """atest server delete hostname"""
320
321    def execute_skylab(self):
322        """Execute the command for skylab inventory changes."""
323        inventory_repo = skylab_utils.InventoryRepo(
324                self.inventory_repo_dir)
325        inventory_repo.initialize()
326        data_dir = inventory_repo.get_data_dir()
327        infrastructure = text_manager.load_infrastructure(data_dir)
328
329        skylab_server.delete(infrastructure, self.hostname, self.environment)
330        text_manager.dump_infrastructure(data_dir, infrastructure)
331
332        message = skylab_utils.construct_commit_message(
333                'Delete server: %s' % self.hostname)
334        self.change_number = inventory_repo.upload_change(
335                message, draft=self.draft, dryrun=self.dryrun,
336                submit=self.submit)
337
338
339    def execute(self):
340        """Execute the command.
341
342        @return: True if server is deleted successfully.
343        """
344        self.failure(ATEST_DISABLE_MSG,
345                     what_failed='Failed to delete server',
346                     item=self.hostname,
347                     fatal=True)
348
349
350    def output(self, results):
351        """Display output.
352
353        @param results: return of the execute call.
354        """
355        if results:
356            print('Server %s is deleted.\n' %
357                   self.hostname)
358
359            if self.skylab and not self.dryrun and not self.submit:
360                print(skylab_utils.get_cl_message(self.change_number))
361
362
363
364class server_modify(server):
365    """atest server modify hostname
366
367    modify action can only change one input at a time. Available inputs are:
368    --status:       Status of the server.
369    --note:         Note of the server.
370    --role:         New role to be added to the server.
371    --delete_role:  Existing role to be deleted from the server.
372    """
373
374    def __init__(self):
375        """Initializer.
376        """
377        super(server_modify, self).__init__()
378        self.parser.add_option('-s', '--status',
379                               help='Status of the server',
380                               type='string',
381                               metavar='STATUS')
382        self.parser.add_option('-n', '--note',
383                               help='Note of the server',
384                               type='string',
385                               default=None,
386                               metavar='NOTE')
387        self.parser.add_option('-d', '--delete',
388                               help=('Set to True to delete given role.'),
389                               action='store_true',
390                               default=False,
391                               metavar='DELETE')
392        self.parser.add_option('-a', '--attribute',
393                               help='Name of the attribute of the server',
394                               type='string',
395                               default=None,
396                               metavar='ATTRIBUTE')
397        self.parser.add_option('-e', '--value',
398                               help='Value for the attribute of the server',
399                               type='string',
400                               default=None,
401                               metavar='VALUE')
402
403
404    def parse(self):
405        """Parse command arguments.
406        """
407        (options, leftover) = super(server_modify, self).parse()
408        self.status = options.status
409        self.note = options.note
410        self.delete = options.delete
411        self.attribute = options.attribute
412        self.value = options.value
413        self.action = options.action
414
415        # modify supports various options. However, it's safer to limit one
416        # option at a time so no complicated role-dependent logic is needed
417        # to handle scenario that both role and status are changed.
418        # self.parser is optparse, which does not have function in argparse like
419        # add_mutually_exclusive_group. That's why the count is used here.
420        flags = [self.status is not None, self.role is not None,
421                 self.attribute is not None, self.note is not None]
422        if flags.count(True) != 1:
423            msg = ('Action modify only support one option at a time. You can '
424                   'try one of following 5 options:\n'
425                   '1. --status:                Change server\'s status.\n'
426                   '2. --note:                  Change server\'s note.\n'
427                   '3. --role with optional -d: Add/delete role from server.\n'
428                   '4. --attribute --value:     Set/change the value of a '
429                   'server\'s attribute.\n'
430                   '5. --attribute -d:          Delete the attribute from the '
431                   'server.\n'
432                   '\nUse option -h to see a complete list of options.')
433            self.invalid_syntax(msg)
434        if (self.status != None or self.note != None) and self.delete:
435            self.invalid_syntax('--delete does not apply to status or note.')
436        if self.attribute != None and not self.delete and self.value == None:
437            self.invalid_syntax('--attribute must be used with option --value '
438                                'or --delete.')
439
440        # TODO(nxia): crbug.com/832964 support --action with --skylab
441        if self.skylab and self.action:
442            self.invalid_syntax('--action is currently not supported with'
443                                ' --skylab.')
444
445        return (options, leftover)
446
447
448    def execute_skylab(self):
449        """Execute the command for skylab inventory changes."""
450        inventory_repo = skylab_utils.InventoryRepo(
451                        self.inventory_repo_dir)
452        inventory_repo.initialize()
453        data_dir = inventory_repo.get_data_dir()
454        infrastructure = text_manager.load_infrastructure(data_dir)
455
456        target_server = skylab_server.modify(
457                infrastructure,
458                self.hostname,
459                self.environment,
460                role=self.role,
461                status=self.status,
462                delete_role=self.delete,
463                note=self.note,
464                attribute=self.attribute,
465                value=self.value,
466                delete_attribute=self.delete)
467        text_manager.dump_infrastructure(data_dir, infrastructure)
468
469        status = inventory_repo.git_repo.status()
470        if not status:
471            print('Nothing is changed for server %s.' % self.hostname)
472            return
473
474        message = skylab_utils.construct_commit_message(
475                'Modify server: %s' % self.hostname)
476        self.change_number = inventory_repo.upload_change(
477                message, draft=self.draft, dryrun=self.dryrun,
478                submit=self.submit)
479
480        return target_server
481
482
483    def execute(self):
484        """Execute the command.
485
486        @return: The updated server object if it is modified successfully.
487        """
488        self.failure(ATEST_DISABLE_MSG,
489                     what_failed='Failed to modify server',
490                     item=self.hostname,
491                     fatal=True)
492
493
494    def output(self, results):
495        """Display output.
496
497        @param results: return of the execute call, which is the updated server
498                        object.
499        """
500        if results:
501            print('Server %s is modified.\n' % self.hostname)
502            print(results)
503
504            if self.skylab and not self.dryrun and not self.submit:
505                print(skylab_utils.get_cl_message(self.change_number))
506