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
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 _commands = [server_cmds, flavor_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 ' [local-path=]PATH: local file to be injected (relative or absolute)',
66 ' [server-path=]SERVER_PATH: destination location inside server Image',
67 ' [owner=]OWNER: virtual servers user id for the remote file',
68 ' [group=]GROUP: virtual servers group id or name for the remote file',
69 ' [mode=]MODE: permission in octal (e.g., 0777)',
70 'e.g., -p /tmp/my.file,owner=root,mode=0777']
73 class _service_wait(object):
75 wait_arguments = dict(
76 progress_bar=ProgressBarArgument(
77 'do not show progress bar', ('-N', '--no-progress-bar'), False)
81 self, service, service_id, status_method, current_status,
82 countdown=True, timeout=60):
83 (progress_bar, wait_cb) = self._safe_progress_bar(
84 '%s %s: status is still %s' % (
85 service, service_id, current_status),
86 countdown=countdown, timeout=timeout)
89 new_mode = status_method(
90 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
92 self.error('%s %s: status is now %s' % (
93 service, service_id, new_mode))
95 self.error('%s %s: status is still %s' % (
96 service, service_id, current_status))
97 except KeyboardInterrupt:
98 self.error('\n- canceled')
100 self._safe_progress_bar_finish(progress_bar)
103 class _server_wait(_service_wait):
105 def _wait(self, server_id, current_status, timeout=60):
106 super(_server_wait, self)._wait(
107 'Server', server_id, self.client.wait_server, current_status,
108 countdown=(current_status not in ('BUILD', )),
109 timeout=timeout if current_status not in ('BUILD', ) else 100)
112 class _init_cyclades(_command_init):
115 def _run(self, service='compute'):
116 if getattr(self, 'cloud', None):
117 base_url = self._custom_url(service) or self._custom_url(
120 token = self._custom_token(service) or self._custom_token(
121 'cyclades') or self.config.get_cloud('token')
122 self.client = CycladesClient(base_url=base_url, token=token)
125 self.cloud = 'default'
126 if getattr(self, 'auth_base', False):
127 cyclades_endpoints = self.auth_base.get_service_endpoints(
128 self._custom_type('cyclades') or 'compute',
129 self._custom_version('cyclades') or '')
130 base_url = cyclades_endpoints['publicURL']
131 token = self.auth_base.token
132 self.client = CycladesClient(base_url=base_url, token=token)
134 raise CLIBaseUrlError(service='cyclades')
140 @command(server_cmds)
141 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
142 """List virtual servers accessible by user
143 Use filtering arguments (e.g., --name-like) to manage long server lists
146 PERMANENTS = ('id', 'name')
149 detail=FlagArgument('show detailed output', ('-l', '--details')),
151 'show only items since date (\' d/m/Y H:M:S \')',
154 'limit number of listed virtual servers', ('-n', '--number')),
156 'output results in pages (-n to set items per page, default 10)',
158 enum=FlagArgument('Enumerate results', '--enumerate'),
159 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
160 image_id=ValueArgument('filter by image id', ('--image-id')),
161 user_id=ValueArgument('filter by user id', ('--user-id')),
162 user_name=ValueArgument('filter by user name', ('--user-name')),
163 status=ValueArgument(
164 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
166 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
167 meta_like=KeyValueArgument(
168 'print only if in key=value, the value is part of actual value',
169 ('--metadata-like')),
172 def _add_user_name(self, servers):
173 uuids = self._uuids2usernames(list(set(
174 [srv['user_id'] for srv in servers] +
175 [srv['tenant_id'] for srv in servers])))
177 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
178 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
181 def _apply_common_filters(self, servers):
182 common_filters = dict()
184 common_filters['status'] = self['status']
185 if self['user_id'] or self['user_name']:
186 uuid = self['user_id'] or self._username2uuid(self['user_name'])
187 common_filters['user_id'] = uuid
188 return filter_dicts_by_dict(servers, common_filters)
190 def _filter_by_image(self, servers):
191 iid = self['image_id']
192 return [srv for srv in servers if srv['image']['id'] == iid]
194 def _filter_by_flavor(self, servers):
195 fid = self['flavor_id']
196 return [srv for srv in servers if (
197 '%s' % srv['image']['id'] == '%s' % fid)]
199 def _filter_by_metadata(self, servers):
202 if not 'metadata' in srv:
204 meta = [dict(srv['metadata'])]
206 meta = filter_dicts_by_dict(meta, self['meta'])
207 if meta and self['meta_like']:
208 meta = filter_dicts_by_dict(
209 meta, self['meta_like'], exact_match=False)
211 new_servers.append(srv)
215 @errors.cyclades.connection
216 @errors.cyclades.date
218 withimage = bool(self['image_id'])
219 withflavor = bool(self['flavor_id'])
220 withmeta = bool(self['meta'] or self['meta_like'])
222 self['status'] or self['user_id'] or self['user_name'])
223 detail = self['detail'] or (
224 withimage or withflavor or withmeta or withcommons)
225 servers = self.client.list_servers(detail, self['since'])
227 servers = self._filter_by_name(servers)
228 servers = self._filter_by_id(servers)
229 servers = self._apply_common_filters(servers)
231 servers = self._filter_by_image(servers)
233 servers = self._filter_by_flavor(servers)
235 servers = self._filter_by_metadata(servers)
237 if self['detail'] and not (
238 self['json_output'] or self['output_format']):
239 servers = self._add_user_name(servers)
240 elif not (self['detail'] or (
241 self['json_output'] or self['output_format'])):
242 remove_from_items(servers, 'links')
243 if detail and not self['detail']:
245 for key in set(srv).difference(self.PERMANENTS):
247 kwargs = dict(with_enumeration=self['enum'])
249 kwargs['out'] = StringIO()
252 servers = servers[:self['limit']]
253 self._print(servers, **kwargs)
255 pager(kwargs['out'].getvalue())
258 super(self.__class__, self)._run()
262 @command(server_cmds)
263 class server_info(_init_cyclades, _optional_json):
264 """Detailed information on a Virtual Machine"""
268 'Show only the network interfaces of this virtual server',
271 'Show VNC connection information (valid for a short period)',
272 '--vnc-credentials'),
273 stats=FlagArgument('Get URLs for server statistics', '--stats'),
274 diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
278 @errors.cyclades.connection
279 @errors.cyclades.server_id
280 def _run(self, server_id):
281 vm = self.client.get_server_details(server_id)
283 self._print(vm.get('attachments', []))
286 '(!) For security reasons, the following credentials are '
287 'invalidated\nafter a short time period, depending on the '
290 self.client.get_server_console(server_id), self.print_dict)
293 self.client.get_server_stats(server_id), self.print_dict)
294 elif self['diagnostics']:
295 self._print(self.client.get_server_diagnostics(server_id))
297 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
298 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
299 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
300 self._print(vm, self.print_dict)
302 def main(self, server_id):
303 super(self.__class__, self)._run()
304 choose_one = ('addr', 'vnc', 'stats')
305 count = len([a for a in choose_one if self[a]])
307 raise CLIInvalidArgument('Invalid argument compination', details=[
308 'Arguments %s cannot be used simultaneously' % ', '.join(
309 [self.arguments[a].lvalue for a in choose_one])])
310 self._run(server_id=server_id)
313 class PersonalityArgument(KeyValueArgument):
316 ('local-path', 'contents'),
317 ('server-path', 'path'),
324 return getattr(self, '_value', [])
327 def value(self, newvalue):
328 if newvalue == self.default:
330 self._value, input_dict = [], {}
331 for i, terms in enumerate(newvalue):
332 termlist = terms.split(',')
333 if len(termlist) > len(self.terms):
334 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
335 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
337 for k, v in self.terms:
339 for item in termlist:
340 if item.lower().startswith(prefix):
341 input_dict[k] = item[len(k) + 1:]
345 termlist.remove(item)
348 path = input_dict['local-path']
350 path = termlist.pop(0)
352 raise CLIInvalidArgument(
353 '--personality: No local path specified',
354 details=howto_personality)
357 raise CLIInvalidArgument(
358 '--personality: File %s does not exist' % path,
359 details=howto_personality)
361 self._value.append(dict(path=path))
362 with open(expanduser(path)) as f:
363 self._value[i]['contents'] = b64encode(f.read())
364 for k, v in self.terms[1:]:
366 self._value[i][v] = input_dict[k]
369 self._value[i][v] = termlist.pop(0)
372 if k in ('mode', ) and self._value[i][v]:
374 self._value[i][v] = int(self._value[i][v], 8)
375 except ValueError as ve:
376 raise CLIInvalidArgument(
377 'Personality mode must be in octal', details=[
381 class NetworkArgument(RepeatableArgument):
382 """[id=]NETWORK_ID[,[ip=]IP]"""
386 return getattr(self, '_value', self.default)
389 def value(self, new_value):
390 for v in new_value or []:
391 part1, sep, part2 = v.partition(',')
393 if part1.startswith('id='):
394 netid = part1[len('id='):]
395 elif part1.startswith('ip='):
396 ip = part1[len('ip='):]
400 if (part2.startswith('id=') and netid) or (
401 part2.startswith('ip=') and ip):
402 raise CLIInvalidArgument(
403 'Invalid network argument %s' % v, details=[
404 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
405 if part2.startswith('id='):
406 netid = part2[len('id='):]
407 elif part2.startswith('ip='):
408 ip = part2[len('ip='):]
414 raise CLIInvalidArgument(
415 'Invalid network argument %s' % v, details=[
416 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
417 self._value = getattr(self, '_value', [])
418 self._value.append(dict(uuid=netid))
420 self._value[-1]['fixed_ip'] = ip
423 @command(server_cmds)
424 class server_create(_init_cyclades, _optional_json, _server_wait):
425 """Create a server (aka Virtual Machine)"""
428 server_name=ValueArgument('The name of the new server', '--name'),
429 flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
430 image_id=ValueArgument('The ID of the hardware image', '--image-id'),
431 personality=PersonalityArgument(
432 (80 * ' ').join(howto_personality), ('-p', '--personality')),
433 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
434 cluster_size=IntArgument(
435 'Create a cluster of servers of this size. In this case, the name'
436 'parameter is the prefix of each server in the cluster (e.g.,'
439 max_threads=IntArgument(
440 'Max threads in cluster mode (default 1)', '--threads'),
441 network_configuration=NetworkArgument(
442 'Connect server to network: [id=]NETWORK_ID[,[ip=]IP] . '
443 'Use only NETWORK_ID for private networks. . '
444 'Use NETWORK_ID,[ip=]IP for networks with IP. . '
445 'Can be repeated, mutually exclussive with --no-network',
447 no_network=FlagArgument(
448 'Do not create any network NICs on the server. . '
449 'Mutually exclusive to --network . '
450 'If neither --network or --no-network are used, the default '
451 'network policy is applied. This policy is configured on the '
452 'cloud and kamaki is oblivious to it',
455 required = ('server_name', 'flavor_id', 'image_id')
457 @errors.cyclades.cluster_size
458 def _create_cluster(self, prefix, flavor_id, image_id, size):
459 networks = self['network_configuration'] or (
460 None if self['no_network'] else [])
462 name='%s%s' % (prefix, i if size > 1 else ''),
465 personality=self['personality'],
466 networks=networks) for i in range(1, 1 + size)]
468 return [self.client.create_server(**servers[0])]
469 self.client.MAX_THREADS = int(self['max_threads'] or 1)
471 r = self.client.async_run(self.client.create_server, servers)
473 except Exception as e:
477 requested_names = [s['name'] for s in servers]
478 spawned_servers = [dict(
480 id=s['id']) for s in self.client.list_servers() if (
481 s['name'] in requested_names)]
482 self.error('Failed to build %s servers' % size)
483 self.error('Found %s matching servers:' % len(spawned_servers))
484 self._print(spawned_servers, out=self._err)
485 self.error('Check if any of these servers should be removed\n')
486 except Exception as ne:
487 self.error('Error (%s) while notifying about errors' % ne)
492 @errors.cyclades.connection
494 @errors.cyclades.flavor_id
495 def _run(self, name, flavor_id, image_id):
496 for r in self._create_cluster(
497 name, flavor_id, image_id, size=self['cluster_size'] or 1):
499 self.error('Create %s: server response was %s' % (name, r))
501 usernames = self._uuids2usernames(
502 [r['user_id'], r['tenant_id']])
503 r['user_id'] += ' (%s)' % usernames[r['user_id']]
504 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
505 self._print(r, self.print_dict)
507 self._wait(r['id'], r['status'])
511 super(self.__class__, self)._run()
512 if self['no_network'] and self['network']:
513 raise CLIInvalidArgument(
514 'Invalid argument compination', importance=2, details=[
515 'Arguments %s and %s are mutually exclusive' % (
516 self.arguments['no_network'].lvalue,
517 self.arguments['network'].lvalue)])
519 name=self['server_name'],
520 flavor_id=self['flavor_id'],
521 image_id=self['image_id'])
524 class FirewallProfileArgument(ValueArgument):
526 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
530 return getattr(self, '_value', None)
533 def value(self, new_profile):
535 new_profile = new_profile.upper()
536 if new_profile in self.profiles:
537 self._value = new_profile
539 raise CLIInvalidArgument(
540 'Invalid firewall profile %s' % new_profile,
541 details=['Valid values: %s' % ', '.join(self.profiles)])
544 @command(server_cmds)
545 class server_modify(_init_cyclades, _optional_output_cmd):
546 """Modify attributes of a virtual server"""
549 server_name=ValueArgument('The new name', '--name'),
550 flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
551 firewall_profile=FirewallProfileArgument(
552 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
554 metadata_to_set=KeyValueArgument(
555 'Set metadata in key=value form (can be repeated)',
557 metadata_to_delete=RepeatableArgument(
558 'Delete metadata by key (can be repeated)', '--metadata-del')
561 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
562 'metadata_to_delete']
565 @errors.cyclades.connection
566 @errors.cyclades.server_id
567 def _run(self, server_id):
568 if self['server_name']:
569 self.client.update_server_name((server_id), self['server_name'])
570 if self['flavor_id']:
571 self.client.resize_server(server_id, self['flavor_id'])
572 if self['firewall_profile']:
573 self.client.set_firewall_profile(
574 server_id=server_id, profile=self['firewall_profile'])
575 if self['metadata_to_set']:
576 self.client.update_server_metadata(
577 server_id, **self['metadata_to_set'])
578 for key in (self['metadata_to_delete'] or []):
579 errors.cyclades.metadata(
580 self.client.delete_server_metadata)(server_id, key=key)
581 if self['with_output']:
582 self._optional_output(self.client.get_server_details(server_id))
584 def main(self, server_id):
585 super(self.__class__, self)._run()
586 self._run(server_id=server_id)
589 @command(server_cmds)
590 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
591 """Delete a virtual server"""
594 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
595 cluster=FlagArgument(
596 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
597 'prefix. In that case, the prefix replaces the server id',
601 def _server_ids(self, server_var):
603 return [s['id'] for s in self.client.list_servers() if (
604 s['name'].startswith(server_var))]
606 @errors.cyclades.server_id
607 def _check_server_id(self, server_id):
610 return [_check_server_id(self, server_id=server_var), ]
613 @errors.cyclades.connection
614 def _run(self, server_var):
615 for server_id in self._server_ids(server_var):
617 details = self.client.get_server_details(server_id)
618 status = details['status']
620 r = self.client.delete_server(server_id)
621 self._optional_output(r)
624 self._wait(server_id, status)
626 def main(self, server_id_or_cluster_prefix):
627 super(self.__class__, self)._run()
628 self._run(server_id_or_cluster_prefix)
631 @command(server_cmds)
632 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
633 """Reboot a virtual server"""
637 'perform a hard reboot (deprecated)', ('-f', '--force')),
638 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
639 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
643 @errors.cyclades.connection
644 @errors.cyclades.server_id
645 def _run(self, server_id):
646 hard_reboot = self['hard']
649 'WARNING: -f/--force will be deprecated in version 0.12\n'
650 '\tIn the future, please use --type=hard instead')
652 if self['type'].lower() in ('soft', ):
654 elif self['type'].lower() in ('hard', ):
657 raise CLISyntaxError(
658 'Invalid reboot type %s' % self['type'],
659 importance=2, details=[
660 '--type values are either SOFT (default) or HARD'])
662 r = self.client.reboot_server(int(server_id), hard_reboot)
663 self._optional_output(r)
666 self._wait(server_id, 'REBOOT')
668 def main(self, server_id):
669 super(self.__class__, self)._run()
670 self._run(server_id=server_id)
673 @command(server_cmds)
674 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
675 """Start an existing virtual server"""
678 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
682 @errors.cyclades.connection
683 @errors.cyclades.server_id
684 def _run(self, server_id):
687 details = self.client.get_server_details(server_id)
688 status = details['status']
689 if status in ('ACTIVE', ):
692 r = self.client.start_server(int(server_id))
693 self._optional_output(r)
696 self._wait(server_id, status)
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_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
705 """Shutdown an active virtual server"""
708 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
712 @errors.cyclades.connection
713 @errors.cyclades.server_id
714 def _run(self, server_id):
717 details = self.client.get_server_details(server_id)
718 status = details['status']
719 if status in ('STOPPED', ):
722 r = self.client.shutdown_server(int(server_id))
723 self._optional_output(r)
726 self._wait(server_id, status)
728 def main(self, server_id):
729 super(self.__class__, self)._run()
730 self._run(server_id=server_id)
733 @command(server_cmds)
734 class server_addr(_init_cyclades):
735 """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
737 def main(self, *args):
738 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
740 ' [kamaki] server info <SERVER_ID> --nics'])
743 @command(server_cmds)
744 class server_console(_init_cyclades, _optional_json):
745 """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
747 def main(self, *args):
748 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
750 ' [kamaki] server info <SERVER_ID> --vnc-credentials'])
753 @command(server_cmds)
754 class server_rename(_init_cyclades, _optional_json):
755 """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
757 def main(self, *args):
758 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
760 ' [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
763 @command(server_cmds)
764 class server_stats(_init_cyclades, _optional_json):
765 """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
767 def main(self, *args):
768 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
770 ' [kamaki] server info <SERVER_ID> --stats'])
773 @command(server_cmds)
774 class server_wait(_init_cyclades, _server_wait):
775 """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
779 'Wait limit in seconds (default: 60)', '--timeout', default=60)
783 @errors.cyclades.connection
784 @errors.cyclades.server_id
785 def _run(self, server_id, current_status):
786 r = self.client.get_server_details(server_id)
787 if r['status'].lower() == current_status.lower():
788 self._wait(server_id, current_status, timeout=self['timeout'])
791 'Server %s: Cannot wait for status %s, '
792 'status is already %s' % (
793 server_id, current_status, r['status']))
795 def main(self, server_id, current_status='BUILD'):
796 super(self.__class__, self)._run()
797 self._run(server_id=server_id, current_status=current_status)
800 @command(flavor_cmds)
801 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
802 """List available hardware flavors"""
804 PERMANENTS = ('id', 'name')
807 detail=FlagArgument('show detailed output', ('-l', '--details')),
808 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
810 'output results in pages (-n to set items per page, default 10)',
812 enum=FlagArgument('Enumerate results', '--enumerate'),
813 ram=ValueArgument('filter by ram', ('--ram')),
814 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
815 disk=ValueArgument('filter by disk size in GB', ('--disk')),
816 disk_template=ValueArgument(
817 'filter by disk_templace', ('--disk-template'))
820 def _apply_common_filters(self, flavors):
821 common_filters = dict()
823 common_filters['ram'] = self['ram']
825 common_filters['vcpus'] = self['vcpus']
827 common_filters['disk'] = self['disk']
828 if self['disk_template']:
829 common_filters['SNF:disk_template'] = self['disk_template']
830 return filter_dicts_by_dict(flavors, common_filters)
833 @errors.cyclades.connection
835 withcommons = self['ram'] or self['vcpus'] or (
836 self['disk'] or self['disk_template'])
837 detail = self['detail'] or withcommons
838 flavors = self.client.list_flavors(detail)
839 flavors = self._filter_by_name(flavors)
840 flavors = self._filter_by_id(flavors)
842 flavors = self._apply_common_filters(flavors)
843 if not (self['detail'] or (
844 self['json_output'] or self['output_format'])):
845 remove_from_items(flavors, 'links')
846 if detail and not self['detail']:
848 for key in set(flv).difference(self.PERMANENTS):
850 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
853 with_redundancy=self['detail'], with_enumeration=self['enum'],
856 pager(kwargs['out'].getvalue())
859 super(self.__class__, self)._run()
863 @command(flavor_cmds)
864 class flavor_info(_init_cyclades, _optional_json):
865 """Detailed information on a hardware flavor
866 To get a list of available flavors and flavor ids, try /flavor list
870 @errors.cyclades.connection
871 @errors.cyclades.flavor_id
872 def _run(self, flavor_id):
874 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
876 def main(self, flavor_id):
877 super(self.__class__, self)._run()
878 self._run(flavor_id=flavor_id)
881 def _add_name(self, net):
882 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
884 uuids.append(user_id)
886 uuids.append(tenant_id)
888 usernames = self._uuids2usernames(uuids)
890 net['user_id'] += ' (%s)' % usernames[user_id]
892 net['tenant_id'] += ' (%s)' % usernames[tenant_id]