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, expanduser
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 (
46 FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
47 ProgressBarArgument, DateArgument, IntArgument)
48 from kamaki.cli.commands import _command_init, errors, addLogSettings
49 from kamaki.cli.commands import (
50 _optional_output_cmd, _optional_json, _name_filter, _id_filter)
53 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
54 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
55 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
56 ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
57 _commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
60 about_authentication = '\nUser Authentication:\
61 \n* to check authentication: /user authenticate\
62 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
65 'Defines a file to be injected to virtual servers file system.',
66 'syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
67 ' [local-path=]PATH: local file to be injected (relative or absolute)',
68 ' [server-path=]SERVER_PATH: destination location inside server Image',
69 ' [owner=]OWNER: virtual servers user id for the remote file',
70 ' [group=]GROUP: virtual servers group id or name for the remote file',
71 ' [mode=]MODE: permission in octal (e.g., 0777)',
72 'e.g., -p /tmp/my.file,owner=root,mode=0777']
75 class _service_wait(object):
77 wait_arguments = dict(
78 progress_bar=ProgressBarArgument(
79 'do not show progress bar', ('-N', '--no-progress-bar'), False)
83 self, service, service_id, status_method, current_status,
84 countdown=True, timeout=60):
85 (progress_bar, wait_cb) = self._safe_progress_bar(
86 '%s %s: status is still %s' % (
87 service, service_id, current_status),
88 countdown=countdown, timeout=timeout)
91 new_mode = status_method(
92 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
94 self.error('%s %s: status is now %s' % (
95 service, service_id, new_mode))
97 self.error('%s %s: status is still %s' % (
98 service, service_id, current_status))
99 except KeyboardInterrupt:
100 self.error('\n- canceled')
102 self._safe_progress_bar_finish(progress_bar)
105 class _server_wait(_service_wait):
107 def _wait(self, server_id, current_status, timeout=60):
108 super(_server_wait, self)._wait(
109 'Server', server_id, self.client.wait_server, current_status,
110 countdown=(current_status not in ('BUILD', )),
111 timeout=timeout if current_status not in ('BUILD', ) else 100)
114 class _network_wait(_service_wait):
116 def _wait(self, net_id, current_status, timeout=60):
117 super(_network_wait, self)._wait(
118 'Network', net_id, self.client.wait_network, current_status,
122 class _firewall_wait(_service_wait):
124 def _wait(self, server_id, current_status, timeout=60):
125 super(_firewall_wait, self)._wait(
126 'Firewall of server',
127 server_id, self.client.wait_firewall, current_status,
131 class _init_cyclades(_command_init):
134 def _run(self, service='compute'):
135 if getattr(self, 'cloud', None):
136 base_url = self._custom_url(service) or self._custom_url(
139 token = self._custom_token(service) or self._custom_token(
140 'cyclades') or self.config.get_cloud('token')
141 self.client = CycladesClient(base_url=base_url, token=token)
144 self.cloud = 'default'
145 if getattr(self, 'auth_base', False):
146 cyclades_endpoints = self.auth_base.get_service_endpoints(
147 self._custom_type('cyclades') or 'compute',
148 self._custom_version('cyclades') or '')
149 base_url = cyclades_endpoints['publicURL']
150 token = self.auth_base.token
151 self.client = CycladesClient(base_url=base_url, token=token)
153 raise CLIBaseUrlError(service='cyclades')
159 @command(server_cmds)
160 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
161 """List virtual servers accessible by user
162 Use filtering arguments (e.g., --name-like) to manage long server lists
165 PERMANENTS = ('id', 'name')
168 detail=FlagArgument('show detailed output', ('-l', '--details')),
170 'show only items since date (\' d/m/Y H:M:S \')',
173 'limit number of listed virtual servers', ('-n', '--number')),
175 'output results in pages (-n to set items per page, default 10)',
177 enum=FlagArgument('Enumerate results', '--enumerate'),
178 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
179 image_id=ValueArgument('filter by image id', ('--image-id')),
180 user_id=ValueArgument('filter by user id', ('--user-id')),
181 user_name=ValueArgument('filter by user name', ('--user-name')),
182 status=ValueArgument(
183 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
185 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
186 meta_like=KeyValueArgument(
187 'print only if in key=value, the value is part of actual value',
188 ('--metadata-like')),
191 def _add_user_name(self, servers):
192 uuids = self._uuids2usernames(list(set(
193 [srv['user_id'] for srv in servers] +
194 [srv['tenant_id'] for srv in servers])))
196 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
197 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
200 def _apply_common_filters(self, servers):
201 common_filters = dict()
203 common_filters['status'] = self['status']
204 if self['user_id'] or self['user_name']:
205 uuid = self['user_id'] or self._username2uuid(self['user_name'])
206 common_filters['user_id'] = uuid
207 return filter_dicts_by_dict(servers, common_filters)
209 def _filter_by_image(self, servers):
210 iid = self['image_id']
211 return [srv for srv in servers if srv['image']['id'] == iid]
213 def _filter_by_flavor(self, servers):
214 fid = self['flavor_id']
215 return [srv for srv in servers if (
216 '%s' % srv['image']['id'] == '%s' % fid)]
218 def _filter_by_metadata(self, servers):
221 if not 'metadata' in srv:
223 meta = [dict(srv['metadata'])]
225 meta = filter_dicts_by_dict(meta, self['meta'])
226 if meta and self['meta_like']:
227 meta = filter_dicts_by_dict(
228 meta, self['meta_like'], exact_match=False)
230 new_servers.append(srv)
234 @errors.cyclades.connection
235 @errors.cyclades.date
237 withimage = bool(self['image_id'])
238 withflavor = bool(self['flavor_id'])
239 withmeta = bool(self['meta'] or self['meta_like'])
241 self['status'] or self['user_id'] or self['user_name'])
242 detail = self['detail'] or (
243 withimage or withflavor or withmeta or withcommons)
244 servers = self.client.list_servers(detail, self['since'])
246 servers = self._filter_by_name(servers)
247 servers = self._filter_by_id(servers)
248 servers = self._apply_common_filters(servers)
250 servers = self._filter_by_image(servers)
252 servers = self._filter_by_flavor(servers)
254 servers = self._filter_by_metadata(servers)
256 if self['detail'] and not (
257 self['json_output'] or self['output_format']):
258 servers = self._add_user_name(servers)
259 elif not (self['detail'] or (
260 self['json_output'] or self['output_format'])):
261 remove_from_items(servers, 'links')
262 if detail and not self['detail']:
264 for key in set(srv).difference(self.PERMANENTS):
266 kwargs = dict(with_enumeration=self['enum'])
268 kwargs['out'] = StringIO()
271 servers = servers[:self['limit']]
272 self._print(servers, **kwargs)
274 pager(kwargs['out'].getvalue())
277 super(self.__class__, self)._run()
281 @command(server_cmds)
282 class server_info(_init_cyclades, _optional_json):
283 """Detailed information on a Virtual Machine
285 - name, id, status, create/update dates
287 - metadata (e.g., os, superuser) and diagnostics
288 - hardware flavor and os image ids
292 @errors.cyclades.connection
293 @errors.cyclades.server_id
294 def _run(self, server_id):
295 vm = self.client.get_server_details(server_id)
296 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
297 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
298 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
299 self._print(vm, self.print_dict)
301 def main(self, server_id):
302 super(self.__class__, self)._run()
303 self._run(server_id=server_id)
306 class PersonalityArgument(KeyValueArgument):
309 ('local-path', 'contents'),
310 ('server-path', 'path'),
317 return getattr(self, '_value', [])
320 def value(self, newvalue):
321 if newvalue == self.default:
323 self._value, input_dict = [], {}
324 for i, terms in enumerate(newvalue):
325 termlist = terms.split(',')
326 if len(termlist) > len(self.terms):
327 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
328 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
330 for k, v in self.terms:
332 for item in termlist:
333 if item.lower().startswith(prefix):
334 input_dict[k] = item[len(k) + 1:]
338 termlist.remove(item)
341 path = input_dict['local-path']
343 path = termlist.pop(0)
345 raise CLIInvalidArgument(
346 '--personality: No local path specified',
347 details=howto_personality)
350 raise CLIInvalidArgument(
351 '--personality: File %s does not exist' % path,
352 details=howto_personality)
354 self._value.append(dict(path=path))
355 with open(expanduser(path)) as f:
356 self._value[i]['contents'] = b64encode(f.read())
357 for k, v in self.terms[1:]:
359 self._value[i][v] = input_dict[k]
362 self._value[i][v] = termlist.pop(0)
365 if k in ('mode', ) and self._value[i][v]:
367 self._value[i][v] = int(self._value[i][v], 8)
368 except ValueError as ve:
369 raise CLIInvalidArgument(
370 'Personality mode must be in octal', details=[
374 @command(server_cmds)
375 class server_create(_init_cyclades, _optional_json, _server_wait):
376 """Create a server (aka Virtual Machine)"""
379 server_name=ValueArgument('The name of the new server', '--name'),
380 flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
381 image_id=IntArgument('The ID of the hardware image', '--image-id'),
382 personality=PersonalityArgument(
383 (80 * ' ').join(howto_personality), ('-p', '--personality')),
384 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
385 cluster_size=IntArgument(
386 'Create a cluster of servers of this size. In this case, the name'
387 'parameter is the prefix of each server in the cluster (e.g.,'
391 required = ('server_name', 'flavor_id', 'image_id')
393 @errors.cyclades.cluster_size
394 def _create_cluster(self, prefix, flavor_id, image_id, size):
396 name='%s%s' % (prefix, i if size > 1 else ''),
399 personality=self['personality']) for i in range(1, 1 + size)]
401 return [self.client.create_server(**servers[0])]
403 r = self.client.async_run(self.client.create_server, servers)
405 except Exception as e:
409 requested_names = [s['name'] for s in servers]
410 spawned_servers = [dict(
412 id=s['id']) for s in self.client.list_servers() if (
413 s['name'] in requested_names)]
414 self.error('Failed to build %s servers' % size)
415 self.error('Found %s matching servers:' % len(spawned_servers))
416 self._print(spawned_servers, out=self._err)
417 self.error('Check if any of these servers should be removed\n')
418 except Exception as ne:
419 self.error('Error (%s) while notifying about errors' % ne)
424 @errors.cyclades.connection
426 @errors.cyclades.flavor_id
427 def _run(self, name, flavor_id, image_id):
428 for r in self._create_cluster(
429 name, flavor_id, image_id, size=self['cluster_size'] or 1):
431 self.error('Create %s: server response was %s' % (name, r))
433 usernames = self._uuids2usernames(
434 [r['user_id'], r['tenant_id']])
435 r['user_id'] += ' (%s)' % usernames[r['user_id']]
436 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
437 self._print(r, self.print_dict)
439 self._wait(r['id'], r['status'])
443 super(self.__class__, self)._run()
445 name=self['server_name'],
446 flavor_id=self['flavor_id'],
447 image_id=self['image_id'])
450 class FirewallProfileArgument(ValueArgument):
452 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
456 return getattr(self, '_value', None)
459 def value(self, new_profile):
461 new_profile = new_profile.upper()
462 if new_profile in self.profiles:
463 self._value = new_profile
465 raise CLIInvalidArgument(
466 'Invalid firewall profile %s' % new_profile,
467 details=['Valid values: %s' % ', '.join(self.profiles)])
470 @command(server_cmds)
471 class server_modify(_init_cyclades, _optional_output_cmd):
472 """Modify attributes of a virtual server"""
475 server_name=ValueArgument('The new name', '--name'),
476 flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
477 firewall_profile=FirewallProfileArgument(
478 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
480 metadata_to_set=KeyValueArgument(
481 'Set metadata in key=value form (can be repeated)',
483 metadata_to_delete=RepeatableArgument(
484 'Delete metadata by key (can be repeated)', '--del-metadata')
487 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
491 @errors.cyclades.connection
492 @errors.cyclades.server_id
493 def _run(self, server_id):
494 if self['server_name']:
495 self.client.update_server_name((server_id), self['server_name'])
496 if self['flavor_id']:
497 self.client.resize_server(server_id, self['flavor_id'])
498 if self['firewall_profile']:
499 self.client.set_firewall_profile(
500 server_id=server_id, profile=self['firewall_profile'])
501 if self['metadata_to_set']:
502 self.client.update_server_metadata(
503 server_id, **self['metadata_to_set'])
504 for key in self['metadata_to_delete']:
505 errors.cyclades.metadata(
506 self.client.delete_server_metadata)(server_id, key=key)
507 if self['with_output']:
508 self._optional_output(self.client.get_server_details(server_id))
510 def main(self, server_id):
511 super(self.__class__, self)._run()
512 self._run(server_id=server_id)
515 @command(server_cmds)
516 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
517 """Delete a virtual server"""
520 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
521 cluster=FlagArgument(
522 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
523 'prefix. In that case, the prefix replaces the server id',
527 def _server_ids(self, server_var):
529 return [s['id'] for s in self.client.list_servers() if (
530 s['name'].startswith(server_var))]
532 @errors.cyclades.server_id
533 def _check_server_id(self, server_id):
536 return [_check_server_id(self, server_id=server_var), ]
539 @errors.cyclades.connection
540 def _run(self, server_var):
541 for server_id in self._server_ids(server_var):
543 details = self.client.get_server_details(server_id)
544 status = details['status']
546 r = self.client.delete_server(server_id)
547 self._optional_output(r)
550 self._wait(server_id, status)
552 def main(self, server_id_or_cluster_prefix):
553 super(self.__class__, self)._run()
554 self._run(server_id_or_cluster_prefix)
557 @command(server_cmds)
558 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
559 """Reboot a virtual server"""
563 'perform a hard reboot (deprecated)', ('-f', '--force')),
564 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
565 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
569 @errors.cyclades.connection
570 @errors.cyclades.server_id
571 def _run(self, server_id):
572 hard_reboot = self['hard']
575 'WARNING: -f/--force will be deprecated in version 0.12\n'
576 '\tIn the future, please use --type=hard instead')
578 if self['type'].lower() in ('soft', ):
580 elif self['type'].lower() in ('hard', ):
583 raise CLISyntaxError(
584 'Invalid reboot type %s' % self['type'],
585 importance=2, details=[
586 '--type values are either SOFT (default) or HARD'])
588 r = self.client.reboot_server(int(server_id), hard_reboot)
589 self._optional_output(r)
592 self._wait(server_id, 'REBOOT')
594 def main(self, server_id):
595 super(self.__class__, self)._run()
596 self._run(server_id=server_id)
599 @command(server_cmds)
600 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
601 """Start an existing virtual server"""
604 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
608 @errors.cyclades.connection
609 @errors.cyclades.server_id
610 def _run(self, server_id):
613 details = self.client.get_server_details(server_id)
614 status = details['status']
615 if status in ('ACTIVE', ):
618 r = self.client.start_server(int(server_id))
619 self._optional_output(r)
622 self._wait(server_id, status)
624 def main(self, server_id):
625 super(self.__class__, self)._run()
626 self._run(server_id=server_id)
629 @command(server_cmds)
630 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
631 """Shutdown an active virtual server"""
634 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
638 @errors.cyclades.connection
639 @errors.cyclades.server_id
640 def _run(self, server_id):
643 details = self.client.get_server_details(server_id)
644 status = details['status']
645 if status in ('STOPPED', ):
648 r = self.client.shutdown_server(int(server_id))
649 self._optional_output(r)
652 self._wait(server_id, status)
654 def main(self, server_id):
655 super(self.__class__, self)._run()
656 self._run(server_id=server_id)
659 @command(server_cmds)
660 class server_console(_init_cyclades, _optional_json):
661 """Get a VNC console to access an existing virtual server
662 Console connection information provided (at least):
663 - host: (url or address) a VNC host
664 - port: (int) the gateway to enter virtual server on host
665 - password: for VNC authorization
669 @errors.cyclades.connection
670 @errors.cyclades.server_id
671 def _run(self, server_id):
673 self.client.get_server_console(int(server_id)), self.print_dict)
675 def main(self, server_id):
676 super(self.__class__, self)._run()
677 self._run(server_id=server_id)
680 @command(server_cmds)
681 class server_addr(_init_cyclades, _optional_json):
682 """List the addresses of all network interfaces on a virtual server"""
685 enum=FlagArgument('Enumerate results', '--enumerate')
689 @errors.cyclades.connection
690 @errors.cyclades.server_id
691 def _run(self, server_id):
692 reply = self.client.list_server_nics(int(server_id))
693 self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
695 def main(self, server_id):
696 super(self.__class__, self)._run()
697 self._run(server_id=server_id)
700 @command(server_cmds)
701 class server_metadata_list(_init_cyclades, _optional_json):
702 """Get server metadata"""
705 @errors.cyclades.connection
706 @errors.cyclades.server_id
707 @errors.cyclades.metadata
708 def _run(self, server_id, key=''):
710 self.client.get_server_metadata(int(server_id), key),
713 def main(self, server_id, key=''):
714 super(self.__class__, self)._run()
715 self._run(server_id=server_id, key=key)
718 @command(server_cmds)
719 class server_metadata_set(_init_cyclades, _optional_json):
720 """Set / update virtual server metadata
721 Metadata should be given in key/value pairs in key=value format
722 For example: /server metadata set <server id> key1=value1 key2=value2
723 Old, unreferenced metadata will remain intact
727 @errors.cyclades.connection
728 @errors.cyclades.server_id
729 def _run(self, server_id, keyvals):
730 assert keyvals, 'Please, add some metadata ( key=value)'
732 for keyval in keyvals:
733 k, sep, v = keyval.partition('=')
738 'Invalid piece of metadata %s' % keyval,
739 importance=2, details=[
740 'Correct metadata format: key=val',
742 '/server metadata set <server id>'
743 'key1=value1 key2=value2'])
745 self.client.update_server_metadata(int(server_id), **metadata),
748 def main(self, server_id, *key_equals_val):
749 super(self.__class__, self)._run()
750 self._run(server_id=server_id, keyvals=key_equals_val)
753 @command(server_cmds)
754 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
755 """Delete virtual server metadata"""
758 @errors.cyclades.connection
759 @errors.cyclades.server_id
760 @errors.cyclades.metadata
761 def _run(self, server_id, key):
762 self._optional_output(
763 self.client.delete_server_metadata(int(server_id), key))
765 def main(self, server_id, key):
766 super(self.__class__, self)._run()
767 self._run(server_id=server_id, key=key)
770 @command(server_cmds)
771 class server_stats(_init_cyclades, _optional_json):
772 """Get virtual server statistics"""
775 @errors.cyclades.connection
776 @errors.cyclades.server_id
777 def _run(self, server_id):
779 self.client.get_server_stats(int(server_id)), self.print_dict)
781 def main(self, server_id):
782 super(self.__class__, self)._run()
783 self._run(server_id=server_id)
786 @command(server_cmds)
787 class server_wait(_init_cyclades, _server_wait):
788 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
792 'Wait limit in seconds (default: 60)', '--timeout', default=60)
796 @errors.cyclades.connection
797 @errors.cyclades.server_id
798 def _run(self, server_id, current_status):
799 r = self.client.get_server_details(server_id)
800 if r['status'].lower() == current_status.lower():
801 self._wait(server_id, current_status, timeout=self['timeout'])
804 'Server %s: Cannot wait for status %s, '
805 'status is already %s' % (
806 server_id, current_status, r['status']))
808 def main(self, server_id, current_status='BUILD'):
809 super(self.__class__, self)._run()
810 self._run(server_id=server_id, current_status=current_status)
813 @command(flavor_cmds)
814 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
815 """List available hardware flavors"""
817 PERMANENTS = ('id', 'name')
820 detail=FlagArgument('show detailed output', ('-l', '--details')),
821 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
823 'output results in pages (-n to set items per page, default 10)',
825 enum=FlagArgument('Enumerate results', '--enumerate'),
826 ram=ValueArgument('filter by ram', ('--ram')),
827 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
828 disk=ValueArgument('filter by disk size in GB', ('--disk')),
829 disk_template=ValueArgument(
830 'filter by disk_templace', ('--disk-template'))
833 def _apply_common_filters(self, flavors):
834 common_filters = dict()
836 common_filters['ram'] = self['ram']
838 common_filters['vcpus'] = self['vcpus']
840 common_filters['disk'] = self['disk']
841 if self['disk_template']:
842 common_filters['SNF:disk_template'] = self['disk_template']
843 return filter_dicts_by_dict(flavors, common_filters)
846 @errors.cyclades.connection
848 withcommons = self['ram'] or self['vcpus'] or (
849 self['disk'] or self['disk_template'])
850 detail = self['detail'] or withcommons
851 flavors = self.client.list_flavors(detail)
852 flavors = self._filter_by_name(flavors)
853 flavors = self._filter_by_id(flavors)
855 flavors = self._apply_common_filters(flavors)
856 if not (self['detail'] or (
857 self['json_output'] or self['output_format'])):
858 remove_from_items(flavors, 'links')
859 if detail and not self['detail']:
861 for key in set(flv).difference(self.PERMANENTS):
863 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
866 with_redundancy=self['detail'], with_enumeration=self['enum'],
869 pager(kwargs['out'].getvalue())
872 super(self.__class__, self)._run()
876 @command(flavor_cmds)
877 class flavor_info(_init_cyclades, _optional_json):
878 """Detailed information on a hardware flavor
879 To get a list of available flavors and flavor ids, try /flavor list
883 @errors.cyclades.connection
884 @errors.cyclades.flavor_id
885 def _run(self, flavor_id):
887 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
889 def main(self, flavor_id):
890 super(self.__class__, self)._run()
891 self._run(flavor_id=flavor_id)
894 def _add_name(self, net):
895 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
897 uuids.append(user_id)
899 uuids.append(tenant_id)
901 usernames = self._uuids2usernames(uuids)
903 net['user_id'] += ' (%s)' % usernames[user_id]
905 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
908 @command(network_cmds)
909 class network_info(_init_cyclades, _optional_json):
910 """Detailed information on a network
911 To get a list of available networks and network ids, try /network list
915 @errors.cyclades.connection
916 @errors.cyclades.network_id
917 def _run(self, network_id):
918 network = self.client.get_network_details(int(network_id))
919 _add_name(self, network)
920 self._print(network, self.print_dict, exclude=('id'))
922 def main(self, network_id):
923 super(self.__class__, self)._run()
924 self._run(network_id=network_id)
927 @command(network_cmds)
928 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
931 PERMANENTS = ('id', 'name')
934 detail=FlagArgument('show detailed output', ('-l', '--details')),
935 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
937 'output results in pages (-n to set items per page, default 10)',
939 enum=FlagArgument('Enumerate results', '--enumerate'),
940 status=ValueArgument('filter by status', ('--status')),
941 public=FlagArgument('only public networks', ('--public')),
942 private=FlagArgument('only private networks', ('--private')),
943 dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
944 no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
945 user_id=ValueArgument('filter by user id', ('--user-id')),
946 user_name=ValueArgument('filter by user name', ('--user-name')),
947 gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
948 gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
949 cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
950 cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
951 type=ValueArgument('filter by type', ('--type')),
954 def _apply_common_filters(self, networks):
955 common_filter = dict()
959 common_filter['public'] = self['public']
960 elif self['private']:
961 common_filter['public'] = False
965 common_filter['dhcp'] = True
966 elif self['no_dhcp']:
967 common_filter['dhcp'] = False
968 if self['user_id'] or self['user_name']:
969 uuid = self['user_id'] or self._username2uuid(self['user_name'])
970 common_filter['user_id'] = uuid
971 for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
973 common_filter[term] = self[term]
974 return filter_dicts_by_dict(networks, common_filter)
976 def _add_name(self, networks, key='user_id'):
977 uuids = self._uuids2usernames(
978 list(set([net[key] for net in networks])))
980 v = net.get(key, None)
982 net[key] += ' (%s)' % uuids[v]
986 @errors.cyclades.connection
990 'status', 'public', 'private', 'user_id', 'user_name', 'type',
991 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
995 detail = self['detail'] or withcommons
996 networks = self.client.list_networks(detail)
997 networks = self._filter_by_name(networks)
998 networks = self._filter_by_id(networks)
1000 networks = self._apply_common_filters(networks)
1001 if not (self['detail'] or (
1002 self['json_output'] or self['output_format'])):
1003 remove_from_items(networks, 'links')
1004 if detail and not self['detail']:
1005 for net in networks:
1006 for key in set(net).difference(self.PERMANENTS):
1008 if self['detail'] and not (
1009 self['json_output'] or self['output_format']):
1010 self._add_name(networks)
1011 self._add_name(networks, 'tenant_id')
1012 kwargs = dict(with_enumeration=self['enum'])
1014 kwargs['out'] = StringIO()
1015 kwargs['title'] = ()
1017 networks = networks[:self['limit']]
1018 self._print(networks, **kwargs)
1020 pager(kwargs['out'].getvalue())
1023 super(self.__class__, self)._run()
1027 @command(network_cmds)
1028 class network_create(_init_cyclades, _optional_json, _network_wait):
1029 """Create an (unconnected) network"""
1032 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1033 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1034 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1036 'Valid network types are '
1037 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1039 default='MAC_FILTERED'),
1040 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1044 @errors.cyclades.connection
1045 @errors.cyclades.network_max
1046 def _run(self, name):
1047 r = self.client.create_network(
1050 gateway=self['gateway'],
1054 self._print(r, self.print_dict)
1055 if self['wait'] and r['status'] in ('PENDING', ):
1056 self._wait(r['id'], 'PENDING')
1058 def main(self, name):
1059 super(self.__class__, self)._run()
1063 @command(network_cmds)
1064 class network_rename(_init_cyclades, _optional_output_cmd):
1065 """Set the name of a network"""
1068 @errors.cyclades.connection
1069 @errors.cyclades.network_id
1070 def _run(self, network_id, new_name):
1071 self._optional_output(
1072 self.client.update_network_name(int(network_id), new_name))
1074 def main(self, network_id, new_name):
1075 super(self.__class__, self)._run()
1076 self._run(network_id=network_id, new_name=new_name)
1079 @command(network_cmds)
1080 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1081 """Delete a network"""
1084 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1088 @errors.cyclades.connection
1089 @errors.cyclades.network_in_use
1090 @errors.cyclades.network_id
1091 def _run(self, network_id):
1094 r = self.client.get_network_details(network_id)
1095 status = r['status']
1096 if status in ('DELETED', ):
1099 r = self.client.delete_network(int(network_id))
1100 self._optional_output(r)
1103 self._wait(network_id, status)
1105 def main(self, network_id):
1106 super(self.__class__, self)._run()
1107 self._run(network_id=network_id)
1110 @command(network_cmds)
1111 class network_connect(_init_cyclades, _optional_output_cmd):
1112 """Connect a server to a network"""
1115 @errors.cyclades.connection
1116 @errors.cyclades.server_id
1117 @errors.cyclades.network_id
1118 def _run(self, server_id, network_id):
1119 self._optional_output(
1120 self.client.connect_server(int(server_id), int(network_id)))
1122 def main(self, server_id, network_id):
1123 super(self.__class__, self)._run()
1124 self._run(server_id=server_id, network_id=network_id)
1127 @command(network_cmds)
1128 class network_disconnect(_init_cyclades):
1129 """Disconnect a nic that connects a server to a network
1130 Nic ids are listed as "attachments" in detailed network information
1131 To get detailed network information: /network info <network id>
1134 @errors.cyclades.nic_format
1135 def _server_id_from_nic(self, nic_id):
1136 return nic_id.split('-')[1]
1139 @errors.cyclades.connection
1140 @errors.cyclades.server_id
1141 @errors.cyclades.nic_id
1142 def _run(self, nic_id, server_id):
1143 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1144 if not num_of_disconnected:
1146 'Network Interface %s not found on server %s' % (
1149 print('Disconnected %s connections' % num_of_disconnected)
1151 def main(self, nic_id):
1152 super(self.__class__, self)._run()
1153 server_id = self._server_id_from_nic(nic_id=nic_id)
1154 self._run(nic_id=nic_id, server_id=server_id)
1157 @command(network_cmds)
1158 class network_wait(_init_cyclades, _network_wait):
1159 """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1162 timeout=IntArgument(
1163 'Wait limit in seconds (default: 60)', '--timeout', default=60)
1167 @errors.cyclades.connection
1168 @errors.cyclades.network_id
1169 def _run(self, network_id, current_status):
1170 net = self.client.get_network_details(network_id)
1171 if net['status'].lower() == current_status.lower():
1172 self._wait(network_id, current_status, timeout=self['timeout'])
1175 'Network %s: Cannot wait for status %s, '
1176 'status is already %s' % (
1177 network_id, current_status, net['status']))
1179 def main(self, network_id, current_status='PENDING'):
1180 super(self.__class__, self)._run()
1181 self._run(network_id=network_id, current_status=current_status)
1185 class ip_pools(_init_cyclades, _optional_json):
1186 """List pools of floating IPs"""
1189 @errors.cyclades.connection
1191 r = self.client.get_floating_ip_pools()
1192 self._print(r if self['json_output'] or self['output_format'] else r[
1193 'floating_ip_pools'])
1196 super(self.__class__, self)._run()
1201 class ip_list(_init_cyclades, _optional_json):
1202 """List reserved floating IPs"""
1205 @errors.cyclades.connection
1207 r = self.client.get_floating_ips()
1208 self._print(r if self['json_output'] or self['output_format'] else r[
1212 super(self.__class__, self)._run()
1217 class ip_info(_init_cyclades, _optional_json):
1218 """Details for an IP"""
1221 @errors.cyclades.connection
1223 self._print(self.client.get_floating_ip(ip), self.print_dict)
1226 super(self.__class__, self)._run()
1231 class ip_reserve(_init_cyclades, _optional_json):
1232 """Reserve a floating IP
1233 An IP is reserved from an IP pool. The default IP pool is chosen
1234 automatically, but there is the option if specifying an explicit IP pool.
1237 arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1240 @errors.cyclades.connection
1241 def _run(self, ip=None):
1242 self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1244 def main(self, requested_IP=None):
1245 super(self.__class__, self)._run()
1246 self._run(ip=requested_IP)
1250 class ip_release(_init_cyclades, _optional_output_cmd):
1251 """Release a floating IP
1252 The release IP is "returned" to the IP pool it came from.
1256 @errors.cyclades.connection
1258 self._optional_output(self.client.delete_floating_ip(ip))
1261 super(self.__class__, self)._run()
1266 class ip_attach(_init_cyclades, _optional_output_cmd):
1267 """Attach a floating IP to a server
1271 @errors.cyclades.connection
1272 @errors.cyclades.server_id
1273 def _run(self, server_id, ip):
1274 self._optional_output(self.client.attach_floating_ip(server_id, ip))
1276 def main(self, server_id, IP):
1277 super(self.__class__, self)._run()
1278 self._run(server_id=server_id, ip=IP)
1282 class ip_detach(_init_cyclades, _optional_output_cmd):
1283 """Detach a floating IP from a server
1287 @errors.cyclades.connection
1288 @errors.cyclades.server_id
1289 def _run(self, server_id, ip):
1290 self._optional_output(self.client.detach_floating_ip(server_id, ip))
1292 def main(self, server_id, IP):
1293 super(self.__class__, self)._run()
1294 self._run(server_id=server_id, ip=IP)