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, StatusArgument)
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']
72 server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')
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 _init_cyclades(_command_init):
117 def _run(self, service='compute'):
118 if getattr(self, 'cloud', None):
119 base_url = self._custom_url(service) or self._custom_url(
122 token = self._custom_token(service) or self._custom_token(
123 'cyclades') or self.config.get_cloud('token')
124 self.client = CycladesClient(base_url=base_url, token=token)
127 self.cloud = 'default'
128 if getattr(self, 'auth_base', False):
129 cyclades_endpoints = self.auth_base.get_service_endpoints(
130 self._custom_type('cyclades') or 'compute',
131 self._custom_version('cyclades') or '')
132 base_url = cyclades_endpoints['publicURL']
133 token = self.auth_base.token
134 self.client = CycladesClient(base_url=base_url, token=token)
136 raise CLIBaseUrlError(service='cyclades')
142 @command(server_cmds)
143 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
144 """List virtual servers accessible by user
145 Use filtering arguments (e.g., --name-like) to manage long server lists
148 PERMANENTS = ('id', 'name')
151 detail=FlagArgument('show detailed output', ('-l', '--details')),
153 'show only items since date (\' d/m/Y H:M:S \')',
156 'limit number of listed virtual servers', ('-n', '--number')),
158 'output results in pages (-n to set items per page, default 10)',
160 enum=FlagArgument('Enumerate results', '--enumerate'),
161 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
162 image_id=ValueArgument('filter by image id', ('--image-id')),
163 user_id=ValueArgument('filter by user id', ('--user-id')),
164 user_name=ValueArgument('filter by user name', ('--user-name')),
165 status=ValueArgument(
166 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
168 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
169 meta_like=KeyValueArgument(
170 'print only if in key=value, the value is part of actual value',
171 ('--metadata-like')),
174 def _add_user_name(self, servers):
175 uuids = self._uuids2usernames(list(set(
176 [srv['user_id'] for srv in servers] +
177 [srv['tenant_id'] for srv in servers])))
179 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
180 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
183 def _apply_common_filters(self, servers):
184 common_filters = dict()
186 common_filters['status'] = self['status']
187 if self['user_id'] or self['user_name']:
188 uuid = self['user_id'] or self._username2uuid(self['user_name'])
189 common_filters['user_id'] = uuid
190 return filter_dicts_by_dict(servers, common_filters)
192 def _filter_by_image(self, servers):
193 iid = self['image_id']
194 return [srv for srv in servers if srv['image']['id'] == iid]
196 def _filter_by_flavor(self, servers):
197 fid = self['flavor_id']
198 return [srv for srv in servers if (
199 '%s' % srv['image']['id'] == '%s' % fid)]
201 def _filter_by_metadata(self, servers):
204 if not 'metadata' in srv:
206 meta = [dict(srv['metadata'])]
208 meta = filter_dicts_by_dict(meta, self['meta'])
209 if meta and self['meta_like']:
210 meta = filter_dicts_by_dict(
211 meta, self['meta_like'], exact_match=False)
213 new_servers.append(srv)
217 @errors.cyclades.connection
218 @errors.cyclades.date
220 withimage = bool(self['image_id'])
221 withflavor = bool(self['flavor_id'])
222 withmeta = bool(self['meta'] or self['meta_like'])
224 self['status'] or self['user_id'] or self['user_name'])
225 detail = self['detail'] or (
226 withimage or withflavor or withmeta or withcommons)
227 servers = self.client.list_servers(detail, self['since'])
229 servers = self._filter_by_name(servers)
230 servers = self._filter_by_id(servers)
231 servers = self._apply_common_filters(servers)
233 servers = self._filter_by_image(servers)
235 servers = self._filter_by_flavor(servers)
237 servers = self._filter_by_metadata(servers)
239 if self['detail'] and not (
240 self['json_output'] or self['output_format']):
241 servers = self._add_user_name(servers)
242 elif not (self['detail'] or (
243 self['json_output'] or self['output_format'])):
244 remove_from_items(servers, 'links')
245 if detail and not self['detail']:
247 for key in set(srv).difference(self.PERMANENTS):
249 kwargs = dict(with_enumeration=self['enum'])
251 kwargs['out'] = StringIO()
254 servers = servers[:self['limit']]
255 self._print(servers, **kwargs)
257 pager(kwargs['out'].getvalue())
260 super(self.__class__, self)._run()
264 @command(server_cmds)
265 class server_info(_init_cyclades, _optional_json):
266 """Detailed information on a Virtual Machine"""
270 'Show only the network interfaces of this virtual server',
272 network_id=ValueArgument(
273 'Show the connection details to that network', '--network-id'),
274 stats=FlagArgument('Get URLs for server statistics', '--stats'),
275 diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
279 @errors.cyclades.connection
280 @errors.cyclades.server_id
281 def _run(self, server_id):
284 self.client.get_server_nics(server_id), self.print_dict)
285 elif self['network_id']:
287 self.client.get_server_network_nics(
288 server_id, self['network_id']), self.print_dict)
291 self.client.get_server_stats(server_id), self.print_dict)
292 elif self['diagnostics']:
293 self._print(self.client.get_server_diagnostics(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 choose_one = ('nics', 'stats', 'diagnostics')
304 count = len([a for a in choose_one if self[a]])
306 raise CLIInvalidArgument('Invalid argument combination', details=[
307 'Arguments %s cannot be used simultaneously' % ', '.join(
308 [self.arguments[a].lvalue for a in choose_one])])
309 self._run(server_id=server_id)
312 class PersonalityArgument(KeyValueArgument):
315 ('local-path', 'contents'),
316 ('server-path', 'path'),
323 return getattr(self, '_value', [])
326 def value(self, newvalue):
327 if newvalue == self.default:
329 self._value, input_dict = [], {}
330 for i, terms in enumerate(newvalue):
331 termlist = terms.split(',')
332 if len(termlist) > len(self.terms):
333 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
334 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
336 for k, v in self.terms:
338 for item in termlist:
339 if item.lower().startswith(prefix):
340 input_dict[k] = item[len(k) + 1:]
344 termlist.remove(item)
347 path = input_dict['local-path']
349 path = termlist.pop(0)
351 raise CLIInvalidArgument(
352 '--personality: No local path specified',
353 details=howto_personality)
356 raise CLIInvalidArgument(
357 '--personality: File %s does not exist' % path,
358 details=howto_personality)
360 self._value.append(dict(path=path))
361 with open(expanduser(path)) as f:
362 self._value[i]['contents'] = b64encode(f.read())
363 for k, v in self.terms[1:]:
365 self._value[i][v] = input_dict[k]
368 self._value[i][v] = termlist.pop(0)
371 if k in ('mode', ) and self._value[i][v]:
373 self._value[i][v] = int(self._value[i][v], 8)
374 except ValueError as ve:
375 raise CLIInvalidArgument(
376 'Personality mode must be in octal', details=[
380 class NetworkArgument(RepeatableArgument):
381 """[id=]NETWORK_ID[,[ip=]IP]"""
385 return getattr(self, '_value', self.default)
388 def value(self, new_value):
389 for v in new_value or []:
390 part1, sep, part2 = v.partition(',')
392 if part1.startswith('id='):
393 netid = part1[len('id='):]
394 elif part1.startswith('ip='):
395 ip = part1[len('ip='):]
399 if (part2.startswith('id=') and netid) or (
400 part2.startswith('ip=') and ip):
401 raise CLIInvalidArgument(
402 'Invalid network argument %s' % v, details=[
403 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
404 if part2.startswith('id='):
405 netid = part2[len('id='):]
406 elif part2.startswith('ip='):
407 ip = part2[len('ip='):]
413 raise CLIInvalidArgument(
414 'Invalid network argument %s' % v, details=[
415 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
416 self._value = getattr(self, '_value', [])
417 self._value.append(dict(uuid=netid))
419 self._value[-1]['fixed_ip'] = ip
422 @command(server_cmds)
423 class server_create(_init_cyclades, _optional_json, _server_wait):
424 """Create a server (aka Virtual Machine)"""
427 server_name=ValueArgument('The name of the new server', '--name'),
428 flavor_id=IntArgument('The ID of the flavor', '--flavor-id'),
429 image_id=ValueArgument('The ID of the image', '--image-id'),
430 personality=PersonalityArgument(
431 (80 * ' ').join(howto_personality), ('-p', '--personality')),
432 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
433 cluster_size=IntArgument(
434 'Create a cluster of servers of this size. In this case, the name'
435 'parameter is the prefix of each server in the cluster (e.g.,'
438 max_threads=IntArgument(
439 'Max threads in cluster mode (default 1)', '--threads'),
440 network_configuration=NetworkArgument(
441 'Connect server to network: [id=]NETWORK_ID[,[ip=]IP] . '
442 'Use only NETWORK_ID for private networks. . '
443 'Use NETWORK_ID,[ip=]IP for networks with IP. . '
444 'Can be repeated, mutually exclussive with --no-network',
446 no_network=FlagArgument(
447 'Do not create any network NICs on the server. . '
448 'Mutually exclusive to --network . '
449 'If neither --network or --no-network are used, the default '
450 'network policy is applied. These policies are set on the cloud, '
451 'so kamaki is oblivious to them',
454 required = ('server_name', 'flavor_id', 'image_id')
456 @errors.cyclades.cluster_size
457 def _create_cluster(self, prefix, flavor_id, image_id, size):
458 networks = self['network_configuration'] or (
459 [] if self['no_network'] else None)
461 name='%s%s' % (prefix, i if size > 1 else ''),
464 personality=self['personality'],
465 networks=networks) for i in range(1, 1 + size)]
467 return [self.client.create_server(**servers[0])]
468 self.client.MAX_THREADS = int(self['max_threads'] or 1)
470 r = self.client.async_run(self.client.create_server, servers)
472 except Exception as e:
476 requested_names = [s['name'] for s in servers]
477 spawned_servers = [dict(
479 id=s['id']) for s in self.client.list_servers() if (
480 s['name'] in requested_names)]
481 self.error('Failed to build %s servers' % size)
482 self.error('Found %s matching servers:' % len(spawned_servers))
483 self._print(spawned_servers, out=self._err)
484 self.error('Check if any of these servers should be removed\n')
485 except Exception as ne:
486 self.error('Error (%s) while notifying about errors' % ne)
491 @errors.cyclades.connection
493 @errors.cyclades.flavor_id
494 def _run(self, name, flavor_id, image_id):
495 for r in self._create_cluster(
496 name, flavor_id, image_id, size=self['cluster_size'] or 1):
498 self.error('Create %s: server response was %s' % (name, r))
500 usernames = self._uuids2usernames(
501 [r['user_id'], r['tenant_id']])
502 r['user_id'] += ' (%s)' % usernames[r['user_id']]
503 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
504 self._print(r, self.print_dict)
506 self._wait(r['id'], r['status'] or 'BUILD')
510 super(self.__class__, self)._run()
511 if self['no_network'] and self['network_configuration']:
512 raise CLIInvalidArgument(
513 'Invalid argument compination', importance=2, details=[
514 'Arguments %s and %s are mutually exclusive' % (
515 self.arguments['no_network'].lvalue,
516 self.arguments['network_configuration'].lvalue)])
518 name=self['server_name'],
519 flavor_id=self['flavor_id'],
520 image_id=self['image_id'])
523 class FirewallProfileArgument(ValueArgument):
525 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
529 return getattr(self, '_value', None)
532 def value(self, new_profile):
534 new_profile = new_profile.upper()
535 if new_profile in self.profiles:
536 self._value = new_profile
538 raise CLIInvalidArgument(
539 'Invalid firewall profile %s' % new_profile,
540 details=['Valid values: %s' % ', '.join(self.profiles)])
543 @command(server_cmds)
544 class server_modify(_init_cyclades, _optional_output_cmd):
545 """Modify attributes of a virtual server"""
548 server_name=ValueArgument('The new name', '--name'),
549 flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
550 firewall_profile=FirewallProfileArgument(
551 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
553 metadata_to_set=KeyValueArgument(
554 'Set metadata in key=value form (can be repeated)',
556 metadata_to_delete=RepeatableArgument(
557 'Delete metadata by key (can be repeated)', '--metadata-del')
560 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
561 'metadata_to_delete']
564 @errors.cyclades.connection
565 @errors.cyclades.server_id
566 def _run(self, server_id):
567 if self['server_name'] is not None:
568 self.client.update_server_name((server_id), self['server_name'])
569 if self['flavor_id']:
570 self.client.resize_server(server_id, self['flavor_id'])
571 if self['firewall_profile']:
572 self.client.set_firewall_profile(
573 server_id=server_id, profile=self['firewall_profile'])
574 if self['metadata_to_set']:
575 self.client.update_server_metadata(
576 server_id, **self['metadata_to_set'])
577 for key in (self['metadata_to_delete'] or []):
578 errors.cyclades.metadata(
579 self.client.delete_server_metadata)(server_id, key=key)
580 if self['with_output']:
581 self._optional_output(self.client.get_server_details(server_id))
583 def main(self, server_id):
584 super(self.__class__, self)._run()
585 self._run(server_id=server_id)
588 @command(server_cmds)
589 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
590 """Delete a virtual server"""
593 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
594 cluster=FlagArgument(
595 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
596 'prefix. In that case, the prefix replaces the server id',
600 def _server_ids(self, server_var):
602 return [s['id'] for s in self.client.list_servers() if (
603 s['name'].startswith(server_var))]
605 @errors.cyclades.server_id
606 def _check_server_id(self, server_id):
609 return [_check_server_id(self, server_id=server_var), ]
612 @errors.cyclades.connection
613 def _run(self, server_var):
614 for server_id in self._server_ids(server_var):
616 details = self.client.get_server_details(server_id)
617 status = details['status']
619 r = self.client.delete_server(server_id)
620 self._optional_output(r)
623 self._wait(server_id, status)
625 def main(self, server_id_or_cluster_prefix):
626 super(self.__class__, self)._run()
627 self._run(server_id_or_cluster_prefix)
630 @command(server_cmds)
631 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
632 """Reboot a virtual server"""
636 'perform a hard reboot (deprecated)', ('-f', '--force')),
637 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
638 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
642 @errors.cyclades.connection
643 @errors.cyclades.server_id
644 def _run(self, server_id):
645 hard_reboot = self['hard']
648 'WARNING: -f/--force will be deprecated in version 0.12\n'
649 '\tIn the future, please use --type=hard instead')
651 if self['type'].lower() in ('soft', ):
653 elif self['type'].lower() in ('hard', ):
656 raise CLISyntaxError(
657 'Invalid reboot type %s' % self['type'],
658 importance=2, details=[
659 '--type values are either SOFT (default) or HARD'])
661 r = self.client.reboot_server(int(server_id), hard_reboot)
662 self._optional_output(r)
665 self._wait(server_id, 'REBOOT')
667 def main(self, server_id):
668 super(self.__class__, self)._run()
669 self._run(server_id=server_id)
672 @command(server_cmds)
673 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
674 """Start an existing virtual server"""
677 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
681 @errors.cyclades.connection
682 @errors.cyclades.server_id
683 def _run(self, server_id):
686 details = self.client.get_server_details(server_id)
687 status = details['status']
688 if status in ('ACTIVE', ):
691 r = self.client.start_server(int(server_id))
692 self._optional_output(r)
695 self._wait(server_id, status)
697 def main(self, server_id):
698 super(self.__class__, self)._run()
699 self._run(server_id=server_id)
702 @command(server_cmds)
703 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
704 """Shutdown an active virtual server"""
707 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
711 @errors.cyclades.connection
712 @errors.cyclades.server_id
713 def _run(self, server_id):
716 details = self.client.get_server_details(server_id)
717 status = details['status']
718 if status in ('STOPPED', ):
721 r = self.client.shutdown_server(int(server_id))
722 self._optional_output(r)
725 self._wait(server_id, status)
727 def main(self, server_id):
728 super(self.__class__, self)._run()
729 self._run(server_id=server_id)
732 @command(server_cmds)
733 class server_nics(_init_cyclades):
734 """DEPRECATED, use: [kamaki] server info SERVER_ID --nics"""
736 def main(self, *args):
737 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
739 ' [kamaki] server info <SERVER_ID> --nics'])
742 @command(server_cmds)
743 class server_console(_init_cyclades, _optional_json):
744 """Create a VMC console and show connection information"""
747 @errors.cyclades.connection
748 @errors.cyclades.server_id
749 def _run(self, server_id):
750 self.error('The following credentials will be invalidated shortly')
752 self.client.get_server_console(server_id), self.print_dict)
754 def main(self, server_id):
755 super(self.__class__, self)._run()
756 self._run(server_id=server_id)
759 @command(server_cmds)
760 class server_rename(_init_cyclades, _optional_json):
761 """DEPRECATED, use: [kamaki] server modify SERVER_ID --name=NEW_NAME"""
763 def main(self, *args):
764 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
766 ' [kamaki] server modify <SERVER_ID> --name=NEW_NAME'])
769 @command(server_cmds)
770 class server_stats(_init_cyclades, _optional_json):
771 """DEPRECATED, use: [kamaki] server info SERVER_ID --stats"""
773 def main(self, *args):
774 raiseCLIError('DEPRECATED since v0.12', importance=3, details=[
776 ' [kamaki] server info <SERVER_ID> --stats'])
779 @command(server_cmds)
780 class server_wait(_init_cyclades, _server_wait):
781 """Wait for server to change its status (default: BUILD)"""
785 'Wait limit in seconds (default: 60)', '--timeout', default=60),
786 server_status=StatusArgument(
787 'Status to wait for (%s, default: %s)' % (
788 ', '.join(server_states), server_states[0]),
790 valid_states=server_states)
794 @errors.cyclades.connection
795 @errors.cyclades.server_id
796 def _run(self, server_id, current_status):
797 r = self.client.get_server_details(server_id)
798 if r['status'].lower() == current_status.lower():
799 self._wait(server_id, current_status, timeout=self['timeout'])
802 'Server %s: Cannot wait for status %s, '
803 'status is already %s' % (
804 server_id, current_status, r['status']))
806 def main(self, server_id):
807 super(self.__class__, self)._run()
809 server_id=server_id, current_status=self['server_status'] or '')
812 @command(flavor_cmds)
813 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
814 """List available hardware flavors"""
816 PERMANENTS = ('id', 'name')
819 detail=FlagArgument('show detailed output', ('-l', '--details')),
820 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
822 'output results in pages (-n to set items per page, default 10)',
824 enum=FlagArgument('Enumerate results', '--enumerate'),
825 ram=ValueArgument('filter by ram', ('--ram')),
826 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
827 disk=ValueArgument('filter by disk size in GB', ('--disk')),
828 disk_template=ValueArgument(
829 'filter by disk_templace', ('--disk-template'))
832 def _apply_common_filters(self, flavors):
833 common_filters = dict()
835 common_filters['ram'] = self['ram']
837 common_filters['vcpus'] = self['vcpus']
839 common_filters['disk'] = self['disk']
840 if self['disk_template']:
841 common_filters['SNF:disk_template'] = self['disk_template']
842 return filter_dicts_by_dict(flavors, common_filters)
845 @errors.cyclades.connection
847 withcommons = self['ram'] or self['vcpus'] or (
848 self['disk'] or self['disk_template'])
849 detail = self['detail'] or withcommons
850 flavors = self.client.list_flavors(detail)
851 flavors = self._filter_by_name(flavors)
852 flavors = self._filter_by_id(flavors)
854 flavors = self._apply_common_filters(flavors)
855 if not (self['detail'] or (
856 self['json_output'] or self['output_format'])):
857 remove_from_items(flavors, 'links')
858 if detail and not self['detail']:
860 for key in set(flv).difference(self.PERMANENTS):
862 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
865 with_redundancy=self['detail'], with_enumeration=self['enum'],
868 pager(kwargs['out'].getvalue())
871 super(self.__class__, self)._run()
875 @command(flavor_cmds)
876 class flavor_info(_init_cyclades, _optional_json):
877 """Detailed information on a hardware flavor
878 To get a list of available flavors and flavor ids, try /flavor list
882 @errors.cyclades.connection
883 @errors.cyclades.flavor_id
884 def _run(self, flavor_id):
886 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
888 def main(self, flavor_id):
889 super(self.__class__, self)._run()
890 self._run(flavor_id=flavor_id)
893 def _add_name(self, net):
894 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
896 uuids.append(user_id)
898 uuids.append(tenant_id)
900 usernames = self._uuids2usernames(uuids)
902 net['user_id'] += ' (%s)' % usernames[user_id]
904 net['tenant_id'] += ' (%s)' % usernames[tenant_id]