Add --diagnostics to server info
[kamaki] / kamaki / cli / commands / cyclades.py
1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
11 #   2. Redistributions in binary form must reproduce the above
12 #      copyright notice, this list of conditions and the following
13 #      disclaimer in the documentation and/or other materials
14 #      provided with the distribution.
15 #
16 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
17 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
18 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
19 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
20 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
23 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
24 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
25 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
26 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27 # POSSIBILITY OF SUCH DAMAGE.
28 #
29 # The views and conclusions contained in the software and
30 # documentation are those of the authors and should not be
31 # interpreted as representing official policies, either expressed
32 # or implied, of GRNET S.A.
33
34 from base64 import b64encode
35 from os.path import exists, expanduser
36 from io import StringIO
37 from pydoc import pager
38
39 from kamaki.cli import command
40 from kamaki.cli.command_tree import CommandTree
41 from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
42 from kamaki.cli.errors import (
43     raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44 from kamaki.clients.cyclades import CycladesClient
45 from kamaki.cli.argument import (
46     FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47     ProgressBarArgument, DateArgument, IntArgument)
48 from kamaki.cli.commands import _command_init, errors, addLogSettings
49 from kamaki.cli.commands import (
50     _optional_output_cmd, _optional_json, _name_filter, _id_filter)
51
52
53 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
54 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
55 _commands = [server_cmds, flavor_cmds]
56
57
58 about_authentication = '\nUser Authentication:\
59     \n* to check authentication: /user authenticate\
60     \n* to set authentication token: /config set cloud.<cloud>.token <token>'
61
62 howto_personality = [
63     'Defines a file to be injected to virtual servers file system.',
64     'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65     '  [local-path=]PATH: local file to be injected (relative or absolute)',
66     '  [server-path=]SERVER_PATH: destination location inside server Image',
67     '  [owner=]OWNER: virtual servers user id for the remote file',
68     '  [group=]GROUP: virtual servers group id or name for the remote file',
69     '  [mode=]MODE: permission in octal (e.g., 0777)',
70     'e.g., -p /tmp/my.file,owner=root,mode=0777']
71
72
73 class _service_wait(object):
74
75     wait_arguments = dict(
76         progress_bar=ProgressBarArgument(
77             'do not show progress bar', ('-N', '--no-progress-bar'), False)
78     )
79
80     def _wait(
81             self, service, service_id, status_method, current_status,
82             countdown=True, timeout=60):
83         (progress_bar, wait_cb) = self._safe_progress_bar(
84             '%s %s: status is still %s' % (
85                 service, service_id, current_status),
86             countdown=countdown, timeout=timeout)
87
88         try:
89             new_mode = status_method(
90                 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
91             if new_mode:
92                 self.error('%s %s: status is now %s' % (
93                     service, service_id, new_mode))
94             else:
95                 self.error('%s %s: status is still %s' % (
96                     service, service_id, current_status))
97         except KeyboardInterrupt:
98             self.error('\n- canceled')
99         finally:
100             self._safe_progress_bar_finish(progress_bar)
101
102
103 class _server_wait(_service_wait):
104
105     def _wait(self, server_id, current_status, timeout=60):
106         super(_server_wait, self)._wait(
107             'Server', server_id, self.client.wait_server, current_status,
108             countdown=(current_status not in ('BUILD', )),
109             timeout=timeout if current_status not in ('BUILD', ) else 100)
110
111
112 class _init_cyclades(_command_init):
113     @errors.generic.all
114     @addLogSettings
115     def _run(self, service='compute'):
116         if getattr(self, 'cloud', None):
117             base_url = self._custom_url(service) or self._custom_url(
118                 'cyclades')
119             if base_url:
120                 token = self._custom_token(service) or self._custom_token(
121                     'cyclades') or self.config.get_cloud('token')
122                 self.client = CycladesClient(base_url=base_url, token=token)
123                 return
124         else:
125             self.cloud = 'default'
126         if getattr(self, 'auth_base', False):
127             cyclades_endpoints = self.auth_base.get_service_endpoints(
128                 self._custom_type('cyclades') or 'compute',
129                 self._custom_version('cyclades') or '')
130             base_url = cyclades_endpoints['publicURL']
131             token = self.auth_base.token
132             self.client = CycladesClient(base_url=base_url, token=token)
133         else:
134             raise CLIBaseUrlError(service='cyclades')
135
136     def main(self):
137         self._run()
138
139
140 @command(server_cmds)
141 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
142     """List virtual servers accessible by user
143     Use filtering arguments (e.g., --name-like) to manage long server lists
144     """
145
146     PERMANENTS = ('id', 'name')
147
148     arguments = dict(
149         detail=FlagArgument('show detailed output', ('-l', '--details')),
150         since=DateArgument(
151             'show only items since date (\' d/m/Y H:M:S \')',
152             '--since'),
153         limit=IntArgument(
154             'limit number of listed virtual servers', ('-n', '--number')),
155         more=FlagArgument(
156             'output results in pages (-n to set items per page, default 10)',
157             '--more'),
158         enum=FlagArgument('Enumerate results', '--enumerate'),
159         flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
160         image_id=ValueArgument('filter by image id', ('--image-id')),
161         user_id=ValueArgument('filter by user id', ('--user-id')),
162         user_name=ValueArgument('filter by user name', ('--user-name')),
163         status=ValueArgument(
164             'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
165             ('--status')),
166         meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
167         meta_like=KeyValueArgument(
168             'print only if in key=value, the value is part of actual value',
169             ('--metadata-like')),
170     )
171
172     def _add_user_name(self, servers):
173         uuids = self._uuids2usernames(list(set(
174                 [srv['user_id'] for srv in servers] +
175                 [srv['tenant_id'] for srv in servers])))
176         for srv in servers:
177             srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
178             srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
179         return servers
180
181     def _apply_common_filters(self, servers):
182         common_filters = dict()
183         if self['status']:
184             common_filters['status'] = self['status']
185         if self['user_id'] or self['user_name']:
186             uuid = self['user_id'] or self._username2uuid(self['user_name'])
187             common_filters['user_id'] = uuid
188         return filter_dicts_by_dict(servers, common_filters)
189
190     def _filter_by_image(self, servers):
191         iid = self['image_id']
192         return [srv for srv in servers if srv['image']['id'] == iid]
193
194     def _filter_by_flavor(self, servers):
195         fid = self['flavor_id']
196         return [srv for srv in servers if (
197             '%s' % srv['image']['id'] == '%s' % fid)]
198
199     def _filter_by_metadata(self, servers):
200         new_servers = []
201         for srv in servers:
202             if not 'metadata' in srv:
203                 continue
204             meta = [dict(srv['metadata'])]
205             if self['meta']:
206                 meta = filter_dicts_by_dict(meta, self['meta'])
207             if meta and self['meta_like']:
208                 meta = filter_dicts_by_dict(
209                     meta, self['meta_like'], exact_match=False)
210             if meta:
211                 new_servers.append(srv)
212         return new_servers
213
214     @errors.generic.all
215     @errors.cyclades.connection
216     @errors.cyclades.date
217     def _run(self):
218         withimage = bool(self['image_id'])
219         withflavor = bool(self['flavor_id'])
220         withmeta = bool(self['meta'] or self['meta_like'])
221         withcommons = bool(
222             self['status'] or self['user_id'] or self['user_name'])
223         detail = self['detail'] or (
224             withimage or withflavor or withmeta or withcommons)
225         servers = self.client.list_servers(detail, self['since'])
226
227         servers = self._filter_by_name(servers)
228         servers = self._filter_by_id(servers)
229         servers = self._apply_common_filters(servers)
230         if withimage:
231             servers = self._filter_by_image(servers)
232         if withflavor:
233             servers = self._filter_by_flavor(servers)
234         if withmeta:
235             servers = self._filter_by_metadata(servers)
236
237         if self['detail'] and not (
238                 self['json_output'] or self['output_format']):
239             servers = self._add_user_name(servers)
240         elif not (self['detail'] or (
241                 self['json_output'] or self['output_format'])):
242             remove_from_items(servers, 'links')
243         if detail and not self['detail']:
244             for srv in servers:
245                 for key in set(srv).difference(self.PERMANENTS):
246                     srv.pop(key)
247         kwargs = dict(with_enumeration=self['enum'])
248         if self['more']:
249             kwargs['out'] = StringIO()
250             kwargs['title'] = ()
251         if self['limit']:
252             servers = servers[:self['limit']]
253         self._print(servers, **kwargs)
254         if self['more']:
255             pager(kwargs['out'].getvalue())
256
257     def main(self):
258         super(self.__class__, self)._run()
259         self._run()
260
261
262 @command(server_cmds)
263 class server_info(_init_cyclades, _optional_json):
264     """Detailed information on a Virtual Machine"""
265
266     arguments = dict(
267         addr=FlagArgument(
268             'Show only the network interfaces of this virtual server',
269             '--nics'),
270         vnc=FlagArgument(
271             'Show VNC connection information (valid for a short period)',
272             '--vnc-credentials'),
273         stats=FlagArgument('Get URLs for server statistics', '--stats'),
274         diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
275     )
276
277     @errors.generic.all
278     @errors.cyclades.connection
279     @errors.cyclades.server_id
280     def _run(self, server_id):
281         vm = self.client.get_server_details(server_id)
282         if self['addr']:
283             self._print(vm.get('attachments', []))
284         elif self['vnc']:
285             self.error(
286                 '(!) For security reasons, the following credentials are '
287                 'invalidated\nafter a short time period, depending on the '
288                 'server settings\n')
289             self._print(
290                 self.client.get_server_console(server_id), self.print_dict)
291         elif self['stats']:
292             self._print(
293                 self.client.get_server_stats(server_id), self.print_dict)
294         elif self['diagnostics']:
295             self._print(self.client.get_server_diagnostics(server_id))
296         else:
297             uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
298             vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
299             vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
300             self._print(vm, self.print_dict)
301
302     def main(self, server_id):
303         super(self.__class__, self)._run()
304         choose_one = ('addr', 'vnc', 'stats')
305         count = len([a for a in choose_one if self[a]])
306         if count > 1:
307             raise CLIInvalidArgument('Invalid argument compination', details=[
308                 'Arguments %s cannot be used simultaneously' % ', '.join(
309                     [self.arguments[a].lvalue for a in choose_one])])
310         self._run(server_id=server_id)
311
312
313 class PersonalityArgument(KeyValueArgument):
314
315     terms = (
316         ('local-path', 'contents'),
317         ('server-path', 'path'),
318         ('owner', 'owner'),
319         ('group', 'group'),
320         ('mode', 'mode'))
321
322     @property
323     def value(self):
324         return getattr(self, '_value', [])
325
326     @value.setter
327     def value(self, newvalue):
328         if newvalue == self.default:
329             return self.value
330         self._value, input_dict = [], {}
331         for i, terms in enumerate(newvalue):
332             termlist = terms.split(',')
333             if len(termlist) > len(self.terms):
334                 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
335                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
336
337             for k, v in self.terms:
338                 prefix = '%s=' % k
339                 for item in termlist:
340                     if item.lower().startswith(prefix):
341                         input_dict[k] = item[len(k) + 1:]
342                         break
343                     item = None
344                 if item:
345                     termlist.remove(item)
346
347             try:
348                 path = input_dict['local-path']
349             except KeyError:
350                 path = termlist.pop(0)
351                 if not path:
352                     raise CLIInvalidArgument(
353                         '--personality: No local path specified',
354                         details=howto_personality)
355
356             if not exists(path):
357                 raise CLIInvalidArgument(
358                     '--personality: File %s does not exist' % path,
359                     details=howto_personality)
360
361             self._value.append(dict(path=path))
362             with open(expanduser(path)) as f:
363                 self._value[i]['contents'] = b64encode(f.read())
364             for k, v in self.terms[1:]:
365                 try:
366                     self._value[i][v] = input_dict[k]
367                 except KeyError:
368                     try:
369                         self._value[i][v] = termlist.pop(0)
370                     except IndexError:
371                         continue
372                 if k in ('mode', ) and self._value[i][v]:
373                     try:
374                         self._value[i][v] = int(self._value[i][v], 8)
375                     except ValueError as ve:
376                         raise CLIInvalidArgument(
377                             'Personality mode must be in octal', details=[
378                                 '%s' % ve])
379
380
381 class NetworkArgument(RepeatableArgument):
382     """[id=]NETWORK_ID[,[ip=]IP]"""
383
384     @property
385     def value(self):
386         return getattr(self, '_value', self.default)
387
388     @value.setter
389     def value(self, new_value):
390         for v in new_value or []:
391             part1, sep, part2 = v.partition(',')
392             netid, ip = '', ''
393             if part1.startswith('id='):
394                 netid = part1[len('id='):]
395             elif part1.startswith('ip='):
396                 ip = part1[len('ip='):]
397             else:
398                 netid = part1
399             if part2:
400                 if (part2.startswith('id=') and netid) or (
401                         part2.startswith('ip=') and ip):
402                     raise CLIInvalidArgument(
403                         'Invalid network argument %s' % v, details=[
404                         'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
405                 if part2.startswith('id='):
406                     netid = part2[len('id='):]
407                 elif part2.startswith('ip='):
408                     ip = part2[len('ip='):]
409                 elif netid:
410                     ip = part2
411                 else:
412                     netid = part2
413             if not netid:
414                 raise CLIInvalidArgument(
415                     'Invalid network argument %s' % v, details=[
416                     'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
417             self._value = getattr(self, '_value', [])
418             self._value.append(dict(uuid=netid))
419             if ip:
420                 self._value[-1]['fixed_ip'] = ip
421
422
423 @command(server_cmds)
424 class server_create(_init_cyclades, _optional_json, _server_wait):
425     """Create a server (aka Virtual Machine)"""
426
427     arguments = dict(
428         server_name=ValueArgument('The name of the new server', '--name'),
429         flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
430         image_id=ValueArgument('The ID of the hardware image', '--image-id'),
431         personality=PersonalityArgument(
432             (80 * ' ').join(howto_personality), ('-p', '--personality')),
433         wait=FlagArgument('Wait server to build', ('-w', '--wait')),
434         cluster_size=IntArgument(
435             'Create a cluster of servers of this size. In this case, the name'
436             'parameter is the prefix of each server in the cluster (e.g.,'
437             'srv1, srv2, etc.',
438             '--cluster-size'),
439         max_threads=IntArgument(
440             'Max threads in cluster mode (default 1)', '--threads'),
441         network_configuration=NetworkArgument(
442             'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
443             'Use only NETWORK_ID for private networks.        . '
444             'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
445             'Can be repeated, mutually exclussive with --no-network',
446             '--network'),
447         no_network=FlagArgument(
448             'Do not create any network NICs on the server.        . '
449             'Mutually exclusive to --network        . '
450             'If neither --network or --no-network are used, the default '
451             'network policy is applied. This policy is configured on the '
452             'cloud and kamaki is oblivious to it',
453             '--no-network')
454     )
455     required = ('server_name', 'flavor_id', 'image_id')
456
457     @errors.cyclades.cluster_size
458     def _create_cluster(self, prefix, flavor_id, image_id, size):
459         networks = self['network_configuration'] or (
460             None if self['no_network'] else [])
461         servers = [dict(
462             name='%s%s' % (prefix, i if size > 1 else ''),
463             flavor_id=flavor_id,
464             image_id=image_id,
465             personality=self['personality'],
466             networks=networks) for i in range(1, 1 + size)]
467         if size == 1:
468             return [self.client.create_server(**servers[0])]
469         self.client.MAX_THREADS = int(self['max_threads'] or 1)
470         try:
471             r = self.client.async_run(self.client.create_server, servers)
472             return r
473         except Exception as e:
474             if size == 1:
475                 raise e
476             try:
477                 requested_names = [s['name'] for s in servers]
478                 spawned_servers = [dict(
479                     name=s['name'],
480                     id=s['id']) for s in self.client.list_servers() if (
481                         s['name'] in requested_names)]
482                 self.error('Failed to build %s servers' % size)
483                 self.error('Found %s matching servers:' % len(spawned_servers))
484                 self._print(spawned_servers, out=self._err)
485                 self.error('Check if any of these servers should be removed\n')
486             except Exception as ne:
487                 self.error('Error (%s) while notifying about errors' % ne)
488             finally:
489                 raise e
490
491     @errors.generic.all
492     @errors.cyclades.connection
493     @errors.plankton.id
494     @errors.cyclades.flavor_id
495     def _run(self, name, flavor_id, image_id):
496         for r in self._create_cluster(
497                 name, flavor_id, image_id, size=self['cluster_size'] or 1):
498             if not r:
499                 self.error('Create %s: server response was %s' % (name, r))
500                 continue
501             usernames = self._uuids2usernames(
502                 [r['user_id'], r['tenant_id']])
503             r['user_id'] += ' (%s)' % usernames[r['user_id']]
504             r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
505             self._print(r, self.print_dict)
506             if self['wait']:
507                 self._wait(r['id'], r['status'])
508             self.writeln(' ')
509
510     def main(self):
511         super(self.__class__, self)._run()
512         if self['no_network'] and self['network']:
513             raise CLIInvalidArgument(
514                 'Invalid argument compination', importance=2, details=[
515                 'Arguments %s and %s are mutually exclusive' % (
516                     self.arguments['no_network'].lvalue,
517                     self.arguments['network'].lvalue)])
518         self._run(
519             name=self['server_name'],
520             flavor_id=self['flavor_id'],
521             image_id=self['image_id'])
522
523
524 class FirewallProfileArgument(ValueArgument):
525
526     profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
527
528     @property
529     def value(self):
530         return getattr(self, '_value', None)
531
532     @value.setter
533     def value(self, new_profile):
534         if new_profile:
535             new_profile = new_profile.upper()
536             if new_profile in self.profiles:
537                 self._value = new_profile
538             else:
539                 raise CLIInvalidArgument(
540                     'Invalid firewall profile %s' % new_profile,
541                     details=['Valid values: %s' % ', '.join(self.profiles)])
542
543
544 @command(server_cmds)
545 class server_modify(_init_cyclades, _optional_output_cmd):
546     """Modify attributes of a virtual server"""
547
548     arguments = dict(
549         server_name=ValueArgument('The new name', '--name'),
550         flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
551         firewall_profile=FirewallProfileArgument(
552             'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
553             '--firewall'),
554         metadata_to_set=KeyValueArgument(
555             'Set metadata in key=value form (can be repeated)',
556             '--metadata-set'),
557         metadata_to_delete=RepeatableArgument(
558             'Delete metadata by key (can be repeated)', '--metadata-del')
559     )
560     required = [
561         'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
562         'metadata_to_delete']
563
564     @errors.generic.all
565     @errors.cyclades.connection
566     @errors.cyclades.server_id
567     def _run(self, server_id):
568         if self['server_name']:
569             self.client.update_server_name((server_id), self['server_name'])
570         if self['flavor_id']:
571             self.client.resize_server(server_id, self['flavor_id'])
572         if self['firewall_profile']:
573             self.client.set_firewall_profile(
574                 server_id=server_id, profile=self['firewall_profile'])
575         if self['metadata_to_set']:
576             self.client.update_server_metadata(
577                 server_id, **self['metadata_to_set'])
578         for key in (self['metadata_to_delete'] or []):
579             errors.cyclades.metadata(
580                 self.client.delete_server_metadata)(server_id, key=key)
581         if self['with_output']:
582             self._optional_output(self.client.get_server_details(server_id))
583
584     def main(self, server_id):
585         super(self.__class__, self)._run()
586         self._run(server_id=server_id)
587
588
589 @command(server_cmds)
590 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
591     """Delete a virtual server"""
592
593     arguments = dict(
594         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
595         cluster=FlagArgument(
596             '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
597             'prefix. In that case, the prefix replaces the server id',
598             '--cluster')
599     )
600
601     def _server_ids(self, server_var):
602         if self['cluster']:
603             return [s['id'] for s in self.client.list_servers() if (
604                 s['name'].startswith(server_var))]
605
606         @errors.cyclades.server_id
607         def _check_server_id(self, server_id):
608             return server_id
609
610         return [_check_server_id(self, server_id=server_var), ]
611
612     @errors.generic.all
613     @errors.cyclades.connection
614     def _run(self, server_var):
615         for server_id in self._server_ids(server_var):
616             if self['wait']:
617                 details = self.client.get_server_details(server_id)
618                 status = details['status']
619
620             r = self.client.delete_server(server_id)
621             self._optional_output(r)
622
623             if self['wait']:
624                 self._wait(server_id, status)
625
626     def main(self, server_id_or_cluster_prefix):
627         super(self.__class__, self)._run()
628         self._run(server_id_or_cluster_prefix)
629
630
631 @command(server_cmds)
632 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
633     """Reboot a virtual server"""
634
635     arguments = dict(
636         hard=FlagArgument(
637             'perform a hard reboot (deprecated)', ('-f', '--force')),
638         type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
639         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
640     )
641
642     @errors.generic.all
643     @errors.cyclades.connection
644     @errors.cyclades.server_id
645     def _run(self, server_id):
646         hard_reboot = self['hard']
647         if hard_reboot:
648             self.error(
649                 'WARNING: -f/--force will be deprecated in version 0.12\n'
650                 '\tIn the future, please use --type=hard instead')
651         if self['type']:
652             if self['type'].lower() in ('soft', ):
653                 hard_reboot = False
654             elif self['type'].lower() in ('hard', ):
655                 hard_reboot = True
656             else:
657                 raise CLISyntaxError(
658                     'Invalid reboot type %s' % self['type'],
659                     importance=2, details=[
660                         '--type values are either SOFT (default) or HARD'])
661
662         r = self.client.reboot_server(int(server_id), hard_reboot)
663         self._optional_output(r)
664
665         if self['wait']:
666             self._wait(server_id, 'REBOOT')
667
668     def main(self, server_id):
669         super(self.__class__, self)._run()
670         self._run(server_id=server_id)
671
672
673 @command(server_cmds)
674 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
675     """Start an existing virtual server"""
676
677     arguments = dict(
678         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
679     )
680
681     @errors.generic.all
682     @errors.cyclades.connection
683     @errors.cyclades.server_id
684     def _run(self, server_id):
685         status = 'ACTIVE'
686         if self['wait']:
687             details = self.client.get_server_details(server_id)
688             status = details['status']
689             if status in ('ACTIVE', ):
690                 return
691
692         r = self.client.start_server(int(server_id))
693         self._optional_output(r)
694
695         if self['wait']:
696             self._wait(server_id, status)
697
698     def main(self, server_id):
699         super(self.__class__, self)._run()
700         self._run(server_id=server_id)
701
702
703 @command(server_cmds)
704 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
705     """Shutdown an active virtual server"""
706
707     arguments = dict(
708         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
709     )
710
711     @errors.generic.all
712     @errors.cyclades.connection
713     @errors.cyclades.server_id
714     def _run(self, server_id):
715         status = 'STOPPED'
716         if self['wait']:
717             details = self.client.get_server_details(server_id)
718             status = details['status']
719             if status in ('STOPPED', ):
720                 return
721
722         r = self.client.shutdown_server(int(server_id))
723         self._optional_output(r)
724
725         if self['wait']:
726             self._wait(server_id, status)
727
728     def main(self, server_id):
729         super(self.__class__, self)._run()
730         self._run(server_id=server_id)
731
732
733 @command(server_cmds)
734 class server_addr(_init_cyclades):
735     """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
736
737     def main(self, *args):
738         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
739             'Replaced by',
740             '  [kamaki] server info <SERVER_ID> --nics'])
741
742
743 @command(server_cmds)
744 class server_console(_init_cyclades, _optional_json):
745     """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
746
747     def main(self, *args):
748         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
749             'Replaced by',
750             '  [kamaki] server info <SERVER_ID> --vnc-credentials'])
751
752
753 @command(server_cmds)
754 class server_rename(_init_cyclades, _optional_json):
755     """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
756
757     def main(self, *args):
758         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
759             'Replaced by',
760             '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
761
762
763 @command(server_cmds)
764 class server_stats(_init_cyclades, _optional_json):
765     """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
766
767     def main(self, *args):
768         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
769             'Replaced by',
770             '  [kamaki] server info <SERVER_ID> --stats'])
771
772
773 @command(server_cmds)
774 class server_wait(_init_cyclades, _server_wait):
775     """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
776
777     arguments = dict(
778         timeout=IntArgument(
779             'Wait limit in seconds (default: 60)', '--timeout', default=60)
780     )
781
782     @errors.generic.all
783     @errors.cyclades.connection
784     @errors.cyclades.server_id
785     def _run(self, server_id, current_status):
786         r = self.client.get_server_details(server_id)
787         if r['status'].lower() == current_status.lower():
788             self._wait(server_id, current_status, timeout=self['timeout'])
789         else:
790             self.error(
791                 'Server %s: Cannot wait for status %s, '
792                 'status is already %s' % (
793                     server_id, current_status, r['status']))
794
795     def main(self, server_id, current_status='BUILD'):
796         super(self.__class__, self)._run()
797         self._run(server_id=server_id, current_status=current_status)
798
799
800 @command(flavor_cmds)
801 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
802     """List available hardware flavors"""
803
804     PERMANENTS = ('id', 'name')
805
806     arguments = dict(
807         detail=FlagArgument('show detailed output', ('-l', '--details')),
808         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
809         more=FlagArgument(
810             'output results in pages (-n to set items per page, default 10)',
811             '--more'),
812         enum=FlagArgument('Enumerate results', '--enumerate'),
813         ram=ValueArgument('filter by ram', ('--ram')),
814         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
815         disk=ValueArgument('filter by disk size in GB', ('--disk')),
816         disk_template=ValueArgument(
817             'filter by disk_templace', ('--disk-template'))
818     )
819
820     def _apply_common_filters(self, flavors):
821         common_filters = dict()
822         if self['ram']:
823             common_filters['ram'] = self['ram']
824         if self['vcpus']:
825             common_filters['vcpus'] = self['vcpus']
826         if self['disk']:
827             common_filters['disk'] = self['disk']
828         if self['disk_template']:
829             common_filters['SNF:disk_template'] = self['disk_template']
830         return filter_dicts_by_dict(flavors, common_filters)
831
832     @errors.generic.all
833     @errors.cyclades.connection
834     def _run(self):
835         withcommons = self['ram'] or self['vcpus'] or (
836             self['disk'] or self['disk_template'])
837         detail = self['detail'] or withcommons
838         flavors = self.client.list_flavors(detail)
839         flavors = self._filter_by_name(flavors)
840         flavors = self._filter_by_id(flavors)
841         if withcommons:
842             flavors = self._apply_common_filters(flavors)
843         if not (self['detail'] or (
844                 self['json_output'] or self['output_format'])):
845             remove_from_items(flavors, 'links')
846         if detail and not self['detail']:
847             for flv in flavors:
848                 for key in set(flv).difference(self.PERMANENTS):
849                     flv.pop(key)
850         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
851         self._print(
852             flavors,
853             with_redundancy=self['detail'], with_enumeration=self['enum'],
854             **kwargs)
855         if self['more']:
856             pager(kwargs['out'].getvalue())
857
858     def main(self):
859         super(self.__class__, self)._run()
860         self._run()
861
862
863 @command(flavor_cmds)
864 class flavor_info(_init_cyclades, _optional_json):
865     """Detailed information on a hardware flavor
866     To get a list of available flavors and flavor ids, try /flavor list
867     """
868
869     @errors.generic.all
870     @errors.cyclades.connection
871     @errors.cyclades.flavor_id
872     def _run(self, flavor_id):
873         self._print(
874             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
875
876     def main(self, flavor_id):
877         super(self.__class__, self)._run()
878         self._run(flavor_id=flavor_id)
879
880
881 def _add_name(self, net):
882         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
883         if user_id:
884             uuids.append(user_id)
885         if tenant_id:
886             uuids.append(tenant_id)
887         if uuids:
888             usernames = self._uuids2usernames(uuids)
889             if user_id:
890                 net['user_id'] += ' (%s)' % usernames[user_id]
891             if tenant_id:
892                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]