Always use "details" call in neworks/subnets list
[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         nics=FlagArgument(
268             'Show only the network interfaces of this virtual server',
269             '--nics'),
270         network_id=ValueArgument(
271             'Show the connection details to that network', '--network-id'),
272         vnc=FlagArgument(
273             'Show VNC connection information (valid for a short period)',
274             '--vnc-credentials'),
275         stats=FlagArgument('Get URLs for server statistics', '--stats')
276     )
277
278     @errors.generic.all
279     @errors.cyclades.connection
280     @errors.cyclades.server_id
281     def _run(self, server_id):
282         vm = self.client.get_server_nics(server_id)
283         if self['nics']:
284             self._print(vm.get('attachments', []))
285         elif self['network_id']:
286             self._print(
287                 self.client.get_server_network_nics(
288                     server_id, self['network_id']), self.print_dict)
289         elif self['vnc']:
290             self.error(
291                 '(!) For security reasons, the following credentials are '
292                 'invalidated\nafter a short time period, depending on the '
293                 'server settings\n')
294             self._print(
295                 self.client.get_server_console(server_id), self.print_dict)
296         elif self['stats']:
297             self._print(
298                 self.client.get_server_stats(server_id), self.print_dict)
299         else:
300             uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
301             vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
302             vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
303             self._print(vm, self.print_dict)
304
305     def main(self, server_id):
306         super(self.__class__, self)._run()
307         choose_one = ('nics', 'vnc', 'stats')
308         count = len([a for a in choose_one if self[a]])
309         if count > 1:
310             raise CLIInvalidArgument('Invalid argument compination', details=[
311                 'Arguments %s cannot be used simultaneously' % ', '.join(
312                     [self.arguments[a].lvalue for a in choose_one])])
313         self._run(server_id=server_id)
314
315
316 class PersonalityArgument(KeyValueArgument):
317
318     terms = (
319         ('local-path', 'contents'),
320         ('server-path', 'path'),
321         ('owner', 'owner'),
322         ('group', 'group'),
323         ('mode', 'mode'))
324
325     @property
326     def value(self):
327         return getattr(self, '_value', [])
328
329     @value.setter
330     def value(self, newvalue):
331         if newvalue == self.default:
332             return self.value
333         self._value, input_dict = [], {}
334         for i, terms in enumerate(newvalue):
335             termlist = terms.split(',')
336             if len(termlist) > len(self.terms):
337                 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
338                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
339
340             for k, v in self.terms:
341                 prefix = '%s=' % k
342                 for item in termlist:
343                     if item.lower().startswith(prefix):
344                         input_dict[k] = item[len(k) + 1:]
345                         break
346                     item = None
347                 if item:
348                     termlist.remove(item)
349
350             try:
351                 path = input_dict['local-path']
352             except KeyError:
353                 path = termlist.pop(0)
354                 if not path:
355                     raise CLIInvalidArgument(
356                         '--personality: No local path specified',
357                         details=howto_personality)
358
359             if not exists(path):
360                 raise CLIInvalidArgument(
361                     '--personality: File %s does not exist' % path,
362                     details=howto_personality)
363
364             self._value.append(dict(path=path))
365             with open(expanduser(path)) as f:
366                 self._value[i]['contents'] = b64encode(f.read())
367             for k, v in self.terms[1:]:
368                 try:
369                     self._value[i][v] = input_dict[k]
370                 except KeyError:
371                     try:
372                         self._value[i][v] = termlist.pop(0)
373                     except IndexError:
374                         continue
375                 if k in ('mode', ) and self._value[i][v]:
376                     try:
377                         self._value[i][v] = int(self._value[i][v], 8)
378                     except ValueError as ve:
379                         raise CLIInvalidArgument(
380                             'Personality mode must be in octal', details=[
381                                 '%s' % ve])
382
383
384 class NetworkArgument(RepeatableArgument):
385     """[id=]NETWORK_ID[,[ip=]IP]"""
386
387     @property
388     def value(self):
389         return getattr(self, '_value', self.default)
390
391     @value.setter
392     def value(self, new_value):
393         for v in new_value or []:
394             part1, sep, part2 = v.partition(',')
395             netid, ip = '', ''
396             if part1.startswith('id='):
397                 netid = part1[len('id='):]
398             elif part1.startswith('ip='):
399                 ip = part1[len('ip='):]
400             else:
401                 netid = part1
402             if part2:
403                 if (part2.startswith('id=') and netid) or (
404                         part2.startswith('ip=') and ip):
405                     raise CLIInvalidArgument(
406                         'Invalid network argument %s' % v, details=[
407                         'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
408                 if part2.startswith('id='):
409                     netid = part2[len('id='):]
410                 elif part2.startswith('ip='):
411                     ip = part2[len('ip='):]
412                 elif netid:
413                     ip = part2
414                 else:
415                     netid = part2
416             if not netid:
417                 raise CLIInvalidArgument(
418                     'Invalid network argument %s' % v, details=[
419                     'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
420             self._value = getattr(self, '_value', [])
421             self._value.append(dict(uuid=netid))
422             if ip:
423                 self._value[-1]['fixed_ip'] = ip
424
425
426 @command(server_cmds)
427 class server_create(_init_cyclades, _optional_json, _server_wait):
428     """Create a server (aka Virtual Machine)"""
429
430     arguments = dict(
431         server_name=ValueArgument('The name of the new server', '--name'),
432         flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
433         image_id=ValueArgument('The ID of the hardware image', '--image-id'),
434         personality=PersonalityArgument(
435             (80 * ' ').join(howto_personality), ('-p', '--personality')),
436         wait=FlagArgument('Wait server to build', ('-w', '--wait')),
437         cluster_size=IntArgument(
438             'Create a cluster of servers of this size. In this case, the name'
439             'parameter is the prefix of each server in the cluster (e.g.,'
440             'srv1, srv2, etc.',
441             '--cluster-size'),
442         max_threads=IntArgument(
443             'Max threads in cluster mode (default 1)', '--threads'),
444         network_configuration=NetworkArgument(
445             'Connect server to network: [id=]NETWORK_ID[,[ip=]IP]        . '
446             'Use only NETWORK_ID for private networks.        . '
447             'Use NETWORK_ID,[ip=]IP for networks with IP.        . '
448             'Can be repeated, mutually exclussive with --no-network',
449             '--network'),
450         no_network=FlagArgument(
451             'Do not create any network NICs on the server.        . '
452             'Mutually exclusive to --network        . '
453             'If neither --network or --no-network are used, the default '
454             'network policy is applied. This policy is configured on the '
455             'cloud and kamaki is oblivious to it',
456             '--no-network')
457     )
458     required = ('server_name', 'flavor_id', 'image_id')
459
460     @errors.cyclades.cluster_size
461     def _create_cluster(self, prefix, flavor_id, image_id, size):
462         networks = self['network_configuration'] or (
463             None if self['no_network'] else [])
464         servers = [dict(
465             name='%s%s' % (prefix, i if size > 1 else ''),
466             flavor_id=flavor_id,
467             image_id=image_id,
468             personality=self['personality'],
469             networks=networks) for i in range(1, 1 + size)]
470         if size == 1:
471             return [self.client.create_server(**servers[0])]
472         self.client.MAX_THREADS = int(self['max_threads'] or 1)
473         try:
474             r = self.client.async_run(self.client.create_server, servers)
475             return r
476         except Exception as e:
477             if size == 1:
478                 raise e
479             try:
480                 requested_names = [s['name'] for s in servers]
481                 spawned_servers = [dict(
482                     name=s['name'],
483                     id=s['id']) for s in self.client.list_servers() if (
484                         s['name'] in requested_names)]
485                 self.error('Failed to build %s servers' % size)
486                 self.error('Found %s matching servers:' % len(spawned_servers))
487                 self._print(spawned_servers, out=self._err)
488                 self.error('Check if any of these servers should be removed\n')
489             except Exception as ne:
490                 self.error('Error (%s) while notifying about errors' % ne)
491             finally:
492                 raise e
493
494     @errors.generic.all
495     @errors.cyclades.connection
496     @errors.plankton.id
497     @errors.cyclades.flavor_id
498     def _run(self, name, flavor_id, image_id):
499         for r in self._create_cluster(
500                 name, flavor_id, image_id, size=self['cluster_size'] or 1):
501             if not r:
502                 self.error('Create %s: server response was %s' % (name, r))
503                 continue
504             usernames = self._uuids2usernames(
505                 [r['user_id'], r['tenant_id']])
506             r['user_id'] += ' (%s)' % usernames[r['user_id']]
507             r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
508             self._print(r, self.print_dict)
509             if self['wait']:
510                 self._wait(r['id'], r['status'])
511             self.writeln(' ')
512
513     def main(self):
514         super(self.__class__, self)._run()
515         if self['no_network'] and self['network']:
516             raise CLIInvalidArgument(
517                 'Invalid argument compination', importance=2, details=[
518                 'Arguments %s and %s are mutually exclusive' % (
519                     self.arguments['no_network'].lvalue,
520                     self.arguments['network'].lvalue)])
521         self._run(
522             name=self['server_name'],
523             flavor_id=self['flavor_id'],
524             image_id=self['image_id'])
525
526
527 class FirewallProfileArgument(ValueArgument):
528
529     profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
530
531     @property
532     def value(self):
533         return getattr(self, '_value', None)
534
535     @value.setter
536     def value(self, new_profile):
537         if new_profile:
538             new_profile = new_profile.upper()
539             if new_profile in self.profiles:
540                 self._value = new_profile
541             else:
542                 raise CLIInvalidArgument(
543                     'Invalid firewall profile %s' % new_profile,
544                     details=['Valid values: %s' % ', '.join(self.profiles)])
545
546
547 @command(server_cmds)
548 class server_modify(_init_cyclades, _optional_output_cmd):
549     """Modify attributes of a virtual server"""
550
551     arguments = dict(
552         server_name=ValueArgument('The new name', '--name'),
553         flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
554         firewall_profile=FirewallProfileArgument(
555             'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
556             '--firewall'),
557         metadata_to_set=KeyValueArgument(
558             'Set metadata in key=value form (can be repeated)',
559             '--metadata-set'),
560         metadata_to_delete=RepeatableArgument(
561             'Delete metadata by key (can be repeated)', '--metadata-del')
562     )
563     required = [
564         'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
565         'metadata_to_delete']
566
567     @errors.generic.all
568     @errors.cyclades.connection
569     @errors.cyclades.server_id
570     def _run(self, server_id):
571         if self['server_name']:
572             self.client.update_server_name((server_id), self['server_name'])
573         if self['flavor_id']:
574             self.client.resize_server(server_id, self['flavor_id'])
575         if self['firewall_profile']:
576             self.client.set_firewall_profile(
577                 server_id=server_id, profile=self['firewall_profile'])
578         if self['metadata_to_set']:
579             self.client.update_server_metadata(
580                 server_id, **self['metadata_to_set'])
581         for key in (self['metadata_to_delete'] or []):
582             errors.cyclades.metadata(
583                 self.client.delete_server_metadata)(server_id, key=key)
584         if self['with_output']:
585             self._optional_output(self.client.get_server_details(server_id))
586
587     def main(self, server_id):
588         super(self.__class__, self)._run()
589         self._run(server_id=server_id)
590
591
592 @command(server_cmds)
593 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
594     """Delete a virtual server"""
595
596     arguments = dict(
597         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
598         cluster=FlagArgument(
599             '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
600             'prefix. In that case, the prefix replaces the server id',
601             '--cluster')
602     )
603
604     def _server_ids(self, server_var):
605         if self['cluster']:
606             return [s['id'] for s in self.client.list_servers() if (
607                 s['name'].startswith(server_var))]
608
609         @errors.cyclades.server_id
610         def _check_server_id(self, server_id):
611             return server_id
612
613         return [_check_server_id(self, server_id=server_var), ]
614
615     @errors.generic.all
616     @errors.cyclades.connection
617     def _run(self, server_var):
618         for server_id in self._server_ids(server_var):
619             if self['wait']:
620                 details = self.client.get_server_details(server_id)
621                 status = details['status']
622
623             r = self.client.delete_server(server_id)
624             self._optional_output(r)
625
626             if self['wait']:
627                 self._wait(server_id, status)
628
629     def main(self, server_id_or_cluster_prefix):
630         super(self.__class__, self)._run()
631         self._run(server_id_or_cluster_prefix)
632
633
634 @command(server_cmds)
635 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
636     """Reboot a virtual server"""
637
638     arguments = dict(
639         hard=FlagArgument(
640             'perform a hard reboot (deprecated)', ('-f', '--force')),
641         type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
642         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
643     )
644
645     @errors.generic.all
646     @errors.cyclades.connection
647     @errors.cyclades.server_id
648     def _run(self, server_id):
649         hard_reboot = self['hard']
650         if hard_reboot:
651             self.error(
652                 'WARNING: -f/--force will be deprecated in version 0.12\n'
653                 '\tIn the future, please use --type=hard instead')
654         if self['type']:
655             if self['type'].lower() in ('soft', ):
656                 hard_reboot = False
657             elif self['type'].lower() in ('hard', ):
658                 hard_reboot = True
659             else:
660                 raise CLISyntaxError(
661                     'Invalid reboot type %s' % self['type'],
662                     importance=2, details=[
663                         '--type values are either SOFT (default) or HARD'])
664
665         r = self.client.reboot_server(int(server_id), hard_reboot)
666         self._optional_output(r)
667
668         if self['wait']:
669             self._wait(server_id, 'REBOOT')
670
671     def main(self, server_id):
672         super(self.__class__, self)._run()
673         self._run(server_id=server_id)
674
675
676 @command(server_cmds)
677 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
678     """Start an existing virtual server"""
679
680     arguments = dict(
681         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
682     )
683
684     @errors.generic.all
685     @errors.cyclades.connection
686     @errors.cyclades.server_id
687     def _run(self, server_id):
688         status = 'ACTIVE'
689         if self['wait']:
690             details = self.client.get_server_details(server_id)
691             status = details['status']
692             if status in ('ACTIVE', ):
693                 return
694
695         r = self.client.start_server(int(server_id))
696         self._optional_output(r)
697
698         if self['wait']:
699             self._wait(server_id, status)
700
701     def main(self, server_id):
702         super(self.__class__, self)._run()
703         self._run(server_id=server_id)
704
705
706 @command(server_cmds)
707 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
708     """Shutdown an active virtual server"""
709
710     arguments = dict(
711         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
712     )
713
714     @errors.generic.all
715     @errors.cyclades.connection
716     @errors.cyclades.server_id
717     def _run(self, server_id):
718         status = 'STOPPED'
719         if self['wait']:
720             details = self.client.get_server_details(server_id)
721             status = details['status']
722             if status in ('STOPPED', ):
723                 return
724
725         r = self.client.shutdown_server(int(server_id))
726         self._optional_output(r)
727
728         if self['wait']:
729             self._wait(server_id, status)
730
731     def main(self, server_id):
732         super(self.__class__, self)._run()
733         self._run(server_id=server_id)
734
735
736 @command(server_cmds)
737 class server_nics(_init_cyclades):
738     """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
739
740     def main(self, *args):
741         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
742             'Replaced by',
743             '  [kamaki] server info <SERVER_ID> --nics'])
744
745
746 @command(server_cmds)
747 class server_console(_init_cyclades, _optional_json):
748     """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
749
750     def main(self, *args):
751         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
752             'Replaced by',
753             '  [kamaki] server info <SERVER_ID> --vnc-credentials'])
754
755
756 @command(server_cmds)
757 class server_rename(_init_cyclades, _optional_json):
758     """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
759
760     def main(self, *args):
761         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
762             'Replaced by',
763             '  [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
764
765
766 @command(server_cmds)
767 class server_stats(_init_cyclades, _optional_json):
768     """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
769
770     def main(self, *args):
771         raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
772             'Replaced by',
773             '  [kamaki] server info <SERVER_ID> --stats'])
774
775
776 @command(server_cmds)
777 class server_wait(_init_cyclades, _server_wait):
778     """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
779
780     arguments = dict(
781         timeout=IntArgument(
782             'Wait limit in seconds (default: 60)', '--timeout', default=60)
783     )
784
785     @errors.generic.all
786     @errors.cyclades.connection
787     @errors.cyclades.server_id
788     def _run(self, server_id, current_status):
789         r = self.client.get_server_details(server_id)
790         if r['status'].lower() == current_status.lower():
791             self._wait(server_id, current_status, timeout=self['timeout'])
792         else:
793             self.error(
794                 'Server %s: Cannot wait for status %s, '
795                 'status is already %s' % (
796                     server_id, current_status, r['status']))
797
798     def main(self, server_id, current_status='BUILD'):
799         super(self.__class__, self)._run()
800         self._run(server_id=server_id, current_status=current_status)
801
802
803 @command(flavor_cmds)
804 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
805     """List available hardware flavors"""
806
807     PERMANENTS = ('id', 'name')
808
809     arguments = dict(
810         detail=FlagArgument('show detailed output', ('-l', '--details')),
811         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
812         more=FlagArgument(
813             'output results in pages (-n to set items per page, default 10)',
814             '--more'),
815         enum=FlagArgument('Enumerate results', '--enumerate'),
816         ram=ValueArgument('filter by ram', ('--ram')),
817         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
818         disk=ValueArgument('filter by disk size in GB', ('--disk')),
819         disk_template=ValueArgument(
820             'filter by disk_templace', ('--disk-template'))
821     )
822
823     def _apply_common_filters(self, flavors):
824         common_filters = dict()
825         if self['ram']:
826             common_filters['ram'] = self['ram']
827         if self['vcpus']:
828             common_filters['vcpus'] = self['vcpus']
829         if self['disk']:
830             common_filters['disk'] = self['disk']
831         if self['disk_template']:
832             common_filters['SNF:disk_template'] = self['disk_template']
833         return filter_dicts_by_dict(flavors, common_filters)
834
835     @errors.generic.all
836     @errors.cyclades.connection
837     def _run(self):
838         withcommons = self['ram'] or self['vcpus'] or (
839             self['disk'] or self['disk_template'])
840         detail = self['detail'] or withcommons
841         flavors = self.client.list_flavors(detail)
842         flavors = self._filter_by_name(flavors)
843         flavors = self._filter_by_id(flavors)
844         if withcommons:
845             flavors = self._apply_common_filters(flavors)
846         if not (self['detail'] or (
847                 self['json_output'] or self['output_format'])):
848             remove_from_items(flavors, 'links')
849         if detail and not self['detail']:
850             for flv in flavors:
851                 for key in set(flv).difference(self.PERMANENTS):
852                     flv.pop(key)
853         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
854         self._print(
855             flavors,
856             with_redundancy=self['detail'], with_enumeration=self['enum'],
857             **kwargs)
858         if self['more']:
859             pager(kwargs['out'].getvalue())
860
861     def main(self):
862         super(self.__class__, self)._run()
863         self._run()
864
865
866 @command(flavor_cmds)
867 class flavor_info(_init_cyclades, _optional_json):
868     """Detailed information on a hardware flavor
869     To get a list of available flavors and flavor ids, try /flavor list
870     """
871
872     @errors.generic.all
873     @errors.cyclades.connection
874     @errors.cyclades.flavor_id
875     def _run(self, flavor_id):
876         self._print(
877             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
878
879     def main(self, flavor_id):
880         super(self.__class__, self)._run()
881         self._run(flavor_id=flavor_id)
882
883
884 def _add_name(self, net):
885         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
886         if user_id:
887             uuids.append(user_id)
888         if tenant_id:
889             uuids.append(tenant_id)
890         if uuids:
891             usernames = self._uuids2usernames(uuids)
892             if user_id:
893                 net['user_id'] += ' (%s)' % usernames[user_id]
894             if tenant_id:
895                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]