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