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