Fix wrong parameter passing to create_server
[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
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 raiseCLIError, CLISyntaxError, CLIBaseUrlError
43 from kamaki.clients.cyclades import CycladesClient, ClientError
44 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
45 from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
46 from kamaki.cli.commands import _command_init, errors, addLogSettings
47 from kamaki.cli.commands import (
48     _optional_output_cmd, _optional_json, _name_filter, _id_filter)
49
50
51 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
52 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
53 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
54 _commands = [server_cmds, flavor_cmds, network_cmds]
55
56
57 about_authentication = '\nUser Authentication:\
58     \n* to check authentication: /user authenticate\
59     \n* to set authentication token: /config set cloud.<cloud>.token <token>'
60
61 howto_personality = [
62     'Defines a file to be injected to VMs file system.',
63     'syntax:  PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
64     '  PATH: local file to be injected (relative or absolute)',
65     '  SERVER_PATH: destination location inside server Image',
66     '  OWNER: VMs user id of the remote destination file',
67     '  GROUP: VMs group id or name of the destination file',
68     '  MODEL: permition in octal (e.g. 0777 or o+rwx)']
69
70
71 class _service_wait(object):
72
73     wait_arguments = dict(
74         progress_bar=ProgressBarArgument(
75             'do not show progress bar', ('-N', '--no-progress-bar'), False)
76     )
77
78     def _wait(self, service, service_id, status_method, currect_status):
79         (progress_bar, wait_cb) = self._safe_progress_bar(
80             '%s %s still in %s mode' % (service, service_id, currect_status))
81
82         try:
83             new_mode = status_method(
84                 service_id, currect_status, wait_cb=wait_cb)
85         finally:
86             self._safe_progress_bar_finish(progress_bar)
87         if new_mode:
88             self.error('%s %s is now in %s mode' % (
89                 service, service_id, new_mode))
90         else:
91             raiseCLIError(None, 'Time out')
92
93
94 class _server_wait(_service_wait):
95
96     def _wait(self, server_id, currect_status):
97         super(_server_wait, self)._wait(
98             'Server', server_id, self.client.wait_server, currect_status)
99
100
101 class _network_wait(_service_wait):
102
103     def _wait(self, net_id, currect_status):
104         super(_network_wait, self)._wait(
105             'Network', net_id, self.client.wait_network, currect_status)
106
107
108 class _init_cyclades(_command_init):
109     @errors.generic.all
110     @addLogSettings
111     def _run(self, service='compute'):
112         if getattr(self, 'cloud', None):
113             base_url = self._custom_url(service) or self._custom_url(
114                 'cyclades')
115             if base_url:
116                 token = self._custom_token(service) or self._custom_token(
117                     'cyclades') or self.config.get_cloud('token')
118                 self.client = CycladesClient(base_url=base_url, token=token)
119                 return
120         else:
121             self.cloud = 'default'
122         if getattr(self, 'auth_base', False):
123             cyclades_endpoints = self.auth_base.get_service_endpoints(
124                 self._custom_type('cyclades') or 'compute',
125                 self._custom_version('cyclades') or '')
126             base_url = cyclades_endpoints['publicURL']
127             token = self.auth_base.token
128             self.client = CycladesClient(base_url=base_url, token=token)
129         else:
130             raise CLIBaseUrlError(service='cyclades')
131
132     def main(self):
133         self._run()
134
135
136 @command(server_cmds)
137 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
138     """List Virtual Machines accessible by user"""
139
140     PERMANENTS = ('id', 'name')
141
142     __doc__ += about_authentication
143
144     arguments = dict(
145         detail=FlagArgument('show detailed output', ('-l', '--details')),
146         since=DateArgument(
147             'show only items since date (\' d/m/Y H:M:S \')',
148             '--since'),
149         limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
150         more=FlagArgument(
151             'output results in pages (-n to set items per page, default 10)',
152             '--more'),
153         enum=FlagArgument('Enumerate results', '--enumerate'),
154         flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
155         image_id=ValueArgument('filter by image id', ('--image-id')),
156         user_id=ValueArgument('filter by user id', ('--user-id')),
157         user_name=ValueArgument('filter by user name', ('--user-name')),
158         status=ValueArgument(
159             'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
160             ('--status')),
161         meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
162         meta_like=KeyValueArgument(
163             'print only if in key=value, the value is part of actual value',
164             ('--metadata-like')),
165     )
166
167     def _add_user_name(self, servers):
168         uuids = self._uuids2usernames(list(set(
169                 [srv['user_id'] for srv in servers] +
170                 [srv['tenant_id'] for srv in servers])))
171         for srv in servers:
172             srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
173             srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
174         return servers
175
176     def _apply_common_filters(self, servers):
177         common_filters = dict()
178         if self['status']:
179             common_filters['status'] = self['status']
180         if self['user_id'] or self['user_name']:
181             uuid = self['user_id'] or self._username2uuid(self['user_name'])
182             common_filters['user_id'] = uuid
183         return filter_dicts_by_dict(servers, common_filters)
184
185     def _filter_by_image(self, servers):
186         iid = self['image_id']
187         return [srv for srv in servers if srv['image']['id'] == iid]
188
189     def _filter_by_flavor(self, servers):
190         fid = self['flavor_id']
191         return [srv for srv in servers if (
192             '%s' % srv['image']['id'] == '%s' % fid)]
193
194     def _filter_by_metadata(self, servers):
195         new_servers = []
196         for srv in servers:
197             if not 'metadata' in srv:
198                 continue
199             meta = [dict(srv['metadata'])]
200             if self['meta']:
201                 meta = filter_dicts_by_dict(meta, self['meta'])
202             if meta and self['meta_like']:
203                 meta = filter_dicts_by_dict(
204                     meta, self['meta_like'], exact_match=False)
205             if meta:
206                 new_servers.append(srv)
207         return new_servers
208
209     @errors.generic.all
210     @errors.cyclades.connection
211     @errors.cyclades.date
212     def _run(self):
213         withimage = bool(self['image_id'])
214         withflavor = bool(self['flavor_id'])
215         withmeta = bool(self['meta'] or self['meta_like'])
216         withcommons = bool(
217             self['status'] or self['user_id'] or self['user_name'])
218         detail = self['detail'] or (
219             withimage or withflavor or withmeta or withcommons)
220         servers = self.client.list_servers(detail, self['since'])
221
222         servers = self._filter_by_name(servers)
223         servers = self._filter_by_id(servers)
224         servers = self._apply_common_filters(servers)
225         if withimage:
226             servers = self._filter_by_image(servers)
227         if withflavor:
228             servers = self._filter_by_flavor(servers)
229         if withmeta:
230             servers = self._filter_by_metadata(servers)
231
232         if self['detail'] and not self['json_output']:
233             servers = self._add_user_name(servers)
234         elif not (self['detail'] or self['json_output']):
235             remove_from_items(servers, 'links')
236         if detail and not self['detail']:
237             for srv in servers:
238                 for key in set(srv).difference(self.PERMANENTS):
239                     srv.pop(key)
240         kwargs = dict(with_enumeration=self['enum'])
241         if self['more']:
242             kwargs['out'] = StringIO()
243             kwargs['title'] = ()
244         if self['limit']:
245             servers = servers[:self['limit']]
246         self._print(servers, **kwargs)
247         if self['more']:
248             pager(kwargs['out'].getvalue())
249
250     def main(self):
251         super(self.__class__, self)._run()
252         self._run()
253
254
255 @command(server_cmds)
256 class server_info(_init_cyclades, _optional_json):
257     """Detailed information on a Virtual Machine
258     Contains:
259     - name, id, status, create/update dates
260     - network interfaces
261     - metadata (e.g. os, superuser) and diagnostics
262     - hardware flavor and os image ids
263     """
264
265     @errors.generic.all
266     @errors.cyclades.connection
267     @errors.cyclades.server_id
268     def _run(self, server_id):
269         vm = self.client.get_server_details(server_id)
270         uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
271         vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
272         vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
273         self._print(vm, self.print_dict)
274
275     def main(self, server_id):
276         super(self.__class__, self)._run()
277         self._run(server_id=server_id)
278
279
280 class PersonalityArgument(KeyValueArgument):
281     @property
282     def value(self):
283         return self._value if hasattr(self, '_value') else []
284
285     @value.setter
286     def value(self, newvalue):
287         if newvalue == self.default:
288             return self.value
289         self._value = []
290         for i, terms in enumerate(newvalue):
291             termlist = terms.split(',')
292             if len(termlist) > 5:
293                 msg = 'Wrong number of terms (should be 1 to 5)'
294                 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
295             path = termlist[0]
296             if not exists(path):
297                 raiseCLIError(
298                     None,
299                     '--personality: File %s does not exist' % path,
300                     importance=1, details=howto_personality)
301             self._value.append(dict(path=path))
302             with open(path) as f:
303                 self._value[i]['contents'] = b64encode(f.read())
304             try:
305                 self._value[i]['path'] = termlist[1]
306                 self._value[i]['owner'] = termlist[2]
307                 self._value[i]['group'] = termlist[3]
308                 self._value[i]['mode'] = termlist[4]
309             except IndexError:
310                 pass
311
312
313 @command(server_cmds)
314 class server_create(_init_cyclades, _optional_json, _server_wait):
315     """Create a server (aka Virtual Machine)
316     Parameters:
317     - name: (single quoted text)
318     - flavor id: Hardware flavor. Pick one from: /flavor list
319     - image id: OS images. Pick one from: /image list
320     """
321
322     arguments = dict(
323         personality=PersonalityArgument(
324             (80 * ' ').join(howto_personality), ('-p', '--personality')),
325         wait=FlagArgument('Wait server to build', ('-w', '--wait'))
326     )
327
328     @errors.generic.all
329     @errors.cyclades.connection
330     @errors.plankton.id
331     @errors.cyclades.flavor_id
332     def _run(self, name, flavor_id, image_id):
333         r = self.client.create_server(
334             name, int(flavor_id), image_id, personality=self['personality'])
335         usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
336         r['user_id'] += ' (%s)' % usernames[r['user_id']]
337         r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
338         self._print(r, self.print_dict)
339         if self['wait']:
340             self._wait(r['id'], r['status'])
341
342     def main(self, name, flavor_id, image_id):
343         super(self.__class__, self)._run()
344         self._run(name=name, flavor_id=flavor_id, image_id=image_id)
345
346
347 @command(server_cmds)
348 class server_rename(_init_cyclades, _optional_output_cmd):
349     """Set/update a server (VM) name
350     VM names are not unique, therefore multiple servers may share the same name
351     """
352
353     @errors.generic.all
354     @errors.cyclades.connection
355     @errors.cyclades.server_id
356     def _run(self, server_id, new_name):
357         self._optional_output(
358             self.client.update_server_name(int(server_id), new_name))
359
360     def main(self, server_id, new_name):
361         super(self.__class__, self)._run()
362         self._run(server_id=server_id, new_name=new_name)
363
364
365 @command(server_cmds)
366 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
367     """Delete a server (VM)"""
368
369     arguments = dict(
370         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
371     )
372
373     @errors.generic.all
374     @errors.cyclades.connection
375     @errors.cyclades.server_id
376     def _run(self, server_id):
377             status = 'DELETED'
378             if self['wait']:
379                 details = self.client.get_server_details(server_id)
380                 status = details['status']
381
382             r = self.client.delete_server(int(server_id))
383             self._optional_output(r)
384
385             if self['wait']:
386                 self._wait(server_id, status)
387
388     def main(self, server_id):
389         super(self.__class__, self)._run()
390         self._run(server_id=server_id)
391
392
393 @command(server_cmds)
394 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
395     """Reboot a server (VM)"""
396
397     arguments = dict(
398         hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
399         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
400     )
401
402     @errors.generic.all
403     @errors.cyclades.connection
404     @errors.cyclades.server_id
405     def _run(self, server_id):
406         r = self.client.reboot_server(int(server_id), self['hard'])
407         self._optional_output(r)
408
409         if self['wait']:
410             self._wait(server_id, 'REBOOT')
411
412     def main(self, server_id):
413         super(self.__class__, self)._run()
414         self._run(server_id=server_id)
415
416
417 @command(server_cmds)
418 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
419     """Start an existing server (VM)"""
420
421     arguments = dict(
422         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
423     )
424
425     @errors.generic.all
426     @errors.cyclades.connection
427     @errors.cyclades.server_id
428     def _run(self, server_id):
429         status = 'ACTIVE'
430         if self['wait']:
431             details = self.client.get_server_details(server_id)
432             status = details['status']
433             if status in ('ACTIVE', ):
434                 return
435
436         r = self.client.start_server(int(server_id))
437         self._optional_output(r)
438
439         if self['wait']:
440             self._wait(server_id, status)
441
442     def main(self, server_id):
443         super(self.__class__, self)._run()
444         self._run(server_id=server_id)
445
446
447 @command(server_cmds)
448 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
449     """Shutdown an active server (VM)"""
450
451     arguments = dict(
452         wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
453     )
454
455     @errors.generic.all
456     @errors.cyclades.connection
457     @errors.cyclades.server_id
458     def _run(self, server_id):
459         status = 'STOPPED'
460         if self['wait']:
461             details = self.client.get_server_details(server_id)
462             status = details['status']
463             if status in ('STOPPED', ):
464                 return
465
466         r = self.client.shutdown_server(int(server_id))
467         self._optional_output(r)
468
469         if self['wait']:
470             self._wait(server_id, status)
471
472     def main(self, server_id):
473         super(self.__class__, self)._run()
474         self._run(server_id=server_id)
475
476
477 @command(server_cmds)
478 class server_console(_init_cyclades, _optional_json):
479     """Get a VNC console to access an existing server (VM)
480     Console connection information provided (at least):
481     - host: (url or address) a VNC host
482     - port: (int) the gateway to enter VM on host
483     - password: for VNC authorization
484     """
485
486     @errors.generic.all
487     @errors.cyclades.connection
488     @errors.cyclades.server_id
489     def _run(self, server_id):
490         self._print(
491             self.client.get_server_console(int(server_id)), self.print_dict)
492
493     def main(self, server_id):
494         super(self.__class__, self)._run()
495         self._run(server_id=server_id)
496
497
498 @command(server_cmds)
499 class server_resize(_init_cyclades, _optional_output_cmd):
500     """Set a different flavor for an existing server
501     To get server ids and flavor ids:
502     /server list
503     /flavor list
504     """
505
506     @errors.generic.all
507     @errors.cyclades.connection
508     @errors.cyclades.server_id
509     @errors.cyclades.flavor_id
510     def _run(self, server_id, flavor_id):
511         self._optional_output(self.client.resize_server(server_id, flavor_id))
512
513     def main(self, server_id, flavor_id):
514         super(self.__class__, self)._run()
515         self._run(server_id=server_id, flavor_id=flavor_id)
516
517
518 @command(server_cmds)
519 class server_firewall(_init_cyclades):
520     """Manage server (VM) firewall profiles for public networks"""
521
522
523 @command(server_cmds)
524 class server_firewall_set(_init_cyclades, _optional_output_cmd):
525     """Set the server (VM) firewall profile on VMs public network
526     Values for profile:
527     - DISABLED: Shutdown firewall
528     - ENABLED: Firewall in normal mode
529     - PROTECTED: Firewall in secure mode
530     """
531
532     @errors.generic.all
533     @errors.cyclades.connection
534     @errors.cyclades.server_id
535     @errors.cyclades.firewall
536     def _run(self, server_id, profile):
537         self._optional_output(self.client.set_firewall_profile(
538             server_id=int(server_id), profile=('%s' % profile).upper()))
539
540     def main(self, server_id, profile):
541         super(self.__class__, self)._run()
542         self._run(server_id=server_id, profile=profile)
543
544
545 @command(server_cmds)
546 class server_firewall_get(_init_cyclades):
547     """Get the server (VM) firewall profile for its public network"""
548
549     @errors.generic.all
550     @errors.cyclades.connection
551     @errors.cyclades.server_id
552     def _run(self, server_id):
553         self.writeln(self.client.get_firewall_profile(server_id))
554
555     def main(self, server_id):
556         super(self.__class__, self)._run()
557         self._run(server_id=server_id)
558
559
560 @command(server_cmds)
561 class server_addr(_init_cyclades, _optional_json):
562     """List the addresses of all network interfaces on a server (VM)"""
563
564     arguments = dict(
565         enum=FlagArgument('Enumerate results', '--enumerate')
566     )
567
568     @errors.generic.all
569     @errors.cyclades.connection
570     @errors.cyclades.server_id
571     def _run(self, server_id):
572         reply = self.client.list_server_nics(int(server_id))
573         self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
574
575     def main(self, server_id):
576         super(self.__class__, self)._run()
577         self._run(server_id=server_id)
578
579
580 @command(server_cmds)
581 class server_metadata(_init_cyclades):
582     """Manage Server metadata (key:value pairs of server attributes)"""
583
584
585 @command(server_cmds)
586 class server_metadata_list(_init_cyclades, _optional_json):
587     """Get server metadata"""
588
589     @errors.generic.all
590     @errors.cyclades.connection
591     @errors.cyclades.server_id
592     @errors.cyclades.metadata
593     def _run(self, server_id, key=''):
594         self._print(
595             self.client.get_server_metadata(int(server_id), key),
596             self.print_dict)
597
598     def main(self, server_id, key=''):
599         super(self.__class__, self)._run()
600         self._run(server_id=server_id, key=key)
601
602
603 @command(server_cmds)
604 class server_metadata_set(_init_cyclades, _optional_json):
605     """Set / update server(VM) metadata
606     Metadata should be given in key/value pairs in key=value format
607     For example: /server metadata set <server id> key1=value1 key2=value2
608     Old, unreferenced metadata will remain intact
609     """
610
611     @errors.generic.all
612     @errors.cyclades.connection
613     @errors.cyclades.server_id
614     def _run(self, server_id, keyvals):
615         assert keyvals, 'Please, add some metadata ( key=value)'
616         metadata = dict()
617         for keyval in keyvals:
618             k, sep, v = keyval.partition('=')
619             if sep and k:
620                 metadata[k] = v
621             else:
622                 raiseCLIError(
623                     'Invalid piece of metadata %s' % keyval,
624                     importance=2, details=[
625                         'Correct metadata format: key=val',
626                         'For example:',
627                         '/server metadata set <server id>'
628                         'key1=value1 key2=value2'])
629         self._print(
630             self.client.update_server_metadata(int(server_id), **metadata),
631             self.print_dict)
632
633     def main(self, server_id, *key_equals_val):
634         super(self.__class__, self)._run()
635         self._run(server_id=server_id, keyvals=key_equals_val)
636
637
638 @command(server_cmds)
639 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
640     """Delete server (VM) metadata"""
641
642     @errors.generic.all
643     @errors.cyclades.connection
644     @errors.cyclades.server_id
645     @errors.cyclades.metadata
646     def _run(self, server_id, key):
647         self._optional_output(
648             self.client.delete_server_metadata(int(server_id), key))
649
650     def main(self, server_id, key):
651         super(self.__class__, self)._run()
652         self._run(server_id=server_id, key=key)
653
654
655 @command(server_cmds)
656 class server_stats(_init_cyclades, _optional_json):
657     """Get server (VM) statistics"""
658
659     @errors.generic.all
660     @errors.cyclades.connection
661     @errors.cyclades.server_id
662     def _run(self, server_id):
663         self._print(
664             self.client.get_server_stats(int(server_id)), self.print_dict)
665
666     def main(self, server_id):
667         super(self.__class__, self)._run()
668         self._run(server_id=server_id)
669
670
671 @command(server_cmds)
672 class server_wait(_init_cyclades, _server_wait):
673     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
674
675     @errors.generic.all
676     @errors.cyclades.connection
677     @errors.cyclades.server_id
678     def _run(self, server_id, currect_status):
679         self._wait(server_id, currect_status)
680
681     def main(self, server_id, currect_status='BUILD'):
682         super(self.__class__, self)._run()
683         self._run(server_id=server_id, currect_status=currect_status)
684
685
686 @command(flavor_cmds)
687 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
688     """List available hardware flavors"""
689
690     PERMANENTS = ('id', 'name')
691
692     arguments = dict(
693         detail=FlagArgument('show detailed output', ('-l', '--details')),
694         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
695         more=FlagArgument(
696             'output results in pages (-n to set items per page, default 10)',
697             '--more'),
698         enum=FlagArgument('Enumerate results', '--enumerate'),
699         ram=ValueArgument('filter by ram', ('--ram')),
700         vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
701         disk=ValueArgument('filter by disk size in GB', ('--disk')),
702         disk_template=ValueArgument(
703             'filter by disk_templace', ('--disk-template'))
704     )
705
706     def _apply_common_filters(self, flavors):
707         common_filters = dict()
708         if self['ram']:
709             common_filters['ram'] = self['ram']
710         if self['vcpus']:
711             common_filters['vcpus'] = self['vcpus']
712         if self['disk']:
713             common_filters['disk'] = self['disk']
714         if self['disk_template']:
715             common_filters['SNF:disk_template'] = self['disk_template']
716         return filter_dicts_by_dict(flavors, common_filters)
717
718     @errors.generic.all
719     @errors.cyclades.connection
720     def _run(self):
721         withcommons = self['ram'] or self['vcpus'] or (
722             self['disk'] or self['disk_template'])
723         detail = self['detail'] or withcommons
724         flavors = self.client.list_flavors(detail)
725         flavors = self._filter_by_name(flavors)
726         flavors = self._filter_by_id(flavors)
727         if withcommons:
728             flavors = self._apply_common_filters(flavors)
729         if not (self['detail'] or self['json_output']):
730             remove_from_items(flavors, 'links')
731         if detail and not self['detail']:
732             for flv in flavors:
733                 for key in set(flv).difference(self.PERMANENTS):
734                     flv.pop(key)
735         kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
736         self._print(
737             flavors,
738             with_redundancy=self['detail'], with_enumeration=self['enum'],
739             **kwargs)
740         if self['more']:
741             pager(kwargs['out'].getvalue())
742
743     def main(self):
744         super(self.__class__, self)._run()
745         self._run()
746
747
748 @command(flavor_cmds)
749 class flavor_info(_init_cyclades, _optional_json):
750     """Detailed information on a hardware flavor
751     To get a list of available flavors and flavor ids, try /flavor list
752     """
753
754     @errors.generic.all
755     @errors.cyclades.connection
756     @errors.cyclades.flavor_id
757     def _run(self, flavor_id):
758         self._print(
759             self.client.get_flavor_details(int(flavor_id)), self.print_dict)
760
761     def main(self, flavor_id):
762         super(self.__class__, self)._run()
763         self._run(flavor_id=flavor_id)
764
765
766 def _add_name(self, net):
767         user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
768         if user_id:
769             uuids.append(user_id)
770         if tenant_id:
771             uuids.append(tenant_id)
772         if uuids:
773             usernames = self._uuids2usernames(uuids)
774             if user_id:
775                 net['user_id'] += ' (%s)' % usernames[user_id]
776             if tenant_id:
777                 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
778
779
780 @command(network_cmds)
781 class network_info(_init_cyclades, _optional_json):
782     """Detailed information on a network
783     To get a list of available networks and network ids, try /network list
784     """
785
786     @errors.generic.all
787     @errors.cyclades.connection
788     @errors.cyclades.network_id
789     def _run(self, network_id):
790         network = self.client.get_network_details(int(network_id))
791         _add_name(self, network)
792         self._print(network, self.print_dict, exclude=('id'))
793
794     def main(self, network_id):
795         super(self.__class__, self)._run()
796         self._run(network_id=network_id)
797
798
799 @command(network_cmds)
800 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
801     """List networks"""
802
803     PERMANENTS = ('id', 'name')
804
805     arguments = dict(
806         detail=FlagArgument('show detailed output', ('-l', '--details')),
807         limit=IntArgument('limit # of listed networks', ('-n', '--number')),
808         more=FlagArgument(
809             'output results in pages (-n to set items per page, default 10)',
810             '--more'),
811         enum=FlagArgument('Enumerate results', '--enumerate'),
812         status=ValueArgument('filter by status', ('--status')),
813         public=FlagArgument('only public networks', ('--public')),
814         private=FlagArgument('only private networks', ('--private')),
815         dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
816         no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
817         user_id=ValueArgument('filter by user id', ('--user-id')),
818         user_name=ValueArgument('filter by user name', ('--user-name')),
819         gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
820         gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
821         cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
822         cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
823         type=ValueArgument('filter by type', ('--type')),
824     )
825
826     def _apply_common_filters(self, networks):
827         common_filter = dict()
828         if self['public']:
829             if self['private']:
830                 return []
831             common_filter['public'] = self['public']
832         elif self['private']:
833             common_filter['public'] = False
834         if self['dhcp']:
835             if self['no_dhcp']:
836                 return []
837             common_filter['dhcp'] = True
838         elif self['no_dhcp']:
839             common_filter['dhcp'] = False
840         if self['user_id'] or self['user_name']:
841             uuid = self['user_id'] or self._username2uuid(self['user_name'])
842             common_filter['user_id'] = uuid
843         for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
844             if self[term]:
845                 common_filter[term] = self[term]
846         return filter_dicts_by_dict(networks, common_filter)
847
848     def _add_name(self, networks, key='user_id'):
849         uuids = self._uuids2usernames(
850             list(set([net[key] for net in networks])))
851         for net in networks:
852             v = net.get(key, None)
853             if v:
854                 net[key] += ' (%s)' % uuids[v]
855         return networks
856
857     @errors.generic.all
858     @errors.cyclades.connection
859     def _run(self):
860         withcommons = False
861         for term in (
862                 'status', 'public', 'private', 'user_id', 'user_name', 'type',
863                 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
864             if self[term]:
865                 withcommons = True
866                 break
867         detail = self['detail'] or withcommons
868         networks = self.client.list_networks(detail)
869         networks = self._filter_by_name(networks)
870         networks = self._filter_by_id(networks)
871         if withcommons:
872             networks = self._apply_common_filters(networks)
873         if not (self['detail'] or self['json_output']):
874             remove_from_items(networks, 'links')
875         if detail and not self['detail']:
876             for net in networks:
877                 for key in set(net).difference(self.PERMANENTS):
878                     net.pop(key)
879         if self['detail'] and not self['json_output']:
880             self._add_name(networks)
881             self._add_name(networks, 'tenant_id')
882         kwargs = dict(with_enumeration=self['enum'])
883         if self['more']:
884             kwargs['out'] = StringIO()
885             kwargs['title'] = ()
886         if self['limit']:
887             networks = networks[:self['limit']]
888         self._print(networks, **kwargs)
889         if self['more']:
890             pager(kwargs['out'].getvalue())
891
892     def main(self):
893         super(self.__class__, self)._run()
894         self._run()
895
896
897 @command(network_cmds)
898 class network_create(_init_cyclades, _optional_json, _network_wait):
899     """Create an (unconnected) network"""
900
901     arguments = dict(
902         cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
903         gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
904         dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
905         type=ValueArgument(
906             'Valid network types are '
907             'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
908             '--with-type',
909             default='MAC_FILTERED'),
910         wait=FlagArgument('Wait network to build', ('-w', '--wait'))
911     )
912
913     @errors.generic.all
914     @errors.cyclades.connection
915     @errors.cyclades.network_max
916     def _run(self, name):
917         r = self.client.create_network(
918             name,
919             cidr=self['cidr'],
920             gateway=self['gateway'],
921             dhcp=self['dhcp'],
922             type=self['type'])
923         _add_name(self, r)
924         self._print(r, self.print_dict)
925         if self['wait']:
926             self._wait(r['id'], 'PENDING')
927
928     def main(self, name):
929         super(self.__class__, self)._run()
930         self._run(name)
931
932
933 @command(network_cmds)
934 class network_rename(_init_cyclades, _optional_output_cmd):
935     """Set the name of a network"""
936
937     @errors.generic.all
938     @errors.cyclades.connection
939     @errors.cyclades.network_id
940     def _run(self, network_id, new_name):
941         self._optional_output(
942                 self.client.update_network_name(int(network_id), new_name))
943
944     def main(self, network_id, new_name):
945         super(self.__class__, self)._run()
946         self._run(network_id=network_id, new_name=new_name)
947
948
949 @command(network_cmds)
950 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
951     """Delete a network"""
952
953     arguments = dict(
954         wait=FlagArgument('Wait network to build', ('-w', '--wait'))
955     )
956
957     @errors.generic.all
958     @errors.cyclades.connection
959     @errors.cyclades.network_id
960     @errors.cyclades.network_in_use
961     def _run(self, network_id):
962         status = 'DELETED'
963         if self['wait']:
964             r = self.client.get_network_details(network_id)
965             status = r['status']
966             if status in ('DELETED', ):
967                 return
968
969         r = self.client.delete_network(int(network_id))
970         self._optional_output(r)
971
972         if self['wait']:
973             self._wait(network_id, status)
974
975     def main(self, network_id):
976         super(self.__class__, self)._run()
977         self._run(network_id=network_id)
978
979
980 @command(network_cmds)
981 class network_connect(_init_cyclades, _optional_output_cmd):
982     """Connect a server to a network"""
983
984     @errors.generic.all
985     @errors.cyclades.connection
986     @errors.cyclades.server_id
987     @errors.cyclades.network_id
988     def _run(self, server_id, network_id):
989         self._optional_output(
990                 self.client.connect_server(int(server_id), int(network_id)))
991
992     def main(self, server_id, network_id):
993         super(self.__class__, self)._run()
994         self._run(server_id=server_id, network_id=network_id)
995
996
997 @command(network_cmds)
998 class network_disconnect(_init_cyclades):
999     """Disconnect a nic that connects a server to a network
1000     Nic ids are listed as "attachments" in detailed network information
1001     To get detailed network information: /network info <network id>
1002     """
1003
1004     @errors.cyclades.nic_format
1005     def _server_id_from_nic(self, nic_id):
1006         return nic_id.split('-')[1]
1007
1008     @errors.generic.all
1009     @errors.cyclades.connection
1010     @errors.cyclades.server_id
1011     @errors.cyclades.nic_id
1012     def _run(self, nic_id, server_id):
1013         num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1014         if not num_of_disconnected:
1015             raise ClientError(
1016                 'Network Interface %s not found on server %s' % (
1017                     nic_id, server_id),
1018                 status=404)
1019         print('Disconnected %s connections' % num_of_disconnected)
1020
1021     def main(self, nic_id):
1022         super(self.__class__, self)._run()
1023         server_id = self._server_id_from_nic(nic_id=nic_id)
1024         self._run(nic_id=nic_id, server_id=server_id)
1025
1026
1027 @command(network_cmds)
1028 class network_wait(_init_cyclades, _network_wait):
1029     """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1030
1031     @errors.generic.all
1032     @errors.cyclades.connection
1033     @errors.cyclades.network_id
1034     def _run(self, network_id, currect_status):
1035         self._wait(network_id, currect_status)
1036
1037     def main(self, network_id, currect_status='PENDING'):
1038         super(self.__class__, self)._run()
1039         self._run(network_id=network_id, currect_status=currect_status)
1040
1041
1042 @command(server_cmds)
1043 class server_ip(_init_cyclades):
1044     """Manage floating IPs for the servers"""
1045
1046
1047 @command(server_cmds)
1048 class server_ip_pools(_init_cyclades, _optional_json):
1049     """List all floating pools of floating ips"""
1050
1051     @errors.generic.all
1052     @errors.cyclades.connection
1053     def _run(self):
1054         r = self.client.get_floating_ip_pools()
1055         self._print(r if self['json_output'] else r['floating_ip_pools'])
1056
1057     def main(self):
1058         super(self.__class__, self)._run()
1059         self._run()
1060
1061
1062 @command(server_cmds)
1063 class server_ip_list(_init_cyclades, _optional_json):
1064     """List all floating ips"""
1065
1066     @errors.generic.all
1067     @errors.cyclades.connection
1068     def _run(self):
1069         r = self.client.get_floating_ips()
1070         self._print(r if self['json_output'] else r['floating_ips'])
1071
1072     def main(self):
1073         super(self.__class__, self)._run()
1074         self._run()
1075
1076
1077 @command(server_cmds)
1078 class server_ip_info(_init_cyclades, _optional_json):
1079     """A floating IPs' details"""
1080
1081     @errors.generic.all
1082     @errors.cyclades.connection
1083     def _run(self, ip):
1084         self._print(self.client.get_floating_ip(ip), self.print_dict)
1085
1086     def main(self, ip):
1087         super(self.__class__, self)._run()
1088         self._run(ip=ip)
1089
1090
1091 @command(server_cmds)
1092 class server_ip_create(_init_cyclades, _optional_json):
1093     """Create a new floating IP"""
1094
1095     arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1096
1097     @errors.generic.all
1098     @errors.cyclades.connection
1099     def _run(self, ip=None):
1100         self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1101
1102     def main(self, requested_address=None):
1103         super(self.__class__, self)._run()
1104         self._run(ip=requested_address)
1105
1106
1107 @command(server_cmds)
1108 class server_ip_delete(_init_cyclades, _optional_output_cmd):
1109     """Delete a floating ip"""
1110
1111     @errors.generic.all
1112     @errors.cyclades.connection
1113     def _run(self, ip):
1114         self._optional_output(self.client.delete_floating_ip(ip))
1115
1116     def main(self, ip):
1117         super(self.__class__, self)._run()
1118         self._run(ip=ip)
1119
1120
1121 @command(server_cmds)
1122 class server_ip_attach(_init_cyclades, _optional_output_cmd):
1123     """Attach a floating ip to a server with server_id
1124     """
1125
1126     @errors.generic.all
1127     @errors.cyclades.connection
1128     @errors.cyclades.server_id
1129     def _run(self, server_id, ip):
1130         self._optional_output(self.client.attach_floating_ip(server_id, ip))
1131
1132     def main(self, server_id, ip):
1133         super(self.__class__, self)._run()
1134         self._run(server_id=server_id, ip=ip)
1135
1136
1137 @command(server_cmds)
1138 class server_ip_detach(_init_cyclades, _optional_output_cmd):
1139     """Detach floating IP from server
1140     """
1141
1142     @errors.generic.all
1143     @errors.cyclades.connection
1144     @errors.cyclades.server_id
1145     def _run(self, server_id, ip):
1146         self._optional_output(self.client.detach_floating_ip(server_id, ip))
1147
1148     def main(self, server_id, ip):
1149         super(self.__class__, self)._run()
1150         self._run(server_id=server_id, ip=ip)