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