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 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)']
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 \')',
150 'limit number of listed virtual servers', ('-n', '--number')),
152 'output results in pages (-n to set items per page, default 10)',
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.)',
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')),
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])))
173 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
174 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
177 def _apply_common_filters(self, servers):
178 common_filters = dict()
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)
186 def _filter_by_image(self, servers):
187 iid = self['image_id']
188 return [srv for srv in servers if srv['image']['id'] == iid]
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)]
195 def _filter_by_metadata(self, servers):
198 if not 'metadata' in srv:
200 meta = [dict(srv['metadata'])]
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)
207 new_servers.append(srv)
211 @errors.cyclades.connection
212 @errors.cyclades.date
214 withimage = bool(self['image_id'])
215 withflavor = bool(self['flavor_id'])
216 withmeta = bool(self['meta'] or self['meta_like'])
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'])
223 servers = self._filter_by_name(servers)
224 servers = self._filter_by_id(servers)
225 servers = self._apply_common_filters(servers)
227 servers = self._filter_by_image(servers)
229 servers = self._filter_by_flavor(servers)
231 servers = self._filter_by_metadata(servers)
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']:
239 for key in set(srv).difference(self.PERMANENTS):
241 kwargs = dict(with_enumeration=self['enum'])
243 kwargs['out'] = StringIO()
246 servers = servers[:self['limit']]
247 self._print(servers, **kwargs)
249 pager(kwargs['out'].getvalue())
252 super(self.__class__, self)._run()
256 @command(server_cmds)
257 class server_info(_init_cyclades, _optional_json):
258 """Detailed information on a Virtual Machine
260 - name, id, status, create/update dates
262 - metadata (e.g. os, superuser) and diagnostics
263 - hardware flavor and os image ids
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)
276 def main(self, server_id):
277 super(self.__class__, self)._run()
278 self._run(server_id=server_id)
281 class PersonalityArgument(KeyValueArgument):
284 return self._value if hasattr(self, '_value') else []
287 def value(self, newvalue):
288 if newvalue == self.default:
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)
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())
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]
314 @command(server_cmds)
315 class server_create(_init_cyclades, _optional_json, _server_wait):
316 """Create a server (aka Virtual Machine)
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
324 personality=PersonalityArgument(
325 (80 * ' ').join(howto_personality), ('-p', '--personality')),
326 wait=FlagArgument('Wait server to build', ('-w', '--wait'))
330 @errors.cyclades.connection
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)
341 self._wait(r['id'], r['status'])
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)
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
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))
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)
367 @command(server_cmds)
368 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
369 """Delete a virtual server"""
372 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
376 @errors.cyclades.connection
377 @errors.cyclades.server_id
378 def _run(self, server_id):
381 details = self.client.get_server_details(server_id)
382 status = details['status']
384 r = self.client.delete_server(int(server_id))
385 self._optional_output(r)
388 self._wait(server_id, status)
390 def main(self, server_id):
391 super(self.__class__, self)._run()
392 self._run(server_id=server_id)
395 @command(server_cmds)
396 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
397 """Reboot a virtual server"""
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'))
407 @errors.cyclades.connection
408 @errors.cyclades.server_id
409 def _run(self, server_id):
410 hard_reboot = self['hard']
413 'WARNING: -f/--force will be deprecated in version 0.12\n'
414 '\tIn the future, please use --type=hard instead')
416 if self['type'].lower() in ('soft', ):
418 elif self['type'].lower() in ('hard', ):
421 raise CLISyntaxError(
422 'Invalid reboot type %s' % self['type'],
423 importance=2, details=[
424 '--type values are either SOFT (default) or HARD'])
426 r = self.client.reboot_server(int(server_id), hard_reboot)
427 self._optional_output(r)
430 self._wait(server_id, 'REBOOT')
432 def main(self, server_id):
433 super(self.__class__, self)._run()
434 self._run(server_id=server_id)
437 @command(server_cmds)
438 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
439 """Start an existing virtual server"""
442 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
446 @errors.cyclades.connection
447 @errors.cyclades.server_id
448 def _run(self, server_id):
451 details = self.client.get_server_details(server_id)
452 status = details['status']
453 if status in ('ACTIVE', ):
456 r = self.client.start_server(int(server_id))
457 self._optional_output(r)
460 self._wait(server_id, status)
462 def main(self, server_id):
463 super(self.__class__, self)._run()
464 self._run(server_id=server_id)
467 @command(server_cmds)
468 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
469 """Shutdown an active virtual server"""
472 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
476 @errors.cyclades.connection
477 @errors.cyclades.server_id
478 def _run(self, server_id):
481 details = self.client.get_server_details(server_id)
482 status = details['status']
483 if status in ('STOPPED', ):
486 r = self.client.shutdown_server(int(server_id))
487 self._optional_output(r)
490 self._wait(server_id, status)
492 def main(self, server_id):
493 super(self.__class__, self)._run()
494 self._run(server_id=server_id)
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
507 @errors.cyclades.connection
508 @errors.cyclades.server_id
509 def _run(self, server_id):
511 self.client.get_server_console(int(server_id)), self.print_dict)
513 def main(self, server_id):
514 super(self.__class__, self)._run()
515 self._run(server_id=server_id)
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:
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))
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)
538 @command(server_cmds)
539 class server_firewall(_init_cyclades):
540 """Manage virtual server firewall profiles for public networks"""
543 @command(server_cmds)
544 class server_firewall_set(_init_cyclades, _optional_output_cmd):
545 """Set the firewall profile on virtual server public network
547 - DISABLED: Shutdown firewall
548 - ENABLED: Firewall in normal mode
549 - PROTECTED: Firewall in secure mode
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()))
560 def main(self, server_id, profile):
561 super(self.__class__, self)._run()
562 self._run(server_id=server_id, profile=profile)
565 @command(server_cmds)
566 class server_firewall_get(_init_cyclades):
567 """Get the firewall profile for a virtual servers' public network"""
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))
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_addr(_init_cyclades, _optional_json):
582 """List the addresses of all network interfaces on a virtual server"""
585 enum=FlagArgument('Enumerate results', '--enumerate')
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)
595 def main(self, server_id):
596 super(self.__class__, self)._run()
597 self._run(server_id=server_id)
600 @command(server_cmds)
601 class server_metadata(_init_cyclades):
602 """Manage Server metadata (key:value pairs of server attributes)"""
605 @command(server_cmds)
606 class server_metadata_list(_init_cyclades, _optional_json):
607 """Get server metadata"""
610 @errors.cyclades.connection
611 @errors.cyclades.server_id
612 @errors.cyclades.metadata
613 def _run(self, server_id, key=''):
615 self.client.get_server_metadata(int(server_id), key),
618 def main(self, server_id, key=''):
619 super(self.__class__, self)._run()
620 self._run(server_id=server_id, key=key)
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
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)'
637 for keyval in keyvals:
638 k, sep, v = keyval.partition('=')
643 'Invalid piece of metadata %s' % keyval,
644 importance=2, details=[
645 'Correct metadata format: key=val',
647 '/server metadata set <server id>'
648 'key1=value1 key2=value2'])
650 self.client.update_server_metadata(int(server_id), **metadata),
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)
658 @command(server_cmds)
659 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
660 """Delete virtual server metadata"""
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))
670 def main(self, server_id, key):
671 super(self.__class__, self)._run()
672 self._run(server_id=server_id, key=key)
675 @command(server_cmds)
676 class server_stats(_init_cyclades, _optional_json):
677 """Get virtual server statistics"""
680 @errors.cyclades.connection
681 @errors.cyclades.server_id
682 def _run(self, server_id):
684 self.client.get_server_stats(int(server_id)), self.print_dict)
686 def main(self, server_id):
687 super(self.__class__, self)._run()
688 self._run(server_id=server_id)
691 @command(server_cmds)
692 class server_wait(_init_cyclades, _server_wait):
693 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
696 @errors.cyclades.connection
697 @errors.cyclades.server_id
698 def _run(self, server_id, currect_status):
699 self._wait(server_id, currect_status)
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)
706 @command(flavor_cmds)
707 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
708 """List available hardware flavors"""
710 PERMANENTS = ('id', 'name')
713 detail=FlagArgument('show detailed output', ('-l', '--details')),
714 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
716 'output results in pages (-n to set items per page, default 10)',
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'))
726 def _apply_common_filters(self, flavors):
727 common_filters = dict()
729 common_filters['ram'] = self['ram']
731 common_filters['vcpus'] = self['vcpus']
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)
739 @errors.cyclades.connection
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)
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']:
753 for key in set(flv).difference(self.PERMANENTS):
755 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
758 with_redundancy=self['detail'], with_enumeration=self['enum'],
761 pager(kwargs['out'].getvalue())
764 super(self.__class__, self)._run()
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
775 @errors.cyclades.connection
776 @errors.cyclades.flavor_id
777 def _run(self, flavor_id):
779 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
781 def main(self, flavor_id):
782 super(self.__class__, self)._run()
783 self._run(flavor_id=flavor_id)
786 def _add_name(self, net):
787 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
789 uuids.append(user_id)
791 uuids.append(tenant_id)
793 usernames = self._uuids2usernames(uuids)
795 net['user_id'] += ' (%s)' % usernames[user_id]
797 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
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
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'))
814 def main(self, network_id):
815 super(self.__class__, self)._run()
816 self._run(network_id=network_id)
819 @command(network_cmds)
820 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
823 PERMANENTS = ('id', 'name')
826 detail=FlagArgument('show detailed output', ('-l', '--details')),
827 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
829 'output results in pages (-n to set items per page, default 10)',
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')),
846 def _apply_common_filters(self, networks):
847 common_filter = dict()
851 common_filter['public'] = self['public']
852 elif self['private']:
853 common_filter['public'] = False
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'):
865 common_filter[term] = self[term]
866 return filter_dicts_by_dict(networks, common_filter)
868 def _add_name(self, networks, key='user_id'):
869 uuids = self._uuids2usernames(
870 list(set([net[key] for net in networks])))
872 v = net.get(key, None)
874 net[key] += ' (%s)' % uuids[v]
878 @errors.cyclades.connection
882 'status', 'public', 'private', 'user_id', 'user_name', 'type',
883 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
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)
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']:
897 for key in set(net).difference(self.PERMANENTS):
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'])
904 kwargs['out'] = StringIO()
907 networks = networks[:self['limit']]
908 self._print(networks, **kwargs)
910 pager(kwargs['out'].getvalue())
913 super(self.__class__, self)._run()
917 @command(network_cmds)
918 class network_create(_init_cyclades, _optional_json, _network_wait):
919 """Create an (unconnected) network"""
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'),
926 'Valid network types are '
927 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
929 default='MAC_FILTERED'),
930 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
934 @errors.cyclades.connection
935 @errors.cyclades.network_max
936 def _run(self, name):
937 r = self.client.create_network(
940 gateway=self['gateway'],
944 self._print(r, self.print_dict)
946 self._wait(r['id'], 'PENDING')
948 def main(self, name):
949 super(self.__class__, self)._run()
953 @command(network_cmds)
954 class network_rename(_init_cyclades, _optional_output_cmd):
955 """Set the name of a network"""
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))
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)
969 @command(network_cmds)
970 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
971 """Delete a network"""
974 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
978 @errors.cyclades.connection
979 @errors.cyclades.network_id
980 @errors.cyclades.network_in_use
981 def _run(self, network_id):
984 r = self.client.get_network_details(network_id)
986 if status in ('DELETED', ):
989 r = self.client.delete_network(int(network_id))
990 self._optional_output(r)
993 self._wait(network_id, status)
995 def main(self, network_id):
996 super(self.__class__, self)._run()
997 self._run(network_id=network_id)
1000 @command(network_cmds)
1001 class network_connect(_init_cyclades, _optional_output_cmd):
1002 """Connect a server to a network"""
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)))
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)
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>
1024 @errors.cyclades.nic_format
1025 def _server_id_from_nic(self, nic_id):
1026 return nic_id.split('-')[1]
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:
1036 'Network Interface %s not found on server %s' % (
1039 print('Disconnected %s connections' % num_of_disconnected)
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)
1047 @command(network_cmds)
1048 class network_wait(_init_cyclades, _network_wait):
1049 """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1052 @errors.cyclades.connection
1053 @errors.cyclades.network_id
1054 def _run(self, network_id, currect_status):
1055 self._wait(network_id, currect_status)
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)
1062 @command(server_cmds)
1063 class server_ip(_init_cyclades):
1064 """Manage floating IPs for the servers"""
1067 @command(server_cmds)
1068 class server_ip_pools(_init_cyclades, _optional_json):
1069 """List all floating pools of floating ips"""
1072 @errors.cyclades.connection
1074 r = self.client.get_floating_ip_pools()
1075 self._print(r if self['json_output'] else r['floating_ip_pools'])
1078 super(self.__class__, self)._run()
1082 @command(server_cmds)
1083 class server_ip_list(_init_cyclades, _optional_json):
1084 """List all floating ips"""
1087 @errors.cyclades.connection
1089 r = self.client.get_floating_ips()
1090 self._print(r if self['json_output'] else r['floating_ips'])
1093 super(self.__class__, self)._run()
1097 @command(server_cmds)
1098 class server_ip_info(_init_cyclades, _optional_json):
1099 """A floating IPs' details"""
1102 @errors.cyclades.connection
1104 self._print(self.client.get_floating_ip(ip), self.print_dict)
1107 super(self.__class__, self)._run()
1111 @command(server_cmds)
1112 class server_ip_create(_init_cyclades, _optional_json):
1113 """Create a new floating IP"""
1115 arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1118 @errors.cyclades.connection
1119 def _run(self, ip=None):
1120 self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1122 def main(self, requested_address=None):
1123 super(self.__class__, self)._run()
1124 self._run(ip=requested_address)
1127 @command(server_cmds)
1128 class server_ip_delete(_init_cyclades, _optional_output_cmd):
1129 """Delete a floating ip"""
1132 @errors.cyclades.connection
1134 self._optional_output(self.client.delete_floating_ip(ip))
1137 super(self.__class__, self)._run()
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
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))
1152 def main(self, server_id, ip):
1153 super(self.__class__, self)._run()
1154 self._run(server_id=server_id, ip=ip)
1157 @command(server_cmds)
1158 class server_ip_detach(_init_cyclades, _optional_output_cmd):
1159 """Detach floating IP from server
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))
1168 def main(self, server_id, ip):
1169 super(self.__class__, self)._run()
1170 self._run(server_id=server_id, ip=ip)