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'),
272 stats=FlagArgument('Get URLs for server statistics', '--stats'),
273 diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
277 @errors.cyclades.connection
278 @errors.cyclades.server_id
279 def _run(self, server_id):
280 vm = self.client.get_server_nics(server_id)
282 self._print(vm.get('attachments', []))
283 elif self['network_id']:
285 self.client.get_server_network_nics(
286 server_id, self['network_id']), self.print_dict)
289 self.client.get_server_stats(server_id), self.print_dict)
291 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
292 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
293 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
294 self._print(vm, self.print_dict)
296 def main(self, server_id):
297 super(self.__class__, self)._run()
298 choose_one = ('nics', 'vnc', 'stats')
299 count = len([a for a in choose_one if self[a]])
301 raise CLIInvalidArgument('Invalid argument compination', details=[
302 'Arguments %s cannot be used simultaneously' % ', '.join(
303 [self.arguments[a].lvalue for a in choose_one])])
304 self._run(server_id=server_id)
307 class PersonalityArgument(KeyValueArgument):
310 ('local-path', 'contents'),
311 ('server-path', 'path'),
318 return getattr(self, '_value', [])
321 def value(self, newvalue):
322 if newvalue == self.default:
324 self._value, input_dict = [], {}
325 for i, terms in enumerate(newvalue):
326 termlist = terms.split(',')
327 if len(termlist) > len(self.terms):
328 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
329 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
331 for k, v in self.terms:
333 for item in termlist:
334 if item.lower().startswith(prefix):
335 input_dict[k] = item[len(k) + 1:]
339 termlist.remove(item)
342 path = input_dict['local-path']
344 path = termlist.pop(0)
346 raise CLIInvalidArgument(
347 '--personality: No local path specified',
348 details=howto_personality)
351 raise CLIInvalidArgument(
352 '--personality: File %s does not exist' % path,
353 details=howto_personality)
355 self._value.append(dict(path=path))
356 with open(expanduser(path)) as f:
357 self._value[i]['contents'] = b64encode(f.read())
358 for k, v in self.terms[1:]:
360 self._value[i][v] = input_dict[k]
363 self._value[i][v] = termlist.pop(0)
366 if k in ('mode', ) and self._value[i][v]:
368 self._value[i][v] = int(self._value[i][v], 8)
369 except ValueError as ve:
370 raise CLIInvalidArgument(
371 'Personality mode must be in octal', details=[
375 class NetworkArgument(RepeatableArgument):
376 """[id=]NETWORK_ID[,[ip=]IP]"""
380 return getattr(self, '_value', self.default)
383 def value(self, new_value):
384 for v in new_value or []:
385 part1, sep, part2 = v.partition(',')
387 if part1.startswith('id='):
388 netid = part1[len('id='):]
389 elif part1.startswith('ip='):
390 ip = part1[len('ip='):]
394 if (part2.startswith('id=') and netid) or (
395 part2.startswith('ip=') and ip):
396 raise CLIInvalidArgument(
397 'Invalid network argument %s' % v, details=[
398 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
399 if part2.startswith('id='):
400 netid = part2[len('id='):]
401 elif part2.startswith('ip='):
402 ip = part2[len('ip='):]
408 raise CLIInvalidArgument(
409 'Invalid network argument %s' % v, details=[
410 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
411 self._value = getattr(self, '_value', [])
412 self._value.append(dict(uuid=netid))
414 self._value[-1]['fixed_ip'] = ip
417 @command(server_cmds)
418 class server_create(_init_cyclades, _optional_json, _server_wait):
419 """Create a server (aka Virtual Machine)"""
422 server_name=ValueArgument('The name of the new server', '--name'),
423 flavor_id=IntArgument('The ID of the flavor', '--flavor-id'),
424 image_id=ValueArgument('The ID of the image', '--image-id'),
425 personality=PersonalityArgument(
426 (80 * ' ').join(howto_personality), ('-p', '--personality')),
427 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
428 cluster_size=IntArgument(
429 'Create a cluster of servers of this size. In this case, the name'
430 'parameter is the prefix of each server in the cluster (e.g.,'
433 max_threads=IntArgument(
434 'Max threads in cluster mode (default 1)', '--threads'),
435 network_configuration=NetworkArgument(
436 'Connect server to network: [id=]NETWORK_ID[,[ip=]IP] . '
437 'Use only NETWORK_ID for private networks. . '
438 'Use NETWORK_ID,[ip=]IP for networks with IP. . '
439 'Can be repeated, mutually exclussive with --no-network',
441 no_network=FlagArgument(
442 'Do not create any network NICs on the server. . '
443 'Mutually exclusive to --network . '
444 'If neither --network or --no-network are used, the default '
445 'network policy is applied. This policy is configured on the '
446 'cloud and kamaki is oblivious to it',
449 required = ('server_name', 'flavor_id', 'image_id')
451 @errors.cyclades.cluster_size
452 def _create_cluster(self, prefix, flavor_id, image_id, size):
453 networks = self['network_configuration'] or (
454 None if self['no_network'] else [])
456 name='%s%s' % (prefix, i if size > 1 else ''),
459 personality=self['personality'],
460 networks=networks) for i in range(1, 1 + size)]
462 return [self.client.create_server(**servers[0])]
463 self.client.MAX_THREADS = int(self['max_threads'] or 1)
465 r = self.client.async_run(self.client.create_server, servers)
467 except Exception as e:
471 requested_names = [s['name'] for s in servers]
472 spawned_servers = [dict(
474 id=s['id']) for s in self.client.list_servers() if (
475 s['name'] in requested_names)]
476 self.error('Failed to build %s servers' % size)
477 self.error('Found %s matching servers:' % len(spawned_servers))
478 self._print(spawned_servers, out=self._err)
479 self.error('Check if any of these servers should be removed\n')
480 except Exception as ne:
481 self.error('Error (%s) while notifying about errors' % ne)
486 @errors.cyclades.connection
488 @errors.cyclades.flavor_id
489 def _run(self, name, flavor_id, image_id):
490 for r in self._create_cluster(
491 name, flavor_id, image_id, size=self['cluster_size'] or 1):
493 self.error('Create %s: server response was %s' % (name, r))
495 usernames = self._uuids2usernames(
496 [r['user_id'], r['tenant_id']])
497 r['user_id'] += ' (%s)' % usernames[r['user_id']]
498 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
499 self._print(r, self.print_dict)
501 self._wait(r['id'], r['status'])
505 super(self.__class__, self)._run()
506 if self['no_network'] and self['network_configuration']:
507 raise CLIInvalidArgument(
508 'Invalid argument compination', importance=2, details=[
509 'Arguments %s and %s are mutually exclusive' % (
510 self.arguments['no_network'].lvalue,
511 self.arguments['network_configuration'].lvalue)])
513 name=self['server_name'],
514 flavor_id=self['flavor_id'],
515 image_id=self['image_id'])
518 class FirewallProfileArgument(ValueArgument):
520 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
524 return getattr(self, '_value', None)
527 def value(self, new_profile):
529 new_profile = new_profile.upper()
530 if new_profile in self.profiles:
531 self._value = new_profile
533 raise CLIInvalidArgument(
534 'Invalid firewall profile %s' % new_profile,
535 details=['Valid values: %s' % ', '.join(self.profiles)])
538 @command(server_cmds)
539 class server_modify(_init_cyclades, _optional_output_cmd):
540 """Modify attributes of a virtual server"""
543 server_name=ValueArgument('The new name', '--name'),
544 flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
545 firewall_profile=FirewallProfileArgument(
546 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
548 metadata_to_set=KeyValueArgument(
549 'Set metadata in key=value form (can be repeated)',
551 metadata_to_delete=RepeatableArgument(
552 'Delete metadata by key (can be repeated)', '--metadata-del')
555 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
556 'metadata_to_delete']
559 @errors.cyclades.connection
560 @errors.cyclades.server_id
561 def _run(self, server_id):
562 if self['server_name']:
563 self.client.update_server_name((server_id), self['server_name'])
564 if self['flavor_id']:
565 self.client.resize_server(server_id, self['flavor_id'])
566 if self['firewall_profile']:
567 self.client.set_firewall_profile(
568 server_id=server_id, profile=self['firewall_profile'])
569 if self['metadata_to_set']:
570 self.client.update_server_metadata(
571 server_id, **self['metadata_to_set'])
572 for key in (self['metadata_to_delete'] or []):
573 errors.cyclades.metadata(
574 self.client.delete_server_metadata)(server_id, key=key)
575 if self['with_output']:
576 self._optional_output(self.client.get_server_details(server_id))
578 def main(self, server_id):
579 super(self.__class__, self)._run()
580 self._run(server_id=server_id)
583 @command(server_cmds)
584 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
585 """Delete a virtual server"""
588 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
589 cluster=FlagArgument(
590 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
591 'prefix. In that case, the prefix replaces the server id',
595 def _server_ids(self, server_var):
597 return [s['id'] for s in self.client.list_servers() if (
598 s['name'].startswith(server_var))]
600 @errors.cyclades.server_id
601 def _check_server_id(self, server_id):
604 return [_check_server_id(self, server_id=server_var), ]
607 @errors.cyclades.connection
608 def _run(self, server_var):
609 for server_id in self._server_ids(server_var):
611 details = self.client.get_server_details(server_id)
612 status = details['status']
614 r = self.client.delete_server(server_id)
615 self._optional_output(r)
618 self._wait(server_id, status)
620 def main(self, server_id_or_cluster_prefix):
621 super(self.__class__, self)._run()
622 self._run(server_id_or_cluster_prefix)
625 @command(server_cmds)
626 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
627 """Reboot a virtual server"""
631 'perform a hard reboot (deprecated)', ('-f', '--force')),
632 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
633 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
637 @errors.cyclades.connection
638 @errors.cyclades.server_id
639 def _run(self, server_id):
640 hard_reboot = self['hard']
643 'WARNING: -f/--force will be deprecated in version 0.12\n'
644 '\tIn the future, please use --type=hard instead')
646 if self['type'].lower() in ('soft', ):
648 elif self['type'].lower() in ('hard', ):
651 raise CLISyntaxError(
652 'Invalid reboot type %s' % self['type'],
653 importance=2, details=[
654 '--type values are either SOFT (default) or HARD'])
656 r = self.client.reboot_server(int(server_id), hard_reboot)
657 self._optional_output(r)
660 self._wait(server_id, 'REBOOT')
662 def main(self, server_id):
663 super(self.__class__, self)._run()
664 self._run(server_id=server_id)
667 @command(server_cmds)
668 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
669 """Start an existing virtual server"""
672 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
676 @errors.cyclades.connection
677 @errors.cyclades.server_id
678 def _run(self, server_id):
681 details = self.client.get_server_details(server_id)
682 status = details['status']
683 if status in ('ACTIVE', ):
686 r = self.client.start_server(int(server_id))
687 self._optional_output(r)
690 self._wait(server_id, status)
692 def main(self, server_id):
693 super(self.__class__, self)._run()
694 self._run(server_id=server_id)
697 @command(server_cmds)
698 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
699 """Shutdown an active virtual server"""
702 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
706 @errors.cyclades.connection
707 @errors.cyclades.server_id
708 def _run(self, server_id):
711 details = self.client.get_server_details(server_id)
712 status = details['status']
713 if status in ('STOPPED', ):
716 r = self.client.shutdown_server(int(server_id))
717 self._optional_output(r)
720 self._wait(server_id, status)
722 def main(self, server_id):
723 super(self.__class__, self)._run()
724 self._run(server_id=server_id)
727 @command(server_cmds)
728 class server_nics(_init_cyclades):
729 """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
731 def main(self, *args):
732 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
734 ' [kamaki] server info <SERVER_ID> --nics'])
737 @command(server_cmds)
738 class server_console(_init_cyclades, _optional_json):
739 """Create a VMC console and show connection information"""
742 @errors.cyclades.connection
743 @errors.cyclades.server_id
744 def _run(self, server_id):
745 self.error('The following credentials will be invalidated shortly')
747 self.client.get_server_console(server_id), self.print_dict)
749 def main(self, server_id):
750 super(self.__class__, self)._run()
751 self._run(server_id=server_id)
754 @command(server_cmds)
755 class server_rename(_init_cyclades, _optional_json):
756 """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
758 def main(self, *args):
759 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
761 ' [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
764 @command(server_cmds)
765 class server_stats(_init_cyclades, _optional_json):
766 """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
768 def main(self, *args):
769 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
771 ' [kamaki] server info <SERVER_ID> --stats'])
774 @command(server_cmds)
775 class server_wait(_init_cyclades, _server_wait):
776 """Wait for server to finish (BUILD, STOPPED, REBOOT, ACTIVE)"""
780 'Wait limit in seconds (default: 60)', '--timeout', default=60)
784 @errors.cyclades.connection
785 @errors.cyclades.server_id
786 def _run(self, server_id, current_status):
787 r = self.client.get_server_details(server_id)
788 if r['status'].lower() == current_status.lower():
789 self._wait(server_id, current_status, timeout=self['timeout'])
792 'Server %s: Cannot wait for status %s, '
793 'status is already %s' % (
794 server_id, current_status, r['status']))
796 def main(self, server_id, current_status='BUILD'):
797 super(self.__class__, self)._run()
798 self._run(server_id=server_id, current_status=current_status)
801 @command(flavor_cmds)
802 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
803 """List available hardware flavors"""
805 PERMANENTS = ('id', 'name')
808 detail=FlagArgument('show detailed output', ('-l', '--details')),
809 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
811 'output results in pages (-n to set items per page, default 10)',
813 enum=FlagArgument('Enumerate results', '--enumerate'),
814 ram=ValueArgument('filter by ram', ('--ram')),
815 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
816 disk=ValueArgument('filter by disk size in GB', ('--disk')),
817 disk_template=ValueArgument(
818 'filter by disk_templace', ('--disk-template'))
821 def _apply_common_filters(self, flavors):
822 common_filters = dict()
824 common_filters['ram'] = self['ram']
826 common_filters['vcpus'] = self['vcpus']
828 common_filters['disk'] = self['disk']
829 if self['disk_template']:
830 common_filters['SNF:disk_template'] = self['disk_template']
831 return filter_dicts_by_dict(flavors, common_filters)
834 @errors.cyclades.connection
836 withcommons = self['ram'] or self['vcpus'] or (
837 self['disk'] or self['disk_template'])
838 detail = self['detail'] or withcommons
839 flavors = self.client.list_flavors(detail)
840 flavors = self._filter_by_name(flavors)
841 flavors = self._filter_by_id(flavors)
843 flavors = self._apply_common_filters(flavors)
844 if not (self['detail'] or (
845 self['json_output'] or self['output_format'])):
846 remove_from_items(flavors, 'links')
847 if detail and not self['detail']:
849 for key in set(flv).difference(self.PERMANENTS):
851 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
854 with_redundancy=self['detail'], with_enumeration=self['enum'],
857 pager(kwargs['out'].getvalue())
860 super(self.__class__, self)._run()
864 @command(flavor_cmds)
865 class flavor_info(_init_cyclades, _optional_json):
866 """Detailed information on a hardware flavor
867 To get a list of available flavors and flavor ids, try /flavor list
871 @errors.cyclades.connection
872 @errors.cyclades.flavor_id
873 def _run(self, flavor_id):
875 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
877 def main(self, flavor_id):
878 super(self.__class__, self)._run()
879 self._run(flavor_id=flavor_id)
882 def _add_name(self, net):
883 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
885 uuids.append(user_id)
887 uuids.append(tenant_id)
889 usernames = self._uuids2usernames(uuids)
891 net['user_id'] += ' (%s)' % usernames[user_id]
893 net['tenant_id'] += ' (%s)' % usernames[tenant_id]