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',
270 network_id=ValueArgument(
271 'Show the connection details to that network', '--network-id'),
273 'Show VNC connection information (valid for a short period)',
274 '--vnc-credentials'),
275 stats=FlagArgument('Get URLs for server statistics', '--stats')
279 @errors.cyclades.connection
280 @errors.cyclades.server_id
281 def _run(self, server_id):
282 vm = self.client.get_server_nics(server_id)
284 self._print(vm.get('attachments', []))
285 elif self['network_id']:
287 self.client.get_server_network_nics(
288 server_id, self['network_id']), self.print_dict)
291 '(!) For security reasons, the following credentials are '
292 'invalidated\nafter a short time period, depending on the '
295 self.client.get_server_console(server_id), self.print_dict)
298 self.client.get_server_stats(server_id), self.print_dict)
300 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
301 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
302 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
303 self._print(vm, self.print_dict)
305 def main(self, server_id):
306 super(self.__class__, self)._run()
307 choose_one = ('nics', 'vnc', 'stats')
308 count = len([a for a in choose_one if self[a]])
310 raise CLIInvalidArgument('Invalid argument compination', details=[
311 'Arguments %s cannot be used simultaneously' % ', '.join(
312 [self.arguments[a].lvalue for a in choose_one])])
313 self._run(server_id=server_id)
316 class PersonalityArgument(KeyValueArgument):
319 ('local-path', 'contents'),
320 ('server-path', 'path'),
327 return getattr(self, '_value', [])
330 def value(self, newvalue):
331 if newvalue == self.default:
333 self._value, input_dict = [], {}
334 for i, terms in enumerate(newvalue):
335 termlist = terms.split(',')
336 if len(termlist) > len(self.terms):
337 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
338 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
340 for k, v in self.terms:
342 for item in termlist:
343 if item.lower().startswith(prefix):
344 input_dict[k] = item[len(k) + 1:]
348 termlist.remove(item)
351 path = input_dict['local-path']
353 path = termlist.pop(0)
355 raise CLIInvalidArgument(
356 '--personality: No local path specified',
357 details=howto_personality)
360 raise CLIInvalidArgument(
361 '--personality: File %s does not exist' % path,
362 details=howto_personality)
364 self._value.append(dict(path=path))
365 with open(expanduser(path)) as f:
366 self._value[i]['contents'] = b64encode(f.read())
367 for k, v in self.terms[1:]:
369 self._value[i][v] = input_dict[k]
372 self._value[i][v] = termlist.pop(0)
375 if k in ('mode', ) and self._value[i][v]:
377 self._value[i][v] = int(self._value[i][v], 8)
378 except ValueError as ve:
379 raise CLIInvalidArgument(
380 'Personality mode must be in octal', details=[
384 class NetworkArgument(RepeatableArgument):
385 """[id=]NETWORK_ID[,[ip=]IP]"""
389 return getattr(self, '_value', self.default)
392 def value(self, new_value):
393 for v in new_value or []:
394 part1, sep, part2 = v.partition(',')
396 if part1.startswith('id='):
397 netid = part1[len('id='):]
398 elif part1.startswith('ip='):
399 ip = part1[len('ip='):]
403 if (part2.startswith('id=') and netid) or (
404 part2.startswith('ip=') and ip):
405 raise CLIInvalidArgument(
406 'Invalid network argument %s' % v, details=[
407 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
408 if part2.startswith('id='):
409 netid = part2[len('id='):]
410 elif part2.startswith('ip='):
411 ip = part2[len('ip='):]
417 raise CLIInvalidArgument(
418 'Invalid network argument %s' % v, details=[
419 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
420 self._value = getattr(self, '_value', [])
421 self._value.append(dict(uuid=netid))
423 self._value[-1]['fixed_ip'] = ip
426 @command(server_cmds)
427 class server_create(_init_cyclades, _optional_json, _server_wait):
428 """Create a server (aka Virtual Machine)"""
431 server_name=ValueArgument('The name of the new server', '--name'),
432 flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
433 image_id=ValueArgument('The ID of the hardware image', '--image-id'),
434 personality=PersonalityArgument(
435 (80 * ' ').join(howto_personality), ('-p', '--personality')),
436 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
437 cluster_size=IntArgument(
438 'Create a cluster of servers of this size. In this case, the name'
439 'parameter is the prefix of each server in the cluster (e.g.,'
442 max_threads=IntArgument(
443 'Max threads in cluster mode (default 1)', '--threads'),
444 network_configuration=NetworkArgument(
445 'Connect server to network: [id=]NETWORK_ID[,[ip=]IP] . '
446 'Use only NETWORK_ID for private networks. . '
447 'Use NETWORK_ID,[ip=]IP for networks with IP. . '
448 'Can be repeated, mutually exclussive with --no-network',
450 no_network=FlagArgument(
451 'Do not create any network NICs on the server. . '
452 'Mutually exclusive to --network . '
453 'If neither --network or --no-network are used, the default '
454 'network policy is applied. This policy is configured on the '
455 'cloud and kamaki is oblivious to it',
458 required = ('server_name', 'flavor_id', 'image_id')
460 @errors.cyclades.cluster_size
461 def _create_cluster(self, prefix, flavor_id, image_id, size):
462 networks = self['network_configuration'] or (
463 None if self['no_network'] else [])
465 name='%s%s' % (prefix, i if size > 1 else ''),
468 personality=self['personality'],
469 networks=networks) for i in range(1, 1 + size)]
471 return [self.client.create_server(**servers[0])]
472 self.client.MAX_THREADS = int(self['max_threads'] or 1)
474 r = self.client.async_run(self.client.create_server, servers)
476 except Exception as e:
480 requested_names = [s['name'] for s in servers]
481 spawned_servers = [dict(
483 id=s['id']) for s in self.client.list_servers() if (
484 s['name'] in requested_names)]
485 self.error('Failed to build %s servers' % size)
486 self.error('Found %s matching servers:' % len(spawned_servers))
487 self._print(spawned_servers, out=self._err)
488 self.error('Check if any of these servers should be removed\n')
489 except Exception as ne:
490 self.error('Error (%s) while notifying about errors' % ne)
495 @errors.cyclades.connection
497 @errors.cyclades.flavor_id
498 def _run(self, name, flavor_id, image_id):
499 for r in self._create_cluster(
500 name, flavor_id, image_id, size=self['cluster_size'] or 1):
502 self.error('Create %s: server response was %s' % (name, r))
504 usernames = self._uuids2usernames(
505 [r['user_id'], r['tenant_id']])
506 r['user_id'] += ' (%s)' % usernames[r['user_id']]
507 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
508 self._print(r, self.print_dict)
510 self._wait(r['id'], r['status'])
514 super(self.__class__, self)._run()
515 if self['no_network'] and self['network']:
516 raise CLIInvalidArgument(
517 'Invalid argument compination', importance=2, details=[
518 'Arguments %s and %s are mutually exclusive' % (
519 self.arguments['no_network'].lvalue,
520 self.arguments['network'].lvalue)])
522 name=self['server_name'],
523 flavor_id=self['flavor_id'],
524 image_id=self['image_id'])
527 class FirewallProfileArgument(ValueArgument):
529 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
533 return getattr(self, '_value', None)
536 def value(self, new_profile):
538 new_profile = new_profile.upper()
539 if new_profile in self.profiles:
540 self._value = new_profile
542 raise CLIInvalidArgument(
543 'Invalid firewall profile %s' % new_profile,
544 details=['Valid values: %s' % ', '.join(self.profiles)])
547 @command(server_cmds)
548 class server_modify(_init_cyclades, _optional_output_cmd):
549 """Modify attributes of a virtual server"""
552 server_name=ValueArgument('The new name', '--name'),
553 flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
554 firewall_profile=FirewallProfileArgument(
555 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
557 metadata_to_set=KeyValueArgument(
558 'Set metadata in key=value form (can be repeated)',
560 metadata_to_delete=RepeatableArgument(
561 'Delete metadata by key (can be repeated)', '--metadata-del')
564 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
565 'metadata_to_delete']
568 @errors.cyclades.connection
569 @errors.cyclades.server_id
570 def _run(self, server_id):
571 if self['server_name']:
572 self.client.update_server_name((server_id), self['server_name'])
573 if self['flavor_id']:
574 self.client.resize_server(server_id, self['flavor_id'])
575 if self['firewall_profile']:
576 self.client.set_firewall_profile(
577 server_id=server_id, profile=self['firewall_profile'])
578 if self['metadata_to_set']:
579 self.client.update_server_metadata(
580 server_id, **self['metadata_to_set'])
581 for key in (self['metadata_to_delete'] or []):
582 errors.cyclades.metadata(
583 self.client.delete_server_metadata)(server_id, key=key)
584 if self['with_output']:
585 self._optional_output(self.client.get_server_details(server_id))
587 def main(self, server_id):
588 super(self.__class__, self)._run()
589 self._run(server_id=server_id)
592 @command(server_cmds)
593 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
594 """Delete a virtual server"""
597 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
598 cluster=FlagArgument(
599 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
600 'prefix. In that case, the prefix replaces the server id',
604 def _server_ids(self, server_var):
606 return [s['id'] for s in self.client.list_servers() if (
607 s['name'].startswith(server_var))]
609 @errors.cyclades.server_id
610 def _check_server_id(self, server_id):
613 return [_check_server_id(self, server_id=server_var), ]
616 @errors.cyclades.connection
617 def _run(self, server_var):
618 for server_id in self._server_ids(server_var):
620 details = self.client.get_server_details(server_id)
621 status = details['status']
623 r = self.client.delete_server(server_id)
624 self._optional_output(r)
627 self._wait(server_id, status)
629 def main(self, server_id_or_cluster_prefix):
630 super(self.__class__, self)._run()
631 self._run(server_id_or_cluster_prefix)
634 @command(server_cmds)
635 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
636 """Reboot a virtual server"""
640 'perform a hard reboot (deprecated)', ('-f', '--force')),
641 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
642 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
646 @errors.cyclades.connection
647 @errors.cyclades.server_id
648 def _run(self, server_id):
649 hard_reboot = self['hard']
652 'WARNING: -f/--force will be deprecated in version 0.12\n'
653 '\tIn the future, please use --type=hard instead')
655 if self['type'].lower() in ('soft', ):
657 elif self['type'].lower() in ('hard', ):
660 raise CLISyntaxError(
661 'Invalid reboot type %s' % self['type'],
662 importance=2, details=[
663 '--type values are either SOFT (default) or HARD'])
665 r = self.client.reboot_server(int(server_id), hard_reboot)
666 self._optional_output(r)
669 self._wait(server_id, 'REBOOT')
671 def main(self, server_id):
672 super(self.__class__, self)._run()
673 self._run(server_id=server_id)
676 @command(server_cmds)
677 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
678 """Start an existing virtual server"""
681 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
685 @errors.cyclades.connection
686 @errors.cyclades.server_id
687 def _run(self, server_id):
690 details = self.client.get_server_details(server_id)
691 status = details['status']
692 if status in ('ACTIVE', ):
695 r = self.client.start_server(int(server_id))
696 self._optional_output(r)
699 self._wait(server_id, status)
701 def main(self, server_id):
702 super(self.__class__, self)._run()
703 self._run(server_id=server_id)
706 @command(server_cmds)
707 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
708 """Shutdown an active virtual server"""
711 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
715 @errors.cyclades.connection
716 @errors.cyclades.server_id
717 def _run(self, server_id):
720 details = self.client.get_server_details(server_id)
721 status = details['status']
722 if status in ('STOPPED', ):
725 r = self.client.shutdown_server(int(server_id))
726 self._optional_output(r)
729 self._wait(server_id, status)
731 def main(self, server_id):
732 super(self.__class__, self)._run()
733 self._run(server_id=server_id)
736 @command(server_cmds)
737 class server_nics(_init_cyclades):
738 """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
740 def main(self, *args):
741 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
743 ' [kamaki] server info <SERVER_ID> --nics'])
746 @command(server_cmds)
747 class server_console(_init_cyclades, _optional_json):
748 """DEPRECATED, use: [kamaki] server info SERVER_ID --vnc-credentials"""
750 def main(self, *args):
751 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
753 ' [kamaki] server info <SERVER_ID> --vnc-credentials'])
756 @command(server_cmds)
757 class server_rename(_init_cyclades, _optional_json):
758 """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
760 def main(self, *args):
761 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
763 ' [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
766 @command(server_cmds)
767 class server_stats(_init_cyclades, _optional_json):
768 """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
770 def main(self, *args):
771 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
773 ' [kamaki] server info <SERVER_ID> --stats'])
776 @command(server_cmds)
777 class server_wait(_init_cyclades, _server_wait):
778 """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
782 'Wait limit in seconds (default: 60)', '--timeout', default=60)
786 @errors.cyclades.connection
787 @errors.cyclades.server_id
788 def _run(self, server_id, current_status):
789 r = self.client.get_server_details(server_id)
790 if r['status'].lower() == current_status.lower():
791 self._wait(server_id, current_status, timeout=self['timeout'])
794 'Server %s: Cannot wait for status %s, '
795 'status is already %s' % (
796 server_id, current_status, r['status']))
798 def main(self, server_id, current_status='BUILD'):
799 super(self.__class__, self)._run()
800 self._run(server_id=server_id, current_status=current_status)
803 @command(flavor_cmds)
804 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
805 """List available hardware flavors"""
807 PERMANENTS = ('id', 'name')
810 detail=FlagArgument('show detailed output', ('-l', '--details')),
811 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
813 'output results in pages (-n to set items per page, default 10)',
815 enum=FlagArgument('Enumerate results', '--enumerate'),
816 ram=ValueArgument('filter by ram', ('--ram')),
817 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
818 disk=ValueArgument('filter by disk size in GB', ('--disk')),
819 disk_template=ValueArgument(
820 'filter by disk_templace', ('--disk-template'))
823 def _apply_common_filters(self, flavors):
824 common_filters = dict()
826 common_filters['ram'] = self['ram']
828 common_filters['vcpus'] = self['vcpus']
830 common_filters['disk'] = self['disk']
831 if self['disk_template']:
832 common_filters['SNF:disk_template'] = self['disk_template']
833 return filter_dicts_by_dict(flavors, common_filters)
836 @errors.cyclades.connection
838 withcommons = self['ram'] or self['vcpus'] or (
839 self['disk'] or self['disk_template'])
840 detail = self['detail'] or withcommons
841 flavors = self.client.list_flavors(detail)
842 flavors = self._filter_by_name(flavors)
843 flavors = self._filter_by_id(flavors)
845 flavors = self._apply_common_filters(flavors)
846 if not (self['detail'] or (
847 self['json_output'] or self['output_format'])):
848 remove_from_items(flavors, 'links')
849 if detail and not self['detail']:
851 for key in set(flv).difference(self.PERMANENTS):
853 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
856 with_redundancy=self['detail'], with_enumeration=self['enum'],
859 pager(kwargs['out'].getvalue())
862 super(self.__class__, self)._run()
866 @command(flavor_cmds)
867 class flavor_info(_init_cyclades, _optional_json):
868 """Detailed information on a hardware flavor
869 To get a list of available flavors and flavor ids, try /flavor list
873 @errors.cyclades.connection
874 @errors.cyclades.flavor_id
875 def _run(self, flavor_id):
877 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
879 def main(self, flavor_id):
880 super(self.__class__, self)._run()
881 self._run(flavor_id=flavor_id)
884 def _add_name(self, net):
885 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
887 uuids.append(user_id)
889 uuids.append(tenant_id)
891 usernames = self._uuids2usernames(uuids)
893 net['user_id'] += ' (%s)' % usernames[user_id]
895 net['tenant_id'] += ' (%s)' % usernames[tenant_id]