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 ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
55 _commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
58 about_authentication = '\nUser Authentication:\
59 \n* to check authentication: /user authenticate\
60 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
63 'Defines a file to be injected to virtual servers file system.',
64 'syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65 ' PATH: local file to be injected (relative or absolute)',
66 ' SERVER_PATH: destination location inside server Image',
67 ' OWNER: virtual servers user id of the remote destination file',
68 ' GROUP: virtual servers group id or name of the destination file',
69 ' MODEL: permition in octal (e.g., 0777 or o+rwx)']
72 class _service_wait(object):
74 wait_arguments = dict(
75 progress_bar=ProgressBarArgument(
76 'do not show progress bar', ('-N', '--no-progress-bar'), False)
80 self, service, service_id, status_method, current_status,
81 countdown=True, timeout=60):
82 (progress_bar, wait_cb) = self._safe_progress_bar(
83 '%s %s: status is still %s' % (
84 service, service_id, current_status),
85 countdown=countdown, timeout=timeout)
88 new_mode = status_method(
89 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
91 self.error('%s %s: status is now %s' % (
92 service, service_id, new_mode))
94 self.error('%s %s: status is still %s' % (
95 service, service_id, current_status))
96 except KeyboardInterrupt:
97 self.error('\n- canceled')
99 self._safe_progress_bar_finish(progress_bar)
102 class _server_wait(_service_wait):
104 def _wait(self, server_id, current_status, timeout=60):
105 super(_server_wait, self)._wait(
106 'Server', server_id, self.client.wait_server, current_status,
107 countdown=(current_status not in ('BUILD', )),
108 timeout=timeout if current_status not in ('BUILD', ) else 100)
111 class _network_wait(_service_wait):
113 def _wait(self, net_id, current_status, timeout=60):
114 super(_network_wait, self)._wait(
115 'Network', net_id, self.client.wait_network, current_status,
119 class _init_cyclades(_command_init):
122 def _run(self, service='compute'):
123 if getattr(self, 'cloud', None):
124 base_url = self._custom_url(service) or self._custom_url(
127 token = self._custom_token(service) or self._custom_token(
128 'cyclades') or self.config.get_cloud('token')
129 self.client = CycladesClient(base_url=base_url, token=token)
132 self.cloud = 'default'
133 if getattr(self, 'auth_base', False):
134 cyclades_endpoints = self.auth_base.get_service_endpoints(
135 self._custom_type('cyclades') or 'compute',
136 self._custom_version('cyclades') or '')
137 base_url = cyclades_endpoints['publicURL']
138 token = self.auth_base.token
139 self.client = CycladesClient(base_url=base_url, token=token)
141 raise CLIBaseUrlError(service='cyclades')
147 @command(server_cmds)
148 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
149 """List virtual servers accessible by user
150 Use filtering arguments (e.g., --name-like) to manage long server lists
153 PERMANENTS = ('id', 'name')
156 detail=FlagArgument('show detailed output', ('-l', '--details')),
158 'show only items since date (\' d/m/Y H:M:S \')',
161 'limit number of listed virtual servers', ('-n', '--number')),
163 'output results in pages (-n to set items per page, default 10)',
165 enum=FlagArgument('Enumerate results', '--enumerate'),
166 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
167 image_id=ValueArgument('filter by image id', ('--image-id')),
168 user_id=ValueArgument('filter by user id', ('--user-id')),
169 user_name=ValueArgument('filter by user name', ('--user-name')),
170 status=ValueArgument(
171 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
173 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
174 meta_like=KeyValueArgument(
175 'print only if in key=value, the value is part of actual value',
176 ('--metadata-like')),
179 def _add_user_name(self, servers):
180 uuids = self._uuids2usernames(list(set(
181 [srv['user_id'] for srv in servers] +
182 [srv['tenant_id'] for srv in servers])))
184 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
185 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
188 def _apply_common_filters(self, servers):
189 common_filters = dict()
191 common_filters['status'] = self['status']
192 if self['user_id'] or self['user_name']:
193 uuid = self['user_id'] or self._username2uuid(self['user_name'])
194 common_filters['user_id'] = uuid
195 return filter_dicts_by_dict(servers, common_filters)
197 def _filter_by_image(self, servers):
198 iid = self['image_id']
199 return [srv for srv in servers if srv['image']['id'] == iid]
201 def _filter_by_flavor(self, servers):
202 fid = self['flavor_id']
203 return [srv for srv in servers if (
204 '%s' % srv['image']['id'] == '%s' % fid)]
206 def _filter_by_metadata(self, servers):
209 if not 'metadata' in srv:
211 meta = [dict(srv['metadata'])]
213 meta = filter_dicts_by_dict(meta, self['meta'])
214 if meta and self['meta_like']:
215 meta = filter_dicts_by_dict(
216 meta, self['meta_like'], exact_match=False)
218 new_servers.append(srv)
222 @errors.cyclades.connection
223 @errors.cyclades.date
225 withimage = bool(self['image_id'])
226 withflavor = bool(self['flavor_id'])
227 withmeta = bool(self['meta'] or self['meta_like'])
229 self['status'] or self['user_id'] or self['user_name'])
230 detail = self['detail'] or (
231 withimage or withflavor or withmeta or withcommons)
232 servers = self.client.list_servers(detail, self['since'])
234 servers = self._filter_by_name(servers)
235 servers = self._filter_by_id(servers)
236 servers = self._apply_common_filters(servers)
238 servers = self._filter_by_image(servers)
240 servers = self._filter_by_flavor(servers)
242 servers = self._filter_by_metadata(servers)
244 if self['detail'] and not self['json_output']:
245 servers = self._add_user_name(servers)
246 elif not (self['detail'] or self['json_output']):
247 remove_from_items(servers, 'links')
248 if detail and not self['detail']:
250 for key in set(srv).difference(self.PERMANENTS):
252 kwargs = dict(with_enumeration=self['enum'])
254 kwargs['out'] = StringIO()
257 servers = servers[:self['limit']]
258 self._print(servers, **kwargs)
260 pager(kwargs['out'].getvalue())
263 super(self.__class__, self)._run()
267 @command(server_cmds)
268 class server_info(_init_cyclades, _optional_json):
269 """Detailed information on a Virtual Machine
271 - name, id, status, create/update dates
273 - metadata (e.g., os, superuser) and diagnostics
274 - hardware flavor and os image ids
278 @errors.cyclades.connection
279 @errors.cyclades.server_id
280 def _run(self, server_id):
281 vm = self.client.get_server_details(server_id)
282 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
283 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
284 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
285 self._print(vm, self.print_dict)
287 def main(self, server_id):
288 super(self.__class__, self)._run()
289 self._run(server_id=server_id)
292 class PersonalityArgument(KeyValueArgument):
295 return self._value if hasattr(self, '_value') else []
298 def value(self, newvalue):
299 if newvalue == self.default:
302 for i, terms in enumerate(newvalue):
303 termlist = terms.split(',')
304 if len(termlist) > 5:
305 msg = 'Wrong number of terms (should be 1 to 5)'
306 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
311 '--personality: File %s does not exist' % path,
312 importance=1, details=howto_personality)
313 self._value.append(dict(path=path))
314 with open(path) as f:
315 self._value[i]['contents'] = b64encode(f.read())
317 self._value[i]['path'] = termlist[1]
318 self._value[i]['owner'] = termlist[2]
319 self._value[i]['group'] = termlist[3]
320 self._value[i]['mode'] = termlist[4]
325 @command(server_cmds)
326 class server_create(_init_cyclades, _optional_json, _server_wait):
327 """Create a server (aka Virtual Machine)
329 - name: (single quoted text)
330 - flavor id: Hardware flavor. Pick one from: /flavor list
331 - image id: OS images. Pick one from: /image list
335 personality=PersonalityArgument(
336 (80 * ' ').join(howto_personality), ('-p', '--personality')),
337 wait=FlagArgument('Wait server to build', ('-w', '--wait'))
341 @errors.cyclades.connection
343 @errors.cyclades.flavor_id
344 def _run(self, name, flavor_id, image_id):
345 r = self.client.create_server(
346 name, int(flavor_id), image_id, personality=self['personality'])
347 usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
348 r['user_id'] += ' (%s)' % usernames[r['user_id']]
349 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
350 self._print(r, self.print_dict)
352 self._wait(r['id'], r['status'])
354 def main(self, name, flavor_id, image_id):
355 super(self.__class__, self)._run()
356 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
359 @command(server_cmds)
360 class server_rename(_init_cyclades, _optional_output_cmd):
361 """Set/update a virtual server name
362 virtual server names are not unique, therefore multiple servers may share
367 @errors.cyclades.connection
368 @errors.cyclades.server_id
369 def _run(self, server_id, new_name):
370 self._optional_output(
371 self.client.update_server_name(int(server_id), new_name))
373 def main(self, server_id, new_name):
374 super(self.__class__, self)._run()
375 self._run(server_id=server_id, new_name=new_name)
378 @command(server_cmds)
379 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
380 """Delete a virtual server"""
383 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
387 @errors.cyclades.connection
388 @errors.cyclades.server_id
389 def _run(self, server_id):
392 details = self.client.get_server_details(server_id)
393 status = details['status']
395 r = self.client.delete_server(int(server_id))
396 self._optional_output(r)
399 self._wait(server_id, status)
401 def main(self, server_id):
402 super(self.__class__, self)._run()
403 self._run(server_id=server_id)
406 @command(server_cmds)
407 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
408 """Reboot a virtual server"""
412 'perform a hard reboot (deprecated)', ('-f', '--force')),
413 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
414 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
418 @errors.cyclades.connection
419 @errors.cyclades.server_id
420 def _run(self, server_id):
421 hard_reboot = self['hard']
424 'WARNING: -f/--force will be deprecated in version 0.12\n'
425 '\tIn the future, please use --type=hard instead')
427 if self['type'].lower() in ('soft', ):
429 elif self['type'].lower() in ('hard', ):
432 raise CLISyntaxError(
433 'Invalid reboot type %s' % self['type'],
434 importance=2, details=[
435 '--type values are either SOFT (default) or HARD'])
437 r = self.client.reboot_server(int(server_id), hard_reboot)
438 self._optional_output(r)
441 self._wait(server_id, 'REBOOT')
443 def main(self, server_id):
444 super(self.__class__, self)._run()
445 self._run(server_id=server_id)
448 @command(server_cmds)
449 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
450 """Start an existing virtual server"""
453 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
457 @errors.cyclades.connection
458 @errors.cyclades.server_id
459 def _run(self, server_id):
462 details = self.client.get_server_details(server_id)
463 status = details['status']
464 if status in ('ACTIVE', ):
467 r = self.client.start_server(int(server_id))
468 self._optional_output(r)
471 self._wait(server_id, status)
473 def main(self, server_id):
474 super(self.__class__, self)._run()
475 self._run(server_id=server_id)
478 @command(server_cmds)
479 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
480 """Shutdown an active virtual server"""
483 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
487 @errors.cyclades.connection
488 @errors.cyclades.server_id
489 def _run(self, server_id):
492 details = self.client.get_server_details(server_id)
493 status = details['status']
494 if status in ('STOPPED', ):
497 r = self.client.shutdown_server(int(server_id))
498 self._optional_output(r)
501 self._wait(server_id, status)
503 def main(self, server_id):
504 super(self.__class__, self)._run()
505 self._run(server_id=server_id)
508 @command(server_cmds)
509 class server_console(_init_cyclades, _optional_json):
510 """Get a VNC console to access an existing virtual server
511 Console connection information provided (at least):
512 - host: (url or address) a VNC host
513 - port: (int) the gateway to enter virtual server on host
514 - password: for VNC authorization
518 @errors.cyclades.connection
519 @errors.cyclades.server_id
520 def _run(self, server_id):
522 self.client.get_server_console(int(server_id)), self.print_dict)
524 def main(self, server_id):
525 super(self.__class__, self)._run()
526 self._run(server_id=server_id)
529 @command(server_cmds)
530 class server_resize(_init_cyclades, _optional_output_cmd):
531 """Set a different flavor for an existing server
532 To get server ids and flavor ids:
538 @errors.cyclades.connection
539 @errors.cyclades.server_id
540 @errors.cyclades.flavor_id
541 def _run(self, server_id, flavor_id):
542 self._optional_output(self.client.resize_server(server_id, flavor_id))
544 def main(self, server_id, flavor_id):
545 super(self.__class__, self)._run()
546 self._run(server_id=server_id, flavor_id=flavor_id)
549 @command(server_cmds)
550 class server_firewall(_init_cyclades):
551 """Manage virtual server firewall profiles for public networks"""
554 @command(server_cmds)
555 class server_firewall_set(_init_cyclades, _optional_output_cmd):
556 """Set the firewall profile on virtual server public network
558 - DISABLED: Shutdown firewall
559 - ENABLED: Firewall in normal mode
560 - PROTECTED: Firewall in secure mode
564 @errors.cyclades.connection
565 @errors.cyclades.server_id
566 @errors.cyclades.firewall
567 def _run(self, server_id, profile):
568 self._optional_output(self.client.set_firewall_profile(
569 server_id=int(server_id), profile=('%s' % profile).upper()))
571 def main(self, server_id, profile):
572 super(self.__class__, self)._run()
573 self._run(server_id=server_id, profile=profile)
576 @command(server_cmds)
577 class server_firewall_get(_init_cyclades):
578 """Get the firewall profile for a virtual servers' public network"""
581 @errors.cyclades.connection
582 @errors.cyclades.server_id
583 def _run(self, server_id):
584 self.writeln(self.client.get_firewall_profile(server_id))
586 def main(self, server_id):
587 super(self.__class__, self)._run()
588 self._run(server_id=server_id)
591 @command(server_cmds)
592 class server_addr(_init_cyclades, _optional_json):
593 """List the addresses of all network interfaces on a virtual server"""
596 enum=FlagArgument('Enumerate results', '--enumerate')
600 @errors.cyclades.connection
601 @errors.cyclades.server_id
602 def _run(self, server_id):
603 reply = self.client.list_server_nics(int(server_id))
604 self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
606 def main(self, server_id):
607 super(self.__class__, self)._run()
608 self._run(server_id=server_id)
611 @command(server_cmds)
612 class server_metadata(_init_cyclades):
613 """Manage Server metadata (key:value pairs of server attributes)"""
616 @command(server_cmds)
617 class server_metadata_list(_init_cyclades, _optional_json):
618 """Get server metadata"""
621 @errors.cyclades.connection
622 @errors.cyclades.server_id
623 @errors.cyclades.metadata
624 def _run(self, server_id, key=''):
626 self.client.get_server_metadata(int(server_id), key),
629 def main(self, server_id, key=''):
630 super(self.__class__, self)._run()
631 self._run(server_id=server_id, key=key)
634 @command(server_cmds)
635 class server_metadata_set(_init_cyclades, _optional_json):
636 """Set / update virtual server metadata
637 Metadata should be given in key/value pairs in key=value format
638 For example: /server metadata set <server id> key1=value1 key2=value2
639 Old, unreferenced metadata will remain intact
643 @errors.cyclades.connection
644 @errors.cyclades.server_id
645 def _run(self, server_id, keyvals):
646 assert keyvals, 'Please, add some metadata ( key=value)'
648 for keyval in keyvals:
649 k, sep, v = keyval.partition('=')
654 'Invalid piece of metadata %s' % keyval,
655 importance=2, details=[
656 'Correct metadata format: key=val',
658 '/server metadata set <server id>'
659 'key1=value1 key2=value2'])
661 self.client.update_server_metadata(int(server_id), **metadata),
664 def main(self, server_id, *key_equals_val):
665 super(self.__class__, self)._run()
666 self._run(server_id=server_id, keyvals=key_equals_val)
669 @command(server_cmds)
670 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
671 """Delete virtual server metadata"""
674 @errors.cyclades.connection
675 @errors.cyclades.server_id
676 @errors.cyclades.metadata
677 def _run(self, server_id, key):
678 self._optional_output(
679 self.client.delete_server_metadata(int(server_id), key))
681 def main(self, server_id, key):
682 super(self.__class__, self)._run()
683 self._run(server_id=server_id, key=key)
686 @command(server_cmds)
687 class server_stats(_init_cyclades, _optional_json):
688 """Get virtual server statistics"""
691 @errors.cyclades.connection
692 @errors.cyclades.server_id
693 def _run(self, server_id):
695 self.client.get_server_stats(int(server_id)), self.print_dict)
697 def main(self, server_id):
698 super(self.__class__, self)._run()
699 self._run(server_id=server_id)
702 @command(server_cmds)
703 class server_wait(_init_cyclades, _server_wait):
704 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
708 'Wait limit in seconds (default: 60)', '--timeout', default=60)
712 @errors.cyclades.connection
713 @errors.cyclades.server_id
714 def _run(self, server_id, current_status):
715 r = self.client.get_server_details(server_id)
716 if r['status'].lower() == current_status.lower():
717 self._wait(server_id, current_status, timeout=self['timeout'])
720 'Server %s: Cannot wait for status %s, '
721 'status is already %s' % (
722 server_id, current_status, r['status']))
724 def main(self, server_id, current_status='BUILD'):
725 super(self.__class__, self)._run()
726 self._run(server_id=server_id, current_status=current_status)
729 @command(flavor_cmds)
730 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
731 """List available hardware flavors"""
733 PERMANENTS = ('id', 'name')
736 detail=FlagArgument('show detailed output', ('-l', '--details')),
737 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
739 'output results in pages (-n to set items per page, default 10)',
741 enum=FlagArgument('Enumerate results', '--enumerate'),
742 ram=ValueArgument('filter by ram', ('--ram')),
743 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
744 disk=ValueArgument('filter by disk size in GB', ('--disk')),
745 disk_template=ValueArgument(
746 'filter by disk_templace', ('--disk-template'))
749 def _apply_common_filters(self, flavors):
750 common_filters = dict()
752 common_filters['ram'] = self['ram']
754 common_filters['vcpus'] = self['vcpus']
756 common_filters['disk'] = self['disk']
757 if self['disk_template']:
758 common_filters['SNF:disk_template'] = self['disk_template']
759 return filter_dicts_by_dict(flavors, common_filters)
762 @errors.cyclades.connection
764 withcommons = self['ram'] or self['vcpus'] or (
765 self['disk'] or self['disk_template'])
766 detail = self['detail'] or withcommons
767 flavors = self.client.list_flavors(detail)
768 flavors = self._filter_by_name(flavors)
769 flavors = self._filter_by_id(flavors)
771 flavors = self._apply_common_filters(flavors)
772 if not (self['detail'] or self['json_output']):
773 remove_from_items(flavors, 'links')
774 if detail and not self['detail']:
776 for key in set(flv).difference(self.PERMANENTS):
778 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
781 with_redundancy=self['detail'], with_enumeration=self['enum'],
784 pager(kwargs['out'].getvalue())
787 super(self.__class__, self)._run()
791 @command(flavor_cmds)
792 class flavor_info(_init_cyclades, _optional_json):
793 """Detailed information on a hardware flavor
794 To get a list of available flavors and flavor ids, try /flavor list
798 @errors.cyclades.connection
799 @errors.cyclades.flavor_id
800 def _run(self, flavor_id):
802 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
804 def main(self, flavor_id):
805 super(self.__class__, self)._run()
806 self._run(flavor_id=flavor_id)
809 def _add_name(self, net):
810 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
812 uuids.append(user_id)
814 uuids.append(tenant_id)
816 usernames = self._uuids2usernames(uuids)
818 net['user_id'] += ' (%s)' % usernames[user_id]
820 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
823 @command(network_cmds)
824 class network_info(_init_cyclades, _optional_json):
825 """Detailed information on a network
826 To get a list of available networks and network ids, try /network list
830 @errors.cyclades.connection
831 @errors.cyclades.network_id
832 def _run(self, network_id):
833 network = self.client.get_network_details(int(network_id))
834 _add_name(self, network)
835 self._print(network, self.print_dict, exclude=('id'))
837 def main(self, network_id):
838 super(self.__class__, self)._run()
839 self._run(network_id=network_id)
842 @command(network_cmds)
843 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
846 PERMANENTS = ('id', 'name')
849 detail=FlagArgument('show detailed output', ('-l', '--details')),
850 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
852 'output results in pages (-n to set items per page, default 10)',
854 enum=FlagArgument('Enumerate results', '--enumerate'),
855 status=ValueArgument('filter by status', ('--status')),
856 public=FlagArgument('only public networks', ('--public')),
857 private=FlagArgument('only private networks', ('--private')),
858 dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
859 no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
860 user_id=ValueArgument('filter by user id', ('--user-id')),
861 user_name=ValueArgument('filter by user name', ('--user-name')),
862 gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
863 gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
864 cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
865 cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
866 type=ValueArgument('filter by type', ('--type')),
869 def _apply_common_filters(self, networks):
870 common_filter = dict()
874 common_filter['public'] = self['public']
875 elif self['private']:
876 common_filter['public'] = False
880 common_filter['dhcp'] = True
881 elif self['no_dhcp']:
882 common_filter['dhcp'] = False
883 if self['user_id'] or self['user_name']:
884 uuid = self['user_id'] or self._username2uuid(self['user_name'])
885 common_filter['user_id'] = uuid
886 for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
888 common_filter[term] = self[term]
889 return filter_dicts_by_dict(networks, common_filter)
891 def _add_name(self, networks, key='user_id'):
892 uuids = self._uuids2usernames(
893 list(set([net[key] for net in networks])))
895 v = net.get(key, None)
897 net[key] += ' (%s)' % uuids[v]
901 @errors.cyclades.connection
905 'status', 'public', 'private', 'user_id', 'user_name', 'type',
906 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
910 detail = self['detail'] or withcommons
911 networks = self.client.list_networks(detail)
912 networks = self._filter_by_name(networks)
913 networks = self._filter_by_id(networks)
915 networks = self._apply_common_filters(networks)
916 if not (self['detail'] or self['json_output']):
917 remove_from_items(networks, 'links')
918 if detail and not self['detail']:
920 for key in set(net).difference(self.PERMANENTS):
922 if self['detail'] and not self['json_output']:
923 self._add_name(networks)
924 self._add_name(networks, 'tenant_id')
925 kwargs = dict(with_enumeration=self['enum'])
927 kwargs['out'] = StringIO()
930 networks = networks[:self['limit']]
931 self._print(networks, **kwargs)
933 pager(kwargs['out'].getvalue())
936 super(self.__class__, self)._run()
940 @command(network_cmds)
941 class network_create(_init_cyclades, _optional_json, _network_wait):
942 """Create an (unconnected) network"""
945 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
946 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
947 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
949 'Valid network types are '
950 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
952 default='MAC_FILTERED'),
953 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
957 @errors.cyclades.connection
958 @errors.cyclades.network_max
959 def _run(self, name):
960 r = self.client.create_network(
963 gateway=self['gateway'],
967 self._print(r, self.print_dict)
968 if self['wait'] and r['status'] in ('PENDING', ):
969 self._wait(r['id'], 'PENDING')
971 def main(self, name):
972 super(self.__class__, self)._run()
976 @command(network_cmds)
977 class network_rename(_init_cyclades, _optional_output_cmd):
978 """Set the name of a network"""
981 @errors.cyclades.connection
982 @errors.cyclades.network_id
983 def _run(self, network_id, new_name):
984 self._optional_output(
985 self.client.update_network_name(int(network_id), new_name))
987 def main(self, network_id, new_name):
988 super(self.__class__, self)._run()
989 self._run(network_id=network_id, new_name=new_name)
992 @command(network_cmds)
993 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
994 """Delete a network"""
997 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1001 @errors.cyclades.connection
1002 @errors.cyclades.network_id
1003 @errors.cyclades.network_in_use
1004 def _run(self, network_id):
1007 r = self.client.get_network_details(network_id)
1008 status = r['status']
1009 if status in ('DELETED', ):
1012 r = self.client.delete_network(int(network_id))
1013 self._optional_output(r)
1016 self._wait(network_id, status)
1018 def main(self, network_id):
1019 super(self.__class__, self)._run()
1020 self._run(network_id=network_id)
1023 @command(network_cmds)
1024 class network_connect(_init_cyclades, _optional_output_cmd):
1025 """Connect a server to a network"""
1028 @errors.cyclades.connection
1029 @errors.cyclades.server_id
1030 @errors.cyclades.network_id
1031 def _run(self, server_id, network_id):
1032 self._optional_output(
1033 self.client.connect_server(int(server_id), int(network_id)))
1035 def main(self, server_id, network_id):
1036 super(self.__class__, self)._run()
1037 self._run(server_id=server_id, network_id=network_id)
1040 @command(network_cmds)
1041 class network_disconnect(_init_cyclades):
1042 """Disconnect a nic that connects a server to a network
1043 Nic ids are listed as "attachments" in detailed network information
1044 To get detailed network information: /network info <network id>
1047 @errors.cyclades.nic_format
1048 def _server_id_from_nic(self, nic_id):
1049 return nic_id.split('-')[1]
1052 @errors.cyclades.connection
1053 @errors.cyclades.server_id
1054 @errors.cyclades.nic_id
1055 def _run(self, nic_id, server_id):
1056 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1057 if not num_of_disconnected:
1059 'Network Interface %s not found on server %s' % (
1062 print('Disconnected %s connections' % num_of_disconnected)
1064 def main(self, nic_id):
1065 super(self.__class__, self)._run()
1066 server_id = self._server_id_from_nic(nic_id=nic_id)
1067 self._run(nic_id=nic_id, server_id=server_id)
1070 @command(network_cmds)
1071 class network_wait(_init_cyclades, _network_wait):
1072 """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1075 timeout=IntArgument(
1076 'Wait limit in seconds (default: 60)', '--timeout', default=60)
1080 @errors.cyclades.connection
1081 @errors.cyclades.network_id
1082 def _run(self, network_id, current_status):
1083 net = self.client.get_network_details(network_id)
1084 if net['status'].lower() == current_status.lower():
1085 self._wait(network_id, current_status, timeout=self['timeout'])
1088 'Network %s: Cannot wait for status %s, '
1089 'status is already %s' % (
1090 network_id, current_status, net['status']))
1092 def main(self, network_id, current_status='PENDING'):
1093 super(self.__class__, self)._run()
1094 self._run(network_id=network_id, current_status=current_status)
1098 class ip_pools(_init_cyclades, _optional_json):
1099 """List pools of floating IPs"""
1102 @errors.cyclades.connection
1104 r = self.client.get_floating_ip_pools()
1105 self._print(r if self['json_output'] else r['floating_ip_pools'])
1108 super(self.__class__, self)._run()
1113 class ip_list(_init_cyclades, _optional_json):
1114 """List reserved floating IPs"""
1117 @errors.cyclades.connection
1119 r = self.client.get_floating_ips()
1120 self._print(r if self['json_output'] else r['floating_ips'])
1123 super(self.__class__, self)._run()
1128 class ip_info(_init_cyclades, _optional_json):
1129 """Details for an IP"""
1132 @errors.cyclades.connection
1134 self._print(self.client.get_floating_ip(ip), self.print_dict)
1137 super(self.__class__, self)._run()
1142 class ip_reserve(_init_cyclades, _optional_json):
1143 """Reserve a floating IP
1144 An IP is reserved from an IP pool. The default IP pool is chosen
1145 automatically, but there is the option if specifying an explicit IP pool.
1148 arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1151 @errors.cyclades.connection
1152 def _run(self, ip=None):
1153 self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1155 def main(self, requested_IP=None):
1156 super(self.__class__, self)._run()
1157 self._run(ip=requested_IP)
1161 class ip_release(_init_cyclades, _optional_output_cmd):
1162 """Release a floating IP
1163 The release IP is "returned" to the IP pool it came from.
1167 @errors.cyclades.connection
1169 self._optional_output(self.client.delete_floating_ip(ip))
1172 super(self.__class__, self)._run()
1177 class ip_attach(_init_cyclades, _optional_output_cmd):
1178 """Attach a floating IP to a server
1182 @errors.cyclades.connection
1183 @errors.cyclades.server_id
1184 def _run(self, server_id, ip):
1185 self._optional_output(self.client.attach_floating_ip(server_id, ip))
1187 def main(self, server_id, IP):
1188 super(self.__class__, self)._run()
1189 self._run(server_id=server_id, ip=IP)
1193 class ip_detach(_init_cyclades, _optional_output_cmd):
1194 """Detach a floating IP from a server
1198 @errors.cyclades.connection
1199 @errors.cyclades.server_id
1200 def _run(self, server_id, ip):
1201 self._optional_output(self.client.detach_floating_ip(server_id, ip))
1203 def main(self, server_id, IP):
1204 super(self.__class__, self)._run()
1205 self._run(server_id=server_id, ip=IP)