1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
7 # 1. Redistributions of source code must retain the above
8 # copyright notice, this list of conditions and the following
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.
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.
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.
34 from base64 import b64encode
35 from os.path import exists
36 from io import StringIO
37 from pydoc import pager
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)
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]
57 about_authentication = '\nUser Authentication:\
58 \n* to check authentication: /user authenticate\
59 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
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)']
71 class _service_wait(object):
73 wait_arguments = dict(
74 progress_bar=ProgressBarArgument(
75 'do not show progress bar', ('-N', '--no-progress-bar'), False)
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))
83 new_mode = status_method(
84 service_id, currect_status, wait_cb=wait_cb)
86 self._safe_progress_bar_finish(progress_bar)
88 self.error('%s %s is now in %s mode' % (
89 service, service_id, new_mode))
91 raiseCLIError(None, 'Time out')
94 class _server_wait(_service_wait):
96 def _wait(self, server_id, currect_status):
97 super(_server_wait, self)._wait(
98 'Server', server_id, self.client.wait_server, currect_status)
101 class _network_wait(_service_wait):
103 def _wait(self, net_id, currect_status):
104 super(_network_wait, self)._wait(
105 'Network', net_id, self.client.wait_network, currect_status)
108 class _init_cyclades(_command_init):
111 def _run(self, service='compute'):
112 if getattr(self, 'cloud', None):
113 base_url = self._custom_url(service) or self._custom_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)
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)
130 raise CLIBaseUrlError(service='cyclades')
136 @command(server_cmds)
137 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
138 """List Virtual Machines accessible by user"""
140 PERMANENTS = ('id', 'name')
142 __doc__ += about_authentication
145 detail=FlagArgument('show detailed output', ('-l', '--details')),
147 'show only items since date (\' d/m/Y H:M:S \')',
149 limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
151 'output results in pages (-n to set items per page, default 10)',
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.)',
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')),
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])))
172 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
173 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
176 def _apply_common_filters(self, servers):
177 common_filters = dict()
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)
185 def _filter_by_image(self, servers):
186 iid = self['image_id']
187 return [srv for srv in servers if srv['image']['id'] == iid]
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)]
194 def _filter_by_metadata(self, servers):
197 if not 'metadata' in srv:
199 meta = [dict(srv['metadata'])]
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)
206 new_servers.append(srv)
210 @errors.cyclades.connection
211 @errors.cyclades.date
213 withimage = bool(self['image_id'])
214 withflavor = bool(self['flavor_id'])
215 withmeta = bool(self['meta'] or self['meta_like'])
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'])
222 servers = self._filter_by_name(servers)
223 servers = self._filter_by_id(servers)
224 servers = self._apply_common_filters(servers)
226 servers = self._filter_by_image(servers)
228 servers = self._filter_by_flavor(servers)
230 servers = self._filter_by_metadata(servers)
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']:
238 for key in set(srv).difference(self.PERMANENTS):
240 kwargs = dict(with_enumeration=self['enum'])
242 kwargs['out'] = StringIO()
245 servers = servers[:self['limit']]
246 self._print(servers, **kwargs)
248 pager(kwargs['out'].getvalue())
251 super(self.__class__, self)._run()
255 @command(server_cmds)
256 class server_info(_init_cyclades, _optional_json):
257 """Detailed information on a Virtual Machine
259 - name, id, status, create/update dates
261 - metadata (e.g. os, superuser) and diagnostics
262 - hardware flavor and os image ids
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)
275 def main(self, server_id):
276 super(self.__class__, self)._run()
277 self._run(server_id=server_id)
280 class PersonalityArgument(KeyValueArgument):
283 return self._value if hasattr(self, '_value') else []
286 def value(self, newvalue):
287 if newvalue == self.default:
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)
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())
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]
313 @command(server_cmds)
314 class server_create(_init_cyclades, _optional_json, _server_wait):
315 """Create a server (aka Virtual Machine)
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
323 personality=PersonalityArgument(
324 (80 * ' ').join(howto_personality), ('-p', '--personality')),
325 wait=FlagArgument('Wait server to build', ('-w', '--wait'))
329 @errors.cyclades.connection
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)
340 self._wait(r['id'], r['status'])
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)
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
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))
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)
365 @command(server_cmds)
366 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
367 """Delete a server (VM)"""
370 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
374 @errors.cyclades.connection
375 @errors.cyclades.server_id
376 def _run(self, server_id):
379 details = self.client.get_server_details(server_id)
380 status = details['status']
382 r = self.client.delete_server(int(server_id))
383 self._optional_output(r)
386 self._wait(server_id, status)
388 def main(self, server_id):
389 super(self.__class__, self)._run()
390 self._run(server_id=server_id)
393 @command(server_cmds)
394 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
395 """Reboot a server (VM)"""
398 hard=FlagArgument('perform a hard reboot', ('-f', '--force')),
399 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
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)
410 self._wait(server_id, 'REBOOT')
412 def main(self, server_id):
413 super(self.__class__, self)._run()
414 self._run(server_id=server_id)
417 @command(server_cmds)
418 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
419 """Start an existing server (VM)"""
422 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
426 @errors.cyclades.connection
427 @errors.cyclades.server_id
428 def _run(self, server_id):
431 details = self.client.get_server_details(server_id)
432 status = details['status']
433 if status in ('ACTIVE', ):
436 r = self.client.start_server(int(server_id))
437 self._optional_output(r)
440 self._wait(server_id, status)
442 def main(self, server_id):
443 super(self.__class__, self)._run()
444 self._run(server_id=server_id)
447 @command(server_cmds)
448 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
449 """Shutdown an active server (VM)"""
452 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
456 @errors.cyclades.connection
457 @errors.cyclades.server_id
458 def _run(self, server_id):
461 details = self.client.get_server_details(server_id)
462 status = details['status']
463 if status in ('STOPPED', ):
466 r = self.client.shutdown_server(int(server_id))
467 self._optional_output(r)
470 self._wait(server_id, status)
472 def main(self, server_id):
473 super(self.__class__, self)._run()
474 self._run(server_id=server_id)
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
487 @errors.cyclades.connection
488 @errors.cyclades.server_id
489 def _run(self, server_id):
491 self.client.get_server_console(int(server_id)), self.print_dict)
493 def main(self, server_id):
494 super(self.__class__, self)._run()
495 self._run(server_id=server_id)
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:
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))
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)
518 @command(server_cmds)
519 class server_firewall(_init_cyclades):
520 """Manage server (VM) firewall profiles for public networks"""
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
527 - DISABLED: Shutdown firewall
528 - ENABLED: Firewall in normal mode
529 - PROTECTED: Firewall in secure mode
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()))
540 def main(self, server_id, profile):
541 super(self.__class__, self)._run()
542 self._run(server_id=server_id, profile=profile)
545 @command(server_cmds)
546 class server_firewall_get(_init_cyclades):
547 """Get the server (VM) firewall profile for its public network"""
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))
555 def main(self, server_id):
556 super(self.__class__, self)._run()
557 self._run(server_id=server_id)
560 @command(server_cmds)
561 class server_addr(_init_cyclades, _optional_json):
562 """List the addresses of all network interfaces on a server (VM)"""
565 enum=FlagArgument('Enumerate results', '--enumerate')
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)
575 def main(self, server_id):
576 super(self.__class__, self)._run()
577 self._run(server_id=server_id)
580 @command(server_cmds)
581 class server_metadata(_init_cyclades):
582 """Manage Server metadata (key:value pairs of server attributes)"""
585 @command(server_cmds)
586 class server_metadata_list(_init_cyclades, _optional_json):
587 """Get server metadata"""
590 @errors.cyclades.connection
591 @errors.cyclades.server_id
592 @errors.cyclades.metadata
593 def _run(self, server_id, key=''):
595 self.client.get_server_metadata(int(server_id), key),
598 def main(self, server_id, key=''):
599 super(self.__class__, self)._run()
600 self._run(server_id=server_id, key=key)
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
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)'
617 for keyval in keyvals:
618 k, sep, v = keyval.partition('=')
623 'Invalid piece of metadata %s' % keyval,
624 importance=2, details=[
625 'Correct metadata format: key=val',
627 '/server metadata set <server id>'
628 'key1=value1 key2=value2'])
630 self.client.update_server_metadata(int(server_id), **metadata),
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)
638 @command(server_cmds)
639 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
640 """Delete server (VM) metadata"""
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))
650 def main(self, server_id, key):
651 super(self.__class__, self)._run()
652 self._run(server_id=server_id, key=key)
655 @command(server_cmds)
656 class server_stats(_init_cyclades, _optional_json):
657 """Get server (VM) statistics"""
660 @errors.cyclades.connection
661 @errors.cyclades.server_id
662 def _run(self, server_id):
664 self.client.get_server_stats(int(server_id)), self.print_dict)
666 def main(self, server_id):
667 super(self.__class__, self)._run()
668 self._run(server_id=server_id)
671 @command(server_cmds)
672 class server_wait(_init_cyclades, _server_wait):
673 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
676 @errors.cyclades.connection
677 @errors.cyclades.server_id
678 def _run(self, server_id, currect_status):
679 self._wait(server_id, currect_status)
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)
686 @command(flavor_cmds)
687 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
688 """List available hardware flavors"""
690 PERMANENTS = ('id', 'name')
693 detail=FlagArgument('show detailed output', ('-l', '--details')),
694 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
696 'output results in pages (-n to set items per page, default 10)',
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'))
706 def _apply_common_filters(self, flavors):
707 common_filters = dict()
709 common_filters['ram'] = self['ram']
711 common_filters['vcpus'] = self['vcpus']
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)
719 @errors.cyclades.connection
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)
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']:
733 for key in set(flv).difference(self.PERMANENTS):
735 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
738 with_redundancy=self['detail'], with_enumeration=self['enum'],
741 pager(kwargs['out'].getvalue())
744 super(self.__class__, self)._run()
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
755 @errors.cyclades.connection
756 @errors.cyclades.flavor_id
757 def _run(self, flavor_id):
759 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
761 def main(self, flavor_id):
762 super(self.__class__, self)._run()
763 self._run(flavor_id=flavor_id)
766 def _add_name(self, net):
767 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
769 uuids.append(user_id)
771 uuids.append(tenant_id)
773 usernames = self._uuids2usernames(uuids)
775 net['user_id'] += ' (%s)' % usernames[user_id]
777 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
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
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'))
794 def main(self, network_id):
795 super(self.__class__, self)._run()
796 self._run(network_id=network_id)
799 @command(network_cmds)
800 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
803 PERMANENTS = ('id', 'name')
806 detail=FlagArgument('show detailed output', ('-l', '--details')),
807 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
809 'output results in pages (-n to set items per page, default 10)',
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')),
826 def _apply_common_filters(self, networks):
827 common_filter = dict()
831 common_filter['public'] = self['public']
832 elif self['private']:
833 common_filter['public'] = False
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'):
845 common_filter[term] = self[term]
846 return filter_dicts_by_dict(networks, common_filter)
848 def _add_name(self, networks, key='user_id'):
849 uuids = self._uuids2usernames(
850 list(set([net[key] for net in networks])))
852 v = net.get(key, None)
854 net[key] += ' (%s)' % uuids[v]
858 @errors.cyclades.connection
862 'status', 'public', 'private', 'user_id', 'user_name', 'type',
863 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
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)
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']:
877 for key in set(net).difference(self.PERMANENTS):
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'])
884 kwargs['out'] = StringIO()
887 networks = networks[:self['limit']]
888 self._print(networks, **kwargs)
890 pager(kwargs['out'].getvalue())
893 super(self.__class__, self)._run()
897 @command(network_cmds)
898 class network_create(_init_cyclades, _optional_json, _network_wait):
899 """Create an (unconnected) network"""
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'),
906 'Valid network types are '
907 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
909 default='MAC_FILTERED'),
910 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
914 @errors.cyclades.connection
915 @errors.cyclades.network_max
916 def _run(self, name):
917 r = self.client.create_network(
920 gateway=self['gateway'],
924 self._print(r, self.print_dict)
926 self._wait(r['id'], 'PENDING')
928 def main(self, name):
929 super(self.__class__, self)._run()
933 @command(network_cmds)
934 class network_rename(_init_cyclades, _optional_output_cmd):
935 """Set the name of a network"""
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))
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)
949 @command(network_cmds)
950 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
951 """Delete a network"""
954 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
958 @errors.cyclades.connection
959 @errors.cyclades.network_id
960 @errors.cyclades.network_in_use
961 def _run(self, network_id):
964 r = self.client.get_network_details(network_id)
966 if status in ('DELETED', ):
969 r = self.client.delete_network(int(network_id))
970 self._optional_output(r)
973 self._wait(network_id, status)
975 def main(self, network_id):
976 super(self.__class__, self)._run()
977 self._run(network_id=network_id)
980 @command(network_cmds)
981 class network_connect(_init_cyclades, _optional_output_cmd):
982 """Connect a server to a network"""
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)))
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)
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>
1004 @errors.cyclades.nic_format
1005 def _server_id_from_nic(self, nic_id):
1006 return nic_id.split('-')[1]
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:
1016 'Network Interface %s not found on server %s' % (
1019 print('Disconnected %s connections' % num_of_disconnected)
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)
1027 @command(network_cmds)
1028 class network_wait(_init_cyclades, _network_wait):
1029 """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1032 @errors.cyclades.connection
1033 @errors.cyclades.network_id
1034 def _run(self, network_id, currect_status):
1035 self._wait(network_id, currect_status)
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)
1042 @command(server_cmds)
1043 class server_ip(_init_cyclades):
1044 """Manage floating IPs for the servers"""
1047 @command(server_cmds)
1048 class server_ip_pools(_init_cyclades, _optional_json):
1049 """List all floating pools of floating ips"""
1052 @errors.cyclades.connection
1054 r = self.client.get_floating_ip_pools()
1055 self._print(r if self['json_output'] else r['floating_ip_pools'])
1058 super(self.__class__, self)._run()
1062 @command(server_cmds)
1063 class server_ip_list(_init_cyclades, _optional_json):
1064 """List all floating ips"""
1067 @errors.cyclades.connection
1069 r = self.client.get_floating_ips()
1070 self._print(r if self['json_output'] else r['floating_ips'])
1073 super(self.__class__, self)._run()
1077 @command(server_cmds)
1078 class server_ip_info(_init_cyclades, _optional_json):
1079 """A floating IPs' details"""
1082 @errors.cyclades.connection
1084 self._print(self.client.get_floating_ip(ip), self.print_dict)
1087 super(self.__class__, self)._run()
1091 @command(server_cmds)
1092 class server_ip_create(_init_cyclades, _optional_json):
1093 """Create a new floating IP"""
1095 arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1098 @errors.cyclades.connection
1099 def _run(self, ip=None):
1100 self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1102 def main(self, requested_address=None):
1103 super(self.__class__, self)._run()
1104 self._run(ip=requested_address)
1107 @command(server_cmds)
1108 class server_ip_delete(_init_cyclades, _optional_output_cmd):
1109 """Delete a floating ip"""
1112 @errors.cyclades.connection
1114 self._optional_output(self.client.delete_floating_ip(ip))
1117 super(self.__class__, self)._run()
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
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))
1132 def main(self, server_id, ip):
1133 super(self.__class__, self)._run()
1134 self._run(server_id=server_id, ip=ip)
1137 @command(server_cmds)
1138 class server_ip_detach(_init_cyclades, _optional_output_cmd):
1139 """Detach floating IP from server
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))
1148 def main(self, server_id, ip):
1149 super(self.__class__, self)._run()
1150 self._run(server_id=server_id, ip=ip)