Remove all traces of networks from cyclades CLI
[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 _network_wait(_service_wait):
113
114     def _wait(self, net_id, current_status, timeout=60):
115         super(_network_wait, self)._wait(
116             'Network', net_id, self.client.wait_network, current_status,
117             timeout=timeout)
118
119
120 class _firewall_wait(_service_wait):
121
122     def _wait(self, server_id, current_status, timeout=60):
123         super(_firewall_wait, self)._wait(
124             'Firewall of server',
125             server_id, self.client.wait_firewall, current_status,
126             timeout=timeout)
127
128
129 class _init_cyclades(_command_init):
130     @errors.generic.all
131     @addLogSettings
132     def _run(self, service='compute'):
133         if getattr(self, 'cloud', None):
134             base_url = self._custom_url(service) or self._custom_url(
135                 'cyclades')
136             if base_url:
137                 token = self._custom_token(service) or self._custom_token(
138                     'cyclades') or self.config.get_cloud('token')
139                 self.client = CycladesClient(base_url=base_url, token=token)
140                 return
141         else:
142             self.cloud = 'default'
143         if getattr(self, 'auth_base', False):
144             cyclades_endpoints = self.auth_base.get_service_endpoints(
145                 self._custom_type('cyclades') or 'compute',
146                 self._custom_version('cyclades') or '')
147             base_url = cyclades_endpoints['publicURL']
148             token = self.auth_base.token
149             self.client = CycladesClient(base_url=base_url, token=token)
150         else:
151             raise CLIBaseUrlError(service='cyclades')
152
153     def main(self):
154         self._run()
155
156
157 @command(server_cmds)
158 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
159     """List virtual servers accessible by user
160     Use filtering arguments (e.g., --name-like) to manage long server lists
161     """
162
163     PERMANENTS = ('id', 'name')
164
165     arguments = dict(
166         detail=FlagArgument('show detailed output', ('-l', '--details')),
167         since=DateArgument(
168             'show only items since date (\' d/m/Y H:M:S \')',
169             '--since'),
170         limit=IntArgument(
171             'limit number of listed virtual servers', ('-n', '--number')),
172         more=FlagArgument(
173             'output results in pages (-n to set items per page, default 10)',
174             '--more'),
175         enum=FlagArgument('Enumerate results', '--enumerate'),
176         flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
177         image_id=ValueArgument('filter by image id', ('--image-id')),
178         user_id=ValueArgument('filter by user id', ('--user-id')),
179         user_name=ValueArgument('filter by user name', ('--user-name')),
180         status=ValueArgument(
181             'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
182             ('--status')),
183         meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
184         meta_like=KeyValueArgument(
185             'print only if in key=value, the value is part of actual value',
186             ('--metadata-like')),
187     )
188
189     def _add_user_name(self, servers):
190         uuids = self._uuids2usernames(list(set(
191                 [srv['user_id'] for srv in servers] +
192                 [srv['tenant_id'] for srv in servers])))
193         for srv in servers:
194             srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
195             srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
196         return servers
197
198     def _apply_common_filters(self, servers):
199         common_filters = dict()
200         if self['status']:
201             common_filters['status'] = self['status']
202         if self['user_id'] or self['user_name']:
203             uuid = self['user_id'] or self._username2uuid(self['user_name'])
204             common_filters['user_id'] = uuid
205         return filter_dicts_by_dict(servers, common_filters)
206
207     def _filter_by_image(self, servers):
208         iid = self['image_id']
209         return [srv for srv in servers if srv['image']['id'] == iid]
210
211     def _filter_by_flavor(self, servers):
212         fid = self['flavor_id']
213         return [srv for srv in servers if (
214             '%s' % srv['image']['id'] == '%s' % fid)]
215
216     def _filter_by_metadata(self, servers):
217         new_servers = []
218         for srv in servers:
219             if not 'metadata' in srv:
220                 continue
221             meta = [dict(srv['metadata'])]
222             if self['meta']:
223                 meta = filter_dicts_by_dict(meta, self['meta'])
224             if meta and self['meta_like']:
225                 meta = filter_dicts_by_dict(
226                     meta, self['meta_like'], exact_match=False)
227             if meta:
228                 new_servers.append(srv)
229         return new_servers
230
231     @errors.generic.all
232     @errors.cyclades.connection
233     @errors.cyclades.date
234     def _run(self):
235         withimage = bool(self['image_id'])
236         withflavor = bool(self['flavor_id'])
237         withmeta = bool(self['meta'] or self['meta_like'])
238         withcommons = bool(
239             self['status'] or self['user_id'] or self['user_name'])
240         detail = self['detail'] or (
241             withimage or withflavor or withmeta or withcommons)
242         servers = self.client.list_servers(detail, self['since'])
243
244         servers = self._filter_by_name(servers)
245         servers = self._filter_by_id(servers)
246         servers = self._apply_common_filters(servers)
247         if withimage:
248             servers = self._filter_by_image(servers)
249         if withflavor:
250             servers = self._filter_by_flavor(servers)
251         if withmeta:
252             servers = self._filter_by_metadata(servers)
253
254         if self['detail'] and not (
255                 self['json_output'] or self['output_format']):
256             servers = self._add_user_name(servers)
257         elif not (self['detail'] or (
258                 self['json_output'] or self['output_format'])):
259             remove_from_items(servers, 'links')
260         if detail and not self['detail']:
261             for srv in servers:
262                 for key in set(srv).difference(self.PERMANENTS):
263                     srv.pop(key)
264         kwargs = dict(with_enumeration=self['enum'])
265         if self['more']:
266             kwargs['out'] = StringIO()
267             kwargs['title'] = ()
268         if self['limit']:
269             servers = servers[:self['limit']]
270         self._print(servers, **kwargs)
271         if self['more']:
272             pager(kwargs['out'].getvalue())
273
274     def main(self):
275         super(self.__class__, self)._run()
276         self._run()
277
278
279 @command(server_cmds)
280 class server_info(_init_cyclades, _optional_json):
281     """Detailed information on a Virtual Machine
282     Contains:
283     - name, id, status, create/update dates
284     - network interfaces
285     - metadata (e.g., os, superuser) and diagnostics
286     - hardware flavor and os image ids
287     """
288
289     @errors.generic.all
290     @errors.cyclades.connection
291     @errors.cyclades.server_id
292     def _run(self, server_id):
293         vm = self.client.get_server_details(server_id)
294         uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
295         vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296         vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
297         self._print(vm, self.print_dict)
298
299     def main(self, server_id):
300         super(self.__class__, self)._run()
301         self._run(server_id=server_id)
302
303
304 class PersonalityArgument(KeyValueArgument):
305
306     terms = (
307         ('local-path', 'contents'),
308         ('server-path', 'path'),
309         ('owner', 'owner'),
310         ('group', 'group'),
311         ('mode', 'mode'))
312
313     @property
314     def value(self):
315         return getattr(self, '_value', [])
316
317     @value.setter
318     def value(self, newvalue):
319         if newvalue == self.default:
320             return self.value
321         self._value, input_dict = [], {}
322         for i, terms in enumerate(newvalue):
323             termlist = terms.split(',')
324             if len(termlist) > len(self.terms):
325                 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
326                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
327
328             for k, v in self.terms:
329                 prefix = '%s=' % k
330                 for item in termlist:
331                     if item.lower().startswith(prefix):
332                         input_dict[k] = item[len(k) + 1:]
333                         break
334                     item = None
335                 if item:
336                     termlist.remove(item)
337
338             try:
339                 path = input_dict['local-path']
340             except KeyError:
341                 path = termlist.pop(0)
342                 if not path:
343                     raise CLIInvalidArgument(
344                         '--personality: No local path specified',
345                         details=howto_personality)
346
347             if not exists(path):
348                 raise CLIInvalidArgument(
349                     '--personality: File %s does not exist' % path,
350                     details=howto_personality)
351
352             self._value.append(dict(path=path))
353             with open(expanduser(path)) as f:
354                 self._value[i]['contents'] = b64encode(f.read())
355             for k, v in self.terms[1:]:
356                 try:
357                     self._value[i][v] = input_dict[k]
358                 except KeyError:
359                     try:
360                         self._value[i][v] = termlist.pop(0)
361                     except IndexError:
362                         continue
363                 if k in ('mode', ) and self._value[i][v]:
364                     try:
365                         self._value[i][v] = int(self._value[i][v], 8)
366                     except ValueError as ve:
367                         raise CLIInvalidArgument(
368                             'Personality mode must be in octal', details=[
369                                 '%s' % ve])
370
371
372 class NetworkIpArgument(RepeatableArgument):
373
374     @property
375     def value(self):
376         return getattr(self, '_value', [])
377
378     @value.setter
379     def value(self, new_value):
380         for v in (new_value or []):
381             net_and_ip = v.split(',')
382             if len(net_and_ip) < 2:
383                 raise CLIInvalidArgument(
384                     'Value "%s" is missing parts' % v,
385                     details=['Correct format: %s NETWORK_ID,IP' % (
386                         self.parsed_name[0])])
387             self._value = getattr(self, '_value', list())
388             self._value.append(
389                 dict(network=net_and_ip[0], fixed_ip=net_and_ip[1]))
390
391
392 @command(server_cmds)
393 class server_create(_init_cyclades, _optional_json, _server_wait):
394     """Create a server (aka Virtual Machine)"""
395
396     arguments = dict(
397         server_name=ValueArgument('The name of the new server', '--name'),
398         flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
399         image_id=ValueArgument('The ID of the hardware image', '--image-id'),
400         personality=PersonalityArgument(
401             (80 * ' ').join(howto_personality), ('-p', '--personality')),
402         wait=FlagArgument('Wait server to build', ('-w', '--wait')),
403         cluster_size=IntArgument(
404             'Create a cluster of servers of this size. In this case, the name'
405             'parameter is the prefix of each server in the cluster (e.g.,'
406             'srv1, srv2, etc.',
407             '--cluster-size'),
408         max_threads=IntArgument(
409             'Max threads in cluster mode (default 1)', '--threads'),
410         network_id=RepeatableArgument(
411             'Connect server to network (can be repeated)', '--network'),
412         network_id_and_ip=NetworkIpArgument(
413             'Connect server to network w. floating ip ( NETWORK_ID,IP )'
414             '(can be repeated)',
415             '--network-with-ip'),
416         automatic_ip=FlagArgument(
417             'Automatically assign an IP to the server', '--automatic-ip')
418     )
419     required = ('server_name', 'flavor_id', 'image_id')
420
421     @errors.cyclades.cluster_size
422     def _create_cluster(self, prefix, flavor_id, image_id, size):
423         if self['automatic_ip']:
424             networks = []
425         else:
426             networks = [dict(network=netid) for netid in (
427                 (self['network_id'] or []) + (self['network_id_and_ip'] or [])
428             )] or None
429         servers = [dict(
430             name='%s%s' % (prefix, i if size > 1 else ''),
431             flavor_id=flavor_id,
432             image_id=image_id,
433             personality=self['personality'],
434             networks=networks) for i in range(1, 1 + size)]
435         if size == 1:
436             return [self.client.create_server(**servers[0])]
437         self.client.MAX_THREADS = int(self['max_threads'] or 1)
438         try:
439             r = self.client.async_run(self.client.create_server, servers)
440             return r
441         except Exception as e:
442             if size == 1:
443                 raise e
444             try:
445                 requested_names = [s['name'] for s in servers]
446                 spawned_servers = [dict(
447                     name=s['name'],
448                     id=s['id']) for s in self.client.list_servers() if (
449                         s['name'] in requested_names)]
450                 self.error('Failed to build %s servers' % size)
451                 self.error('Found %s matching servers:' % len(spawned_servers))
452                 self._print(spawned_servers, out=self._err)
453                 self.error('Check if any of these servers should be removed\n')
454             except Exception as ne:
455                 self.error('Error (%s) while notifying about errors' % ne)
456             finally:
457                 raise e
458
459     @errors.generic.all
460     @errors.cyclades.connection
461     @errors.plankton.id
462     @errors.cyclades.flavor_id
463     def _run(self, name, flavor_id, image_id):
464         for r in self._create_cluster(
465                 name, flavor_id, image_id, size=self['cluster_size'] or 1):
466             if not r:
467                 self.error('Create %s: server response was %s' % (name, r))
468                 continue
469             usernames = self._uuids2usernames(
470                 [r['user_id'], r['tenant_id']])
471             r['user_id'] += ' (%s)' % usernames[r['user_id']]
472             r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
473             self._print(r, self.print_dict)
474             if self['wait']:
475                 self._wait(r['id'], r['status'])
476             self.writeln(' ')
477
478     def main(self):
479         super(self.__class__, self)._run()
480         if self['automatic_ip'] and (
481                 self['network_id'] or self['network_id_and_ip']):
482             raise CLIInvalidArgument('Invalid argument combination', details=[
483                 'Argument %s should not be combined with other' % (
484                     self.arguments['automatic_ip'].lvalue),
485                 'network-related arguments i.e., %s or %s' % (
486                     self.arguments['network_id'].lvalue,
487                     self.arguments['network_id_and_ip'].lvalue)])
488         self._run(
489             name=self['server_name'],
490             flavor_id=self['flavor_id'],
491             image_id=self['image_id'])
492
493
494 class FirewallProfileArgument(ValueArgument):
495
496     profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
497
498     @property
499     def value(self):
500         return getattr(self, '_value', None)
501
502     @value.setter
503     def value(self, new_profile):
504         if new_profile:
505             new_profile = new_profile.upper()
506             if new_profile in self.profiles:
507                 self._value = new_profile
508             else:
509                 raise CLIInvalidArgument(
510                     'Invalid firewall profile %s' % new_profile,
511                     details=['Valid values: %s' % ', '.join(self.profiles)])
512
513
514 @command(server_cmds)
515 class server_modify(_init_cyclades, _optional_output_cmd):
516     """Modify attributes of a virtual server"""
517
518     arguments = dict(
519         server_name=ValueArgument('The new name', '--name'),
520         flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
521         firewall_profile=FirewallProfileArgument(
522             'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
523             '--firewall'),
524         metadata_to_set=KeyValueArgument(
525             'Set metadata in key=value form (can be repeated)',
526             '--metadata-set'),
527         metadata_to_delete=RepeatableArgument(
528             'Delete metadata by key (can be repeated)', '--metadata-del')
529     )
530     required = [
531         'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
532         'metadata_to_delete']
533
534     @errors.generic.all
535     @errors.cyclades.connection
536     @errors.cyclades.server_id
537     def _run(self, server_id):
538         if self['server_name']:
539             self.client.update_server_name((server_id), self['server_name'])
540         if self['flavor_id']:
541             self.client.resize_server(server_id, self['flavor_id'])
542         if self['firewall_profile']:
543             self.client.set_firewall_profile(
544                 server_id=server_id, profile=self['firewall_profile'])
545         if self['metadata_to_set']:
546             self.client.update_server_metadata(
547                 server_id, **self['metadata_to_set'])
548         for key in self['metadata_to_delete']:
549             errors.cyclades.metadata(
550                 self.client.delete_server_metadata)(server_id, key=key)
551         if self['with_output']:
552             self._optional_output(self.client.get_server_details(server_id))
553
554     def main(self, server_id):
555         super(self.__class__, self)._run()
556         self._run(server_id=server_id)
557
558
559 @command(server_cmds)
560 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
561     """Delete a virtual server"""
562
563     arguments = dict(
564         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
565         cluster=FlagArgument(
566             '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
567             'prefix. In that case, the prefix replaces the server id',
568             '--cluster')
569     )
570
571     def _server_ids(self, server_var):
572         if self['cluster']:
573             return [s['id'] for s in self.client.list_servers() if (
574                 s['name'].startswith(server_var))]
575
576         @errors.cyclades.server_id
577         def _check_server_id(self, server_id):
578             return server_id
579
580         return [_check_server_id(self, server_id=server_var), ]
581
582     @errors.generic.all
583     @errors.cyclades.connection
584     def _run(self, server_var):
585         for server_id in self._server_ids(server_var):
586             if self['wait']:
587                 details = self.client.get_server_details(server_id)
588                 status = details['status']
589
590             r = self.client.delete_server(server_id)
591             self._optional_output(r)
592
593             if self['wait']:
594                 self._wait(server_id, status)
595
596     def main(self, server_id_or_cluster_prefix):
597         super(self.__class__, self)._run()
598         self._run(server_id_or_cluster_prefix)
599
600
601 @command(server_cmds)
602 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
603     """Reboot a virtual server"""
604
605     arguments = dict(
606         hard=FlagArgument(
607             'perform a hard reboot (deprecated)', ('-f', '--force')),
608         type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
609         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
610     )
611
612     @errors.generic.all
613     @errors.cyclades.connection
614     @errors.cyclades.server_id
615     def _run(self, server_id):
616         hard_reboot = self['hard']
617         if hard_reboot:
618             self.error(
619                 'WARNING: -f/--force will be deprecated in version 0.12\n'
620                 '\tIn the future, please use --type=hard instead')
621         if self['type']:
622             if self['type'].lower() in ('soft', ):
623                 hard_reboot = False
624             elif self['type'].lower() in ('hard', ):
625                 hard_reboot = True
626             else:
627                 raise CLISyntaxError(
628                     'Invalid reboot type %s' % self['type'],
629                     importance=2, details=[
630                         '--type values are either SOFT (default) or HARD'])
631
632         r = self.client.reboot_server(int(server_id), hard_reboot)
633         self._optional_output(r)
634
635         if self['wait']:
636             self._wait(server_id, 'REBOOT')
637
638     def main(self, server_id):
639         super(self.__class__, self)._run()
640         self._run(server_id=server_id)
641
642
643 @command(server_cmds)
644 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
645     """Start an existing virtual server"""
646
647     arguments = dict(
648         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
649     )
650
651     @errors.generic.all
652     @errors.cyclades.connection
653     @errors.cyclades.server_id
654     def _run(self, server_id):
655         status = 'ACTIVE'
656         if self['wait']:
657             details = self.client.get_server_details(server_id)
658             status = details['status']
659             if status in ('ACTIVE', ):
660                 return
661
662         r = self.client.start_server(int(server_id))
663         self._optional_output(r)
664
665         if self['wait']:
666             self._wait(server_id, status)
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_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
675     """Shutdown an active 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 = 'STOPPED'
686         if self['wait']:
687             details = self.client.get_server_details(server_id)
688             status = details['status']
689             if status in ('STOPPED', ):
690                 return
691
692         r = self.client.shutdown_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_console(_init_cyclades, _optional_json):
705     """Get a VNC console to access an existing virtual server
706     Console connection information provided (at least):
707     - host: (url or address) a VNC host
708     - port: (int) the gateway to enter virtual server on host
709     - password: for VNC authorization
710     """
711
712     @errors.generic.all
713     @errors.cyclades.connection
714     @errors.cyclades.server_id
715     def _run(self, server_id):
716         self._print(
717             self.client.get_server_console(int(server_id)), self.print_dict)
718
719     def main(self, server_id):
720         super(self.__class__, self)._run()
721         self._run(server_id=server_id)
722
723
724 @command(server_cmds)
725 class server_addr(_init_cyclades, _optional_json):
726     """List the addresses of all network interfaces on a virtual server"""
727
728     arguments = dict(
729         enum=FlagArgument('Enumerate results', '--enumerate')
730     )
731
732     @errors.generic.all
733     @errors.cyclades.connection
734     @errors.cyclades.server_id
735     def _run(self, server_id):
736         reply = self.client.list_server_nics(int(server_id))
737         self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
738
739     def main(self, server_id):
740         super(self.__class__, self)._run()
741         self._run(server_id=server_id)
742
743
744 @command(server_cmds)
745 class server_stats(_init_cyclades, _optional_json):
746     """Get virtual server statistics"""
747
748     @errors.generic.all
749     @errors.cyclades.connection
750     @errors.cyclades.server_id
751     def _run(self, server_id):
752         self._print(
753             self.client.get_server_stats(int(server_id)), self.print_dict)
754
755     def main(self, server_id):
756         super(self.__class__, self)._run()
757         self._run(server_id=server_id)
758
759
760 @command(server_cmds)
761 class server_wait(_init_cyclades, _server_wait):
762     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
763
764     arguments = dict(
765         timeout=IntArgument(
766             'Wait limit in seconds (default: 60)', '--timeout', default=60)
767     )
768
769     @errors.generic.all
770     @errors.cyclades.connection
771     @errors.cyclades.server_id
772     def _run(self, server_id, current_status):
773         r = self.client.get_server_details(server_id)
774         if r['status'].lower() == current_status.lower():
775             self._wait(server_id, current_status, timeout=self['timeout'])
776         else:
777             self.error(
778                 'Server %s: Cannot wait for status %s, '
779                 'status is already %s' % (
780                     server_id, current_status, r['status']))
781
782     def main(self, server_id, current_status='BUILD'):
783         super(self.__class__, self)._run()
784         self._run(server_id=server_id, current_status=current_status)
785
786
787 @command(flavor_cmds)
788 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
789     """List available hardware flavors"""
790
791     PERMANENTS = ('id', 'name')
792
793     arguments = dict(
794         detail=FlagArgument('show detailed output', ('-l', '--details')),
795         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
796         more=FlagArgument(
797             'output results in pages (-n to set items per page, default 10)',
798             '--more'),
799         enum=FlagArgument('Enumerate results', '--enumerate'),
800         ram=ValueArgument('filter by ram', ('--ram')),
801         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
802         disk=ValueArgument('filter by disk size in GB', ('--disk')),
803         disk_template=ValueArgument(
804             'filter by disk_templace', ('--disk-template'))
805     )
806
807     def _apply_common_filters(self, flavors):
808         common_filters = dict()
809         if self['ram']:
810             common_filters['ram'] = self['ram']
811         if self['vcpus']:
812             common_filters['vcpus'] = self['vcpus']
813         if self['disk']:
814             common_filters['disk'] = self['disk']
815         if self['disk_template']:
816             common_filters['SNF:disk_template'] = self['disk_template']
817         return filter_dicts_by_dict(flavors, common_filters)
818
819     @errors.generic.all
820     @errors.cyclades.connection
821     def _run(self):
822         withcommons = self['ram'] or self['vcpus'] or (
823             self['disk'] or self['disk_template'])
824         detail = self['detail'] or withcommons
825         flavors = self.client.list_flavors(detail)
826         flavors = self._filter_by_name(flavors)
827         flavors = self._filter_by_id(flavors)
828         if withcommons:
829             flavors = self._apply_common_filters(flavors)
830         if not (self['detail'] or (
831                 self['json_output'] or self['output_format'])):
832             remove_from_items(flavors, 'links')
833         if detail and not self['detail']:
834             for flv in flavors:
835                 for key in set(flv).difference(self.PERMANENTS):
836                     flv.pop(key)
837         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
838         self._print(
839             flavors,
840             with_redundancy=self['detail'], with_enumeration=self['enum'],
841             **kwargs)
842         if self['more']:
843             pager(kwargs['out'].getvalue())
844
845     def main(self):
846         super(self.__class__, self)._run()
847         self._run()
848
849
850 @command(flavor_cmds)
851 class flavor_info(_init_cyclades, _optional_json):
852     """Detailed information on a hardware flavor
853     To get a list of available flavors and flavor ids, try /flavor list
854     """
855
856     @errors.generic.all
857     @errors.cyclades.connection
858     @errors.cyclades.flavor_id
859     def _run(self, flavor_id):
860         self._print(
861             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
862
863     def main(self, flavor_id):
864         super(self.__class__, self)._run()
865         self._run(flavor_id=flavor_id)
866
867
868 def _add_name(self, net):
869         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
870         if user_id:
871             uuids.append(user_id)
872         if tenant_id:
873             uuids.append(tenant_id)
874         if uuids:
875             usernames = self._uuids2usernames(uuids)
876             if user_id:
877                 net['user_id'] += ' (%s)' % usernames[user_id]
878             if tenant_id:
879                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]