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