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 (
43 raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
44 from kamaki.clients.cyclades import CycladesClient, ClientError
45 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
46 from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
47 from kamaki.cli.commands import _command_init, errors, addLogSettings
48 from kamaki.cli.commands import (
49 _optional_output_cmd, _optional_json, _name_filter, _id_filter)
52 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
53 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
54 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
55 ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
56 _commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
59 about_authentication = '\nUser Authentication:\
60 \n* to check authentication: /user authenticate\
61 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
64 'Defines a file to be injected to virtual servers file system.',
65 'syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
66 ' [local-path=]PATH: local file to be injected (relative or absolute)',
67 ' [server-path=]SERVER_PATH: destination location inside server Image',
68 ' [owner=]OWNER: virtual servers user id for the remote file',
69 ' [group=]GROUP: virtual servers group id or name for the remote file',
70 ' [mode=]MODE: permission in octal (e.g., 0777 or o+rwx)',
71 'e.g., -p /tmp/my.file,owner=root,mode=0777']
74 class _service_wait(object):
76 wait_arguments = dict(
77 progress_bar=ProgressBarArgument(
78 'do not show progress bar', ('-N', '--no-progress-bar'), False)
82 self, service, service_id, status_method, current_status,
83 countdown=True, timeout=60):
84 (progress_bar, wait_cb) = self._safe_progress_bar(
85 '%s %s: status is still %s' % (
86 service, service_id, current_status),
87 countdown=countdown, timeout=timeout)
90 new_mode = status_method(
91 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
93 self.error('%s %s: status is now %s' % (
94 service, service_id, new_mode))
96 self.error('%s %s: status is still %s' % (
97 service, service_id, current_status))
98 except KeyboardInterrupt:
99 self.error('\n- canceled')
101 self._safe_progress_bar_finish(progress_bar)
104 class _server_wait(_service_wait):
106 def _wait(self, server_id, current_status, timeout=60):
107 super(_server_wait, self)._wait(
108 'Server', server_id, self.client.wait_server, current_status,
109 countdown=(current_status not in ('BUILD', )),
110 timeout=timeout if current_status not in ('BUILD', ) else 100)
113 class _network_wait(_service_wait):
115 def _wait(self, net_id, current_status, timeout=60):
116 super(_network_wait, self)._wait(
117 'Network', net_id, self.client.wait_network, current_status,
121 class _firewall_wait(_service_wait):
123 def _wait(self, server_id, current_status, timeout=60):
124 super(_firewall_wait, self)._wait(
125 'Firewall of server',
126 server_id, self.client.wait_firewall, current_status,
130 class _init_cyclades(_command_init):
133 def _run(self, service='compute'):
134 if getattr(self, 'cloud', None):
135 base_url = self._custom_url(service) or self._custom_url(
138 token = self._custom_token(service) or self._custom_token(
139 'cyclades') or self.config.get_cloud('token')
140 self.client = CycladesClient(base_url=base_url, token=token)
143 self.cloud = 'default'
144 if getattr(self, 'auth_base', False):
145 cyclades_endpoints = self.auth_base.get_service_endpoints(
146 self._custom_type('cyclades') or 'compute',
147 self._custom_version('cyclades') or '')
148 base_url = cyclades_endpoints['publicURL']
149 token = self.auth_base.token
150 self.client = CycladesClient(base_url=base_url, token=token)
152 raise CLIBaseUrlError(service='cyclades')
158 @command(server_cmds)
159 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
160 """List virtual servers accessible by user
161 Use filtering arguments (e.g., --name-like) to manage long server lists
164 PERMANENTS = ('id', 'name')
167 detail=FlagArgument('show detailed output', ('-l', '--details')),
169 'show only items since date (\' d/m/Y H:M:S \')',
172 'limit number of listed virtual servers', ('-n', '--number')),
174 'output results in pages (-n to set items per page, default 10)',
176 enum=FlagArgument('Enumerate results', '--enumerate'),
177 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
178 image_id=ValueArgument('filter by image id', ('--image-id')),
179 user_id=ValueArgument('filter by user id', ('--user-id')),
180 user_name=ValueArgument('filter by user name', ('--user-name')),
181 status=ValueArgument(
182 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
184 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
185 meta_like=KeyValueArgument(
186 'print only if in key=value, the value is part of actual value',
187 ('--metadata-like')),
190 def _add_user_name(self, servers):
191 uuids = self._uuids2usernames(list(set(
192 [srv['user_id'] for srv in servers] +
193 [srv['tenant_id'] for srv in servers])))
195 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
196 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
199 def _apply_common_filters(self, servers):
200 common_filters = dict()
202 common_filters['status'] = self['status']
203 if self['user_id'] or self['user_name']:
204 uuid = self['user_id'] or self._username2uuid(self['user_name'])
205 common_filters['user_id'] = uuid
206 return filter_dicts_by_dict(servers, common_filters)
208 def _filter_by_image(self, servers):
209 iid = self['image_id']
210 return [srv for srv in servers if srv['image']['id'] == iid]
212 def _filter_by_flavor(self, servers):
213 fid = self['flavor_id']
214 return [srv for srv in servers if (
215 '%s' % srv['image']['id'] == '%s' % fid)]
217 def _filter_by_metadata(self, servers):
220 if not 'metadata' in srv:
222 meta = [dict(srv['metadata'])]
224 meta = filter_dicts_by_dict(meta, self['meta'])
225 if meta and self['meta_like']:
226 meta = filter_dicts_by_dict(
227 meta, self['meta_like'], exact_match=False)
229 new_servers.append(srv)
233 @errors.cyclades.connection
234 @errors.cyclades.date
236 withimage = bool(self['image_id'])
237 withflavor = bool(self['flavor_id'])
238 withmeta = bool(self['meta'] or self['meta_like'])
240 self['status'] or self['user_id'] or self['user_name'])
241 detail = self['detail'] or (
242 withimage or withflavor or withmeta or withcommons)
243 servers = self.client.list_servers(detail, self['since'])
245 servers = self._filter_by_name(servers)
246 servers = self._filter_by_id(servers)
247 servers = self._apply_common_filters(servers)
249 servers = self._filter_by_image(servers)
251 servers = self._filter_by_flavor(servers)
253 servers = self._filter_by_metadata(servers)
255 if self['detail'] and not (
256 self['json_output'] or self['output_format']):
257 servers = self._add_user_name(servers)
258 elif not (self['detail'] or (
259 self['json_output'] or self['output_format'])):
260 remove_from_items(servers, 'links')
261 if detail and not self['detail']:
263 for key in set(srv).difference(self.PERMANENTS):
265 kwargs = dict(with_enumeration=self['enum'])
267 kwargs['out'] = StringIO()
270 servers = servers[:self['limit']]
271 self._print(servers, **kwargs)
273 pager(kwargs['out'].getvalue())
276 super(self.__class__, self)._run()
280 @command(server_cmds)
281 class server_info(_init_cyclades, _optional_json):
282 """Detailed information on a Virtual Machine
284 - name, id, status, create/update dates
286 - metadata (e.g., os, superuser) and diagnostics
287 - hardware flavor and os image ids
291 @errors.cyclades.connection
292 @errors.cyclades.server_id
293 def _run(self, server_id):
294 vm = self.client.get_server_details(server_id)
295 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
296 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
297 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
298 self._print(vm, self.print_dict)
300 def main(self, server_id):
301 super(self.__class__, self)._run()
302 self._run(server_id=server_id)
305 class PersonalityArgument(KeyValueArgument):
308 ('local-path', 'contents'),
309 ('server-path', 'path'),
316 return self._value if hasattr(self, '_value') else []
319 def value(self, newvalue):
320 if newvalue == self.default:
322 self._value, input_dict = [], {}
323 for i, terms in enumerate(newvalue):
324 termlist = terms.split(',')
325 if len(termlist) > len(self.terms):
326 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
327 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
329 for k, v in self.terms:
331 for item in termlist:
332 if item.lower().startswith(prefix):
333 input_dict[k] = item[len(k) + 1:]
337 termlist.remove(item)
340 path = input_dict['local-path']
342 path = termlist.pop(0)
344 raise CLIInvalidArgument(
345 '--personality: No local path specified',
346 details=howto_personality)
349 raise CLIInvalidArgument(
350 '--personality: File %s does not exist' % path,
351 details=howto_personality)
353 self._value.append(dict(path=path))
354 with open(path) as f:
355 self._value[i]['contents'] = b64encode(f.read())
356 for k, v in self.terms[1:]:
358 self._value[i][v] = input_dict[k]
361 self._value[i][v] = termlist.pop(0)
366 @command(server_cmds)
367 class server_create(_init_cyclades, _optional_json, _server_wait):
368 """Create a server (aka Virtual Machine)
370 - name: (single quoted text)
371 - flavor id: Hardware flavor. Pick one from: /flavor list
372 - image id: OS images. Pick one from: /image list
376 personality=PersonalityArgument(
377 (80 * ' ').join(howto_personality), ('-p', '--personality')),
378 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
379 cluster_size=IntArgument(
380 'Create a cluster of servers of this size. In this case, the name'
381 'parameter is the prefix of each server in the cluster (e.g.,'
386 @errors.cyclades.cluster_size
387 def _create_cluster(self, prefix, flavor_id, image_id, size):
389 name='%s%s' % (prefix, i),
392 personality=self['personality']) for i in range(size)]
394 return [self.client.create_server(**servers[0])]
395 return self.client.async_run(self.client.create_server, servers)
398 @errors.cyclades.connection
400 @errors.cyclades.flavor_id
401 def _run(self, name, flavor_id, image_id):
402 for r in self._create_cluster(
403 name, flavor_id, image_id, size=self['cluster_size'] or 1):
404 print 'HEY I GOT A', r
405 print 'MKEY?????????????????'
406 usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
407 r['user_id'] += ' (%s)' % usernames[r['user_id']]
408 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
409 self._print(r, self.print_dict)
411 self._wait(r['id'], r['status'])
414 def main(self, name, flavor_id, image_id):
415 super(self.__class__, self)._run()
416 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
419 @command(server_cmds)
420 class server_rename(_init_cyclades, _optional_output_cmd):
421 """Set/update a virtual server name
422 virtual server names are not unique, therefore multiple servers may share
427 @errors.cyclades.connection
428 @errors.cyclades.server_id
429 def _run(self, server_id, new_name):
430 self._optional_output(
431 self.client.update_server_name(int(server_id), new_name))
433 def main(self, server_id, new_name):
434 super(self.__class__, self)._run()
435 self._run(server_id=server_id, new_name=new_name)
438 @command(server_cmds)
439 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
440 """Delete a virtual server"""
443 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
444 cluster=FlagArgument(
445 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
446 'prefix. In that case, the prefix replaces the server id',
450 def _server_ids(self, server_var):
452 return [s['id'] for s in self.client.list_servers() if (
453 s['name'].startswith(server_var))]
455 @errors.cyclades.server_id
456 def _check_server_id(self, server_id):
459 return [_check_server_id(self, server_id=server_var), ]
462 @errors.cyclades.connection
463 def _run(self, server_var):
464 for server_id in self._server_ids(server_var):
466 details = self.client.get_server_details(server_id)
467 status = details['status']
469 r = self.client.delete_server(server_id)
470 self._optional_output(r)
473 self._wait(server_id, status)
475 def main(self, server_id_or_cluster_prefix):
476 super(self.__class__, self)._run()
477 self._run(server_id_or_cluster_prefix)
480 @command(server_cmds)
481 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
482 """Reboot a virtual server"""
486 'perform a hard reboot (deprecated)', ('-f', '--force')),
487 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
488 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
492 @errors.cyclades.connection
493 @errors.cyclades.server_id
494 def _run(self, server_id):
495 hard_reboot = self['hard']
498 'WARNING: -f/--force will be deprecated in version 0.12\n'
499 '\tIn the future, please use --type=hard instead')
501 if self['type'].lower() in ('soft', ):
503 elif self['type'].lower() in ('hard', ):
506 raise CLISyntaxError(
507 'Invalid reboot type %s' % self['type'],
508 importance=2, details=[
509 '--type values are either SOFT (default) or HARD'])
511 r = self.client.reboot_server(int(server_id), hard_reboot)
512 self._optional_output(r)
515 self._wait(server_id, 'REBOOT')
517 def main(self, server_id):
518 super(self.__class__, self)._run()
519 self._run(server_id=server_id)
522 @command(server_cmds)
523 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
524 """Start an existing virtual server"""
527 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
531 @errors.cyclades.connection
532 @errors.cyclades.server_id
533 def _run(self, server_id):
536 details = self.client.get_server_details(server_id)
537 status = details['status']
538 if status in ('ACTIVE', ):
541 r = self.client.start_server(int(server_id))
542 self._optional_output(r)
545 self._wait(server_id, status)
547 def main(self, server_id):
548 super(self.__class__, self)._run()
549 self._run(server_id=server_id)
552 @command(server_cmds)
553 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
554 """Shutdown an active virtual server"""
557 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
561 @errors.cyclades.connection
562 @errors.cyclades.server_id
563 def _run(self, server_id):
566 details = self.client.get_server_details(server_id)
567 status = details['status']
568 if status in ('STOPPED', ):
571 r = self.client.shutdown_server(int(server_id))
572 self._optional_output(r)
575 self._wait(server_id, status)
577 def main(self, server_id):
578 super(self.__class__, self)._run()
579 self._run(server_id=server_id)
582 @command(server_cmds)
583 class server_console(_init_cyclades, _optional_json):
584 """Get a VNC console to access an existing virtual server
585 Console connection information provided (at least):
586 - host: (url or address) a VNC host
587 - port: (int) the gateway to enter virtual server on host
588 - password: for VNC authorization
592 @errors.cyclades.connection
593 @errors.cyclades.server_id
594 def _run(self, server_id):
596 self.client.get_server_console(int(server_id)), self.print_dict)
598 def main(self, server_id):
599 super(self.__class__, self)._run()
600 self._run(server_id=server_id)
603 @command(server_cmds)
604 class server_resize(_init_cyclades, _optional_output_cmd):
605 """Set a different flavor for an existing server
606 To get server ids and flavor ids:
612 @errors.cyclades.connection
613 @errors.cyclades.server_id
614 @errors.cyclades.flavor_id
615 def _run(self, server_id, flavor_id):
616 self._optional_output(self.client.resize_server(server_id, flavor_id))
618 def main(self, server_id, flavor_id):
619 super(self.__class__, self)._run()
620 self._run(server_id=server_id, flavor_id=flavor_id)
623 @command(server_cmds)
624 class server_firewall(_init_cyclades):
625 """Manage virtual server firewall profiles for public networks"""
628 @command(server_cmds)
629 class server_firewall_set(
630 _init_cyclades, _optional_output_cmd, _firewall_wait):
631 """Set the firewall profile on virtual server public network
633 - DISABLED: Shutdown firewall
634 - ENABLED: Firewall in normal mode
635 - PROTECTED: Firewall in secure mode
639 wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
641 'Set wait timeout in seconds (default: 60)', '--timeout',
646 @errors.cyclades.connection
647 @errors.cyclades.server_id
648 @errors.cyclades.firewall
649 def _run(self, server_id, profile):
650 if self['timeout'] and not self['wait']:
651 raise CLIInvalidArgument('Invalid use of --timeout', details=[
652 'Timeout is used only along with -w/--wait'])
653 old_profile = self.client.get_firewall_profile(server_id)
654 if old_profile.lower() == profile.lower():
655 self.error('Firewall of server %s: allready in status %s' % (
656 server_id, old_profile))
658 self._optional_output(self.client.set_firewall_profile(
659 server_id=int(server_id), profile=('%s' % profile).upper()))
661 self._wait(server_id, old_profile, timeout=self['timeout'])
663 def main(self, server_id, profile):
664 super(self.__class__, self)._run()
665 self._run(server_id=server_id, profile=profile)
668 @command(server_cmds)
669 class server_firewall_get(_init_cyclades):
670 """Get the firewall profile for a virtual servers' public network"""
673 @errors.cyclades.connection
674 @errors.cyclades.server_id
675 def _run(self, server_id):
676 self.writeln(self.client.get_firewall_profile(server_id))
678 def main(self, server_id):
679 super(self.__class__, self)._run()
680 self._run(server_id=server_id)
683 @command(server_cmds)
684 class server_addr(_init_cyclades, _optional_json):
685 """List the addresses of all network interfaces on a virtual server"""
688 enum=FlagArgument('Enumerate results', '--enumerate')
692 @errors.cyclades.connection
693 @errors.cyclades.server_id
694 def _run(self, server_id):
695 reply = self.client.list_server_nics(int(server_id))
696 self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
698 def main(self, server_id):
699 super(self.__class__, self)._run()
700 self._run(server_id=server_id)
703 @command(server_cmds)
704 class server_metadata(_init_cyclades):
705 """Manage Server metadata (key:value pairs of server attributes)"""
708 @command(server_cmds)
709 class server_metadata_list(_init_cyclades, _optional_json):
710 """Get server metadata"""
713 @errors.cyclades.connection
714 @errors.cyclades.server_id
715 @errors.cyclades.metadata
716 def _run(self, server_id, key=''):
718 self.client.get_server_metadata(int(server_id), key),
721 def main(self, server_id, key=''):
722 super(self.__class__, self)._run()
723 self._run(server_id=server_id, key=key)
726 @command(server_cmds)
727 class server_metadata_set(_init_cyclades, _optional_json):
728 """Set / update virtual server metadata
729 Metadata should be given in key/value pairs in key=value format
730 For example: /server metadata set <server id> key1=value1 key2=value2
731 Old, unreferenced metadata will remain intact
735 @errors.cyclades.connection
736 @errors.cyclades.server_id
737 def _run(self, server_id, keyvals):
738 assert keyvals, 'Please, add some metadata ( key=value)'
740 for keyval in keyvals:
741 k, sep, v = keyval.partition('=')
746 'Invalid piece of metadata %s' % keyval,
747 importance=2, details=[
748 'Correct metadata format: key=val',
750 '/server metadata set <server id>'
751 'key1=value1 key2=value2'])
753 self.client.update_server_metadata(int(server_id), **metadata),
756 def main(self, server_id, *key_equals_val):
757 super(self.__class__, self)._run()
758 self._run(server_id=server_id, keyvals=key_equals_val)
761 @command(server_cmds)
762 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
763 """Delete virtual server metadata"""
766 @errors.cyclades.connection
767 @errors.cyclades.server_id
768 @errors.cyclades.metadata
769 def _run(self, server_id, key):
770 self._optional_output(
771 self.client.delete_server_metadata(int(server_id), key))
773 def main(self, server_id, key):
774 super(self.__class__, self)._run()
775 self._run(server_id=server_id, key=key)
778 @command(server_cmds)
779 class server_stats(_init_cyclades, _optional_json):
780 """Get virtual server statistics"""
783 @errors.cyclades.connection
784 @errors.cyclades.server_id
785 def _run(self, server_id):
787 self.client.get_server_stats(int(server_id)), self.print_dict)
789 def main(self, server_id):
790 super(self.__class__, self)._run()
791 self._run(server_id=server_id)
794 @command(server_cmds)
795 class server_wait(_init_cyclades, _server_wait):
796 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
800 'Wait limit in seconds (default: 60)', '--timeout', default=60)
804 @errors.cyclades.connection
805 @errors.cyclades.server_id
806 def _run(self, server_id, current_status):
807 r = self.client.get_server_details(server_id)
808 if r['status'].lower() == current_status.lower():
809 self._wait(server_id, current_status, timeout=self['timeout'])
812 'Server %s: Cannot wait for status %s, '
813 'status is already %s' % (
814 server_id, current_status, r['status']))
816 def main(self, server_id, current_status='BUILD'):
817 super(self.__class__, self)._run()
818 self._run(server_id=server_id, current_status=current_status)
821 @command(flavor_cmds)
822 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
823 """List available hardware flavors"""
825 PERMANENTS = ('id', 'name')
828 detail=FlagArgument('show detailed output', ('-l', '--details')),
829 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
831 'output results in pages (-n to set items per page, default 10)',
833 enum=FlagArgument('Enumerate results', '--enumerate'),
834 ram=ValueArgument('filter by ram', ('--ram')),
835 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
836 disk=ValueArgument('filter by disk size in GB', ('--disk')),
837 disk_template=ValueArgument(
838 'filter by disk_templace', ('--disk-template'))
841 def _apply_common_filters(self, flavors):
842 common_filters = dict()
844 common_filters['ram'] = self['ram']
846 common_filters['vcpus'] = self['vcpus']
848 common_filters['disk'] = self['disk']
849 if self['disk_template']:
850 common_filters['SNF:disk_template'] = self['disk_template']
851 return filter_dicts_by_dict(flavors, common_filters)
854 @errors.cyclades.connection
856 withcommons = self['ram'] or self['vcpus'] or (
857 self['disk'] or self['disk_template'])
858 detail = self['detail'] or withcommons
859 flavors = self.client.list_flavors(detail)
860 flavors = self._filter_by_name(flavors)
861 flavors = self._filter_by_id(flavors)
863 flavors = self._apply_common_filters(flavors)
864 if not (self['detail'] or (
865 self['json_output'] or self['output_format'])):
866 remove_from_items(flavors, 'links')
867 if detail and not self['detail']:
869 for key in set(flv).difference(self.PERMANENTS):
871 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
874 with_redundancy=self['detail'], with_enumeration=self['enum'],
877 pager(kwargs['out'].getvalue())
880 super(self.__class__, self)._run()
884 @command(flavor_cmds)
885 class flavor_info(_init_cyclades, _optional_json):
886 """Detailed information on a hardware flavor
887 To get a list of available flavors and flavor ids, try /flavor list
891 @errors.cyclades.connection
892 @errors.cyclades.flavor_id
893 def _run(self, flavor_id):
895 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
897 def main(self, flavor_id):
898 super(self.__class__, self)._run()
899 self._run(flavor_id=flavor_id)
902 def _add_name(self, net):
903 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
905 uuids.append(user_id)
907 uuids.append(tenant_id)
909 usernames = self._uuids2usernames(uuids)
911 net['user_id'] += ' (%s)' % usernames[user_id]
913 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
916 @command(network_cmds)
917 class network_info(_init_cyclades, _optional_json):
918 """Detailed information on a network
919 To get a list of available networks and network ids, try /network list
923 @errors.cyclades.connection
924 @errors.cyclades.network_id
925 def _run(self, network_id):
926 network = self.client.get_network_details(int(network_id))
927 _add_name(self, network)
928 self._print(network, self.print_dict, exclude=('id'))
930 def main(self, network_id):
931 super(self.__class__, self)._run()
932 self._run(network_id=network_id)
935 @command(network_cmds)
936 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
939 PERMANENTS = ('id', 'name')
942 detail=FlagArgument('show detailed output', ('-l', '--details')),
943 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
945 'output results in pages (-n to set items per page, default 10)',
947 enum=FlagArgument('Enumerate results', '--enumerate'),
948 status=ValueArgument('filter by status', ('--status')),
949 public=FlagArgument('only public networks', ('--public')),
950 private=FlagArgument('only private networks', ('--private')),
951 dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
952 no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
953 user_id=ValueArgument('filter by user id', ('--user-id')),
954 user_name=ValueArgument('filter by user name', ('--user-name')),
955 gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
956 gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
957 cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
958 cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
959 type=ValueArgument('filter by type', ('--type')),
962 def _apply_common_filters(self, networks):
963 common_filter = dict()
967 common_filter['public'] = self['public']
968 elif self['private']:
969 common_filter['public'] = False
973 common_filter['dhcp'] = True
974 elif self['no_dhcp']:
975 common_filter['dhcp'] = False
976 if self['user_id'] or self['user_name']:
977 uuid = self['user_id'] or self._username2uuid(self['user_name'])
978 common_filter['user_id'] = uuid
979 for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
981 common_filter[term] = self[term]
982 return filter_dicts_by_dict(networks, common_filter)
984 def _add_name(self, networks, key='user_id'):
985 uuids = self._uuids2usernames(
986 list(set([net[key] for net in networks])))
988 v = net.get(key, None)
990 net[key] += ' (%s)' % uuids[v]
994 @errors.cyclades.connection
998 'status', 'public', 'private', 'user_id', 'user_name', 'type',
999 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
1003 detail = self['detail'] or withcommons
1004 networks = self.client.list_networks(detail)
1005 networks = self._filter_by_name(networks)
1006 networks = self._filter_by_id(networks)
1008 networks = self._apply_common_filters(networks)
1009 if not (self['detail'] or (
1010 self['json_output'] or self['output_format'])):
1011 remove_from_items(networks, 'links')
1012 if detail and not self['detail']:
1013 for net in networks:
1014 for key in set(net).difference(self.PERMANENTS):
1016 if self['detail'] and not (
1017 self['json_output'] or self['output_format']):
1018 self._add_name(networks)
1019 self._add_name(networks, 'tenant_id')
1020 kwargs = dict(with_enumeration=self['enum'])
1022 kwargs['out'] = StringIO()
1023 kwargs['title'] = ()
1025 networks = networks[:self['limit']]
1026 self._print(networks, **kwargs)
1028 pager(kwargs['out'].getvalue())
1031 super(self.__class__, self)._run()
1035 @command(network_cmds)
1036 class network_create(_init_cyclades, _optional_json, _network_wait):
1037 """Create an (unconnected) network"""
1040 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1041 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1042 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1044 'Valid network types are '
1045 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1047 default='MAC_FILTERED'),
1048 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1052 @errors.cyclades.connection
1053 @errors.cyclades.network_max
1054 def _run(self, name):
1055 r = self.client.create_network(
1058 gateway=self['gateway'],
1062 self._print(r, self.print_dict)
1063 if self['wait'] and r['status'] in ('PENDING', ):
1064 self._wait(r['id'], 'PENDING')
1066 def main(self, name):
1067 super(self.__class__, self)._run()
1071 @command(network_cmds)
1072 class network_rename(_init_cyclades, _optional_output_cmd):
1073 """Set the name of a network"""
1076 @errors.cyclades.connection
1077 @errors.cyclades.network_id
1078 def _run(self, network_id, new_name):
1079 self._optional_output(
1080 self.client.update_network_name(int(network_id), new_name))
1082 def main(self, network_id, new_name):
1083 super(self.__class__, self)._run()
1084 self._run(network_id=network_id, new_name=new_name)
1087 @command(network_cmds)
1088 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1089 """Delete a network"""
1092 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1096 @errors.cyclades.connection
1097 @errors.cyclades.network_in_use
1098 @errors.cyclades.network_id
1099 def _run(self, network_id):
1102 r = self.client.get_network_details(network_id)
1103 status = r['status']
1104 if status in ('DELETED', ):
1107 r = self.client.delete_network(int(network_id))
1108 self._optional_output(r)
1111 self._wait(network_id, status)
1113 def main(self, network_id):
1114 super(self.__class__, self)._run()
1115 self._run(network_id=network_id)
1118 @command(network_cmds)
1119 class network_connect(_init_cyclades, _optional_output_cmd):
1120 """Connect a server to a network"""
1123 @errors.cyclades.connection
1124 @errors.cyclades.server_id
1125 @errors.cyclades.network_id
1126 def _run(self, server_id, network_id):
1127 self._optional_output(
1128 self.client.connect_server(int(server_id), int(network_id)))
1130 def main(self, server_id, network_id):
1131 super(self.__class__, self)._run()
1132 self._run(server_id=server_id, network_id=network_id)
1135 @command(network_cmds)
1136 class network_disconnect(_init_cyclades):
1137 """Disconnect a nic that connects a server to a network
1138 Nic ids are listed as "attachments" in detailed network information
1139 To get detailed network information: /network info <network id>
1142 @errors.cyclades.nic_format
1143 def _server_id_from_nic(self, nic_id):
1144 return nic_id.split('-')[1]
1147 @errors.cyclades.connection
1148 @errors.cyclades.server_id
1149 @errors.cyclades.nic_id
1150 def _run(self, nic_id, server_id):
1151 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1152 if not num_of_disconnected:
1154 'Network Interface %s not found on server %s' % (
1157 print('Disconnected %s connections' % num_of_disconnected)
1159 def main(self, nic_id):
1160 super(self.__class__, self)._run()
1161 server_id = self._server_id_from_nic(nic_id=nic_id)
1162 self._run(nic_id=nic_id, server_id=server_id)
1165 @command(network_cmds)
1166 class network_wait(_init_cyclades, _network_wait):
1167 """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1170 timeout=IntArgument(
1171 'Wait limit in seconds (default: 60)', '--timeout', default=60)
1175 @errors.cyclades.connection
1176 @errors.cyclades.network_id
1177 def _run(self, network_id, current_status):
1178 net = self.client.get_network_details(network_id)
1179 if net['status'].lower() == current_status.lower():
1180 self._wait(network_id, current_status, timeout=self['timeout'])
1183 'Network %s: Cannot wait for status %s, '
1184 'status is already %s' % (
1185 network_id, current_status, net['status']))
1187 def main(self, network_id, current_status='PENDING'):
1188 super(self.__class__, self)._run()
1189 self._run(network_id=network_id, current_status=current_status)
1193 class ip_pools(_init_cyclades, _optional_json):
1194 """List pools of floating IPs"""
1197 @errors.cyclades.connection
1199 r = self.client.get_floating_ip_pools()
1200 self._print(r if self['json_output'] or self['output_format'] else r[
1201 'floating_ip_pools'])
1204 super(self.__class__, self)._run()
1209 class ip_list(_init_cyclades, _optional_json):
1210 """List reserved floating IPs"""
1213 @errors.cyclades.connection
1215 r = self.client.get_floating_ips()
1216 self._print(r if self['json_output'] or self['output_format'] else r[
1220 super(self.__class__, self)._run()
1225 class ip_info(_init_cyclades, _optional_json):
1226 """Details for an IP"""
1229 @errors.cyclades.connection
1231 self._print(self.client.get_floating_ip(ip), self.print_dict)
1234 super(self.__class__, self)._run()
1239 class ip_reserve(_init_cyclades, _optional_json):
1240 """Reserve a floating IP
1241 An IP is reserved from an IP pool. The default IP pool is chosen
1242 automatically, but there is the option if specifying an explicit IP pool.
1245 arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1248 @errors.cyclades.connection
1249 def _run(self, ip=None):
1250 self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1252 def main(self, requested_IP=None):
1253 super(self.__class__, self)._run()
1254 self._run(ip=requested_IP)
1258 class ip_release(_init_cyclades, _optional_output_cmd):
1259 """Release a floating IP
1260 The release IP is "returned" to the IP pool it came from.
1264 @errors.cyclades.connection
1266 self._optional_output(self.client.delete_floating_ip(ip))
1269 super(self.__class__, self)._run()
1274 class ip_attach(_init_cyclades, _optional_output_cmd):
1275 """Attach a floating IP to a server
1279 @errors.cyclades.connection
1280 @errors.cyclades.server_id
1281 def _run(self, server_id, ip):
1282 self._optional_output(self.client.attach_floating_ip(server_id, ip))
1284 def main(self, server_id, IP):
1285 super(self.__class__, self)._run()
1286 self._run(server_id=server_id, ip=IP)
1290 class ip_detach(_init_cyclades, _optional_output_cmd):
1291 """Detach a floating IP from a server
1295 @errors.cyclades.connection
1296 @errors.cyclades.server_id
1297 def _run(self, server_id, ip):
1298 self._optional_output(self.client.detach_floating_ip(server_id, ip))
1300 def main(self, server_id, IP):
1301 super(self.__class__, self)._run()
1302 self._run(server_id=server_id, ip=IP)