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.
35 from base64 import b64encode
36 from os.path import exists, expanduser
37 from io import StringIO
38 from pydoc import pager
39 from json import dumps
41 from kamaki.cli import command
42 from kamaki.cli.command_tree import CommandTree
43 from kamaki.cli.utils import remove_from_items, filter_dicts_by_dict
44 from kamaki.cli.errors import (
45 raiseCLIError, CLISyntaxError, CLIBaseUrlError, CLIInvalidArgument)
46 from kamaki.clients.cyclades import CycladesClient
47 from kamaki.cli.argument import (
48 FlagArgument, ValueArgument, KeyValueArgument, RepeatableArgument,
49 ProgressBarArgument, DateArgument, IntArgument, StatusArgument)
50 from kamaki.cli.commands import (
51 _command_init, errors, addLogSettings, dataModification,
52 _optional_output_cmd, _optional_json, _name_filter, _id_filter)
55 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
56 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
57 _commands = [server_cmds, flavor_cmds]
60 about_authentication = '\nUser Authentication:\
61 \n* to check authentication: /user authenticate\
62 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
65 'Defines a file to be injected to virtual servers file system.',
66 'syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
67 ' [local-path=]PATH: local file to be injected (relative or absolute)',
68 ' [server-path=]SERVER_PATH: destination location inside server Image',
69 ' [owner=]OWNER: virtual servers user id for the remote file',
70 ' [group=]GROUP: virtual servers group id or name for the remote file',
71 ' [mode=]MODE: permission in octal (e.g., 0777)',
72 'e.g., -p /tmp/my.file,owner=root,mode=0777']
74 server_states = ('BUILD', 'ACTIVE', 'STOPPED', 'REBOOT')
77 class _service_wait(object):
79 wait_arguments = dict(
80 progress_bar=ProgressBarArgument(
81 'do not show progress bar', ('-N', '--no-progress-bar'), False)
85 self, service, service_id, status_method, current_status,
86 countdown=True, timeout=60):
87 (progress_bar, wait_cb) = self._safe_progress_bar(
88 '%s %s: status is still %s' % (
89 service, service_id, current_status),
90 countdown=countdown, timeout=timeout)
93 new_mode = status_method(
94 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
96 self.error('%s %s: status is now %s' % (
97 service, service_id, new_mode))
99 self.error('%s %s: status is still %s' % (
100 service, service_id, current_status))
101 except KeyboardInterrupt:
102 self.error('\n- canceled')
104 self._safe_progress_bar_finish(progress_bar)
107 class _server_wait(_service_wait):
109 def _wait(self, server_id, current_status, timeout=60):
110 super(_server_wait, self)._wait(
111 'Server', server_id, self.client.wait_server, current_status,
112 countdown=(current_status not in ('BUILD', )),
113 timeout=timeout if current_status not in ('BUILD', ) else 100)
116 class _init_cyclades(_command_init):
119 def _run(self, service='compute'):
120 if getattr(self, 'cloud', None):
121 base_url = self._custom_url(service) or self._custom_url(
124 token = self._custom_token(service) or self._custom_token(
125 'cyclades') or self.config.get_cloud('token')
126 self.client = CycladesClient(base_url=base_url, token=token)
129 self.cloud = 'default'
130 if getattr(self, 'auth_base', False):
131 cyclades_endpoints = self.auth_base.get_service_endpoints(
132 self._custom_type('cyclades') or 'compute',
133 self._custom_version('cyclades') or '')
134 base_url = cyclades_endpoints['publicURL']
135 token = self.auth_base.token
136 self.client = CycladesClient(base_url=base_url, token=token)
138 raise CLIBaseUrlError(service='cyclades')
141 def _restruct_server_info(self, vm):
146 img.pop('links', None)
147 img['name'] = self.client.get_image_details(img['id'])['name']
152 flv.pop('links', None)
153 flv['name'] = self.client.get_flavor_details(flv['id'])['name']
156 vm['ports'] = vm.pop('attachments', dict())
157 for port in vm['ports']:
158 netid = port.get('network_id')
159 for k in vm['addresses'].get(netid, []):
161 k.pop('version', None)
163 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
164 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
165 for key in ('addresses', 'tenant_id', 'links'):
173 @command(server_cmds)
174 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
175 """List virtual servers accessible by user
176 Use filtering arguments (e.g., --name-like) to manage long server lists
179 PERMANENTS = ('id', 'name')
182 detail=FlagArgument('show detailed output', ('-l', '--details')),
184 'show only items since date (\' d/m/Y H:M:S \')',
187 'limit number of listed virtual servers', ('-n', '--number')),
189 'output results in pages (-n to set items per page, default 10)',
191 enum=FlagArgument('Enumerate results', '--enumerate'),
192 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
193 image_id=ValueArgument('filter by image id', ('--image-id')),
194 user_id=ValueArgument('filter by user id', ('--user-id')),
195 user_name=ValueArgument('filter by user name', ('--user-name')),
196 status=ValueArgument(
197 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
199 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
200 meta_like=KeyValueArgument(
201 'print only if in key=value, the value is part of actual value',
202 ('--metadata-like')),
205 def _add_user_name(self, servers):
206 uuids = self._uuids2usernames(list(set(
207 [srv['user_id'] for srv in servers] +
208 [srv['tenant_id'] for srv in servers])))
210 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
211 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
214 def _apply_common_filters(self, servers):
215 common_filters = dict()
217 common_filters['status'] = self['status']
218 if self['user_id'] or self['user_name']:
219 uuid = self['user_id'] or self._username2uuid(self['user_name'])
220 common_filters['user_id'] = uuid
221 return filter_dicts_by_dict(servers, common_filters)
223 def _filter_by_image(self, servers):
224 iid = self['image_id']
225 return [srv for srv in servers if srv['image']['id'] == iid]
227 def _filter_by_flavor(self, servers):
228 fid = self['flavor_id']
229 return [srv for srv in servers if (
230 '%s' % srv['image']['id'] == '%s' % fid)]
232 def _filter_by_metadata(self, servers):
235 if not 'metadata' in srv:
237 meta = [dict(srv['metadata'])]
239 meta = filter_dicts_by_dict(meta, self['meta'])
240 if meta and self['meta_like']:
241 meta = filter_dicts_by_dict(
242 meta, self['meta_like'], exact_match=False)
244 new_servers.append(srv)
248 @errors.cyclades.connection
249 @errors.cyclades.date
251 withimage = bool(self['image_id'])
252 withflavor = bool(self['flavor_id'])
253 withmeta = bool(self['meta'] or self['meta_like'])
255 self['status'] or self['user_id'] or self['user_name'])
256 detail = self['detail'] or (
257 withimage or withflavor or withmeta or withcommons)
258 servers = self.client.list_servers(detail, self['since'])
260 servers = self._filter_by_name(servers)
261 servers = self._filter_by_id(servers)
262 servers = self._apply_common_filters(servers)
264 servers = self._filter_by_image(servers)
266 servers = self._filter_by_flavor(servers)
268 servers = self._filter_by_metadata(servers)
270 if detail and self['detail']:
271 # servers = [self._restruct_server_info(vm) for vm in servers]
275 for key in set(srv).difference(self.PERMANENTS):
278 kwargs = dict(with_enumeration=self['enum'])
280 codecinfo = codecs.lookup('utf-8')
281 kwargs['out'] = codecs.StreamReaderWriter(
282 cStringIO.StringIO(),
283 codecinfo.streamreader,
284 codecinfo.streamwriter)
287 servers = servers[:self['limit']]
288 self._print(servers, **kwargs)
290 pager(kwargs['out'].getvalue())
293 super(self.__class__, self)._run()
297 @command(server_cmds)
298 class server_info(_init_cyclades, _optional_json):
299 """Detailed information on a Virtual Machine"""
303 'Show only the network interfaces of this virtual server',
305 stats=FlagArgument('Get URLs for server statistics', '--stats'),
306 diagnostics=FlagArgument('Diagnostic information', '--diagnostics')
310 @errors.cyclades.connection
311 @errors.cyclades.server_id
312 def _run(self, server_id):
315 self.client.get_server_nics(server_id), self.print_dict)
318 self.client.get_server_stats(server_id), self.print_dict)
319 elif self['diagnostics']:
320 self._print(self.client.get_server_diagnostics(server_id))
322 vm = self.client.get_server_details(server_id)
323 # self._print(self._restruct_server_info(vm), self.print_dict)
324 self._print(vm, self.print_dict)
326 def main(self, server_id):
327 super(self.__class__, self)._run()
328 choose_one = ('nics', 'stats', 'diagnostics')
329 count = len([a for a in choose_one if self[a]])
331 raise CLIInvalidArgument('Invalid argument combination', details=[
332 'Arguments %s cannot be used simultaneously' % ', '.join(
333 [self.arguments[a].lvalue for a in choose_one])])
334 self._run(server_id=server_id)
337 class PersonalityArgument(KeyValueArgument):
340 ('local-path', 'contents'),
341 ('server-path', 'path'),
348 return getattr(self, '_value', [])
351 def value(self, newvalue):
352 if newvalue == self.default:
354 self._value, input_dict = [], {}
355 for i, terms in enumerate(newvalue):
356 termlist = terms.split(',')
357 if len(termlist) > len(self.terms):
358 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
359 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
361 for k, v in self.terms:
363 for item in termlist:
364 if item.lower().startswith(prefix):
365 input_dict[k] = item[len(k) + 1:]
369 termlist.remove(item)
372 path = input_dict['local-path']
374 path = termlist.pop(0)
376 raise CLIInvalidArgument(
377 '--personality: No local path specified',
378 details=howto_personality)
381 raise CLIInvalidArgument(
382 '--personality: File %s does not exist' % path,
383 details=howto_personality)
385 self._value.append(dict(path=path))
386 with open(expanduser(path)) as f:
387 self._value[i]['contents'] = b64encode(f.read())
388 for k, v in self.terms[1:]:
390 self._value[i][v] = input_dict[k]
393 self._value[i][v] = termlist.pop(0)
396 if k in ('mode', ) and self._value[i][v]:
398 self._value[i][v] = int(self._value[i][v], 8)
399 except ValueError as ve:
400 raise CLIInvalidArgument(
401 'Personality mode must be in octal', details=[
405 class NetworkArgument(RepeatableArgument):
406 """[id=]NETWORK_ID[,[ip=]IP]"""
410 return getattr(self, '_value', self.default)
413 def value(self, new_value):
414 for v in new_value or []:
415 part1, sep, part2 = v.partition(',')
417 if part1.startswith('id='):
418 netid = part1[len('id='):]
419 elif part1.startswith('ip='):
420 ip = part1[len('ip='):]
424 if (part2.startswith('id=') and netid) or (
425 part2.startswith('ip=') and ip):
426 raise CLIInvalidArgument(
427 'Invalid network argument %s' % v, details=[
428 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
429 if part2.startswith('id='):
430 netid = part2[len('id='):]
431 elif part2.startswith('ip='):
432 ip = part2[len('ip='):]
438 raise CLIInvalidArgument(
439 'Invalid network argument %s' % v, details=[
440 'Valid format: [id=]NETWORK_ID[,[ip=]IP]'])
441 self._value = getattr(self, '_value', [])
442 self._value.append(dict(uuid=netid))
444 self._value[-1]['fixed_ip'] = ip
447 @command(server_cmds)
448 class server_create(_init_cyclades, _optional_json, _server_wait):
449 """Create a server (aka Virtual Machine)"""
452 server_name=ValueArgument('The name of the new server', '--name'),
453 flavor_id=IntArgument('The ID of the flavor', '--flavor-id'),
454 image_id=ValueArgument('The ID of the image', '--image-id'),
455 personality=PersonalityArgument(
456 (80 * ' ').join(howto_personality), ('-p', '--personality')),
457 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
458 cluster_size=IntArgument(
459 'Create a cluster of servers of this size. In this case, the name'
460 'parameter is the prefix of each server in the cluster (e.g.,'
463 max_threads=IntArgument(
464 'Max threads in cluster mode (default 1)', '--threads'),
465 network_configuration=NetworkArgument(
466 'Connect server to network: [id=]NETWORK_ID[,[ip=]IP] . '
467 'Use only NETWORK_ID for private networks. . '
468 'Use NETWORK_ID,[ip=]IP for networks with IP. . '
469 'Can be repeated, mutually exclussive with --no-network',
471 no_network=FlagArgument(
472 'Do not create any network NICs on the server. . '
473 'Mutually exclusive to --network . '
474 'If neither --network or --no-network are used, the default '
475 'network policy is applied. These policies are set on the cloud, '
476 'so kamaki is oblivious to them',
479 required = ('server_name', 'flavor_id', 'image_id')
481 @errors.cyclades.cluster_size
482 def _create_cluster(self, prefix, flavor_id, image_id, size):
483 networks = self['network_configuration'] or (
484 [] if self['no_network'] else None)
486 name='%s%s' % (prefix, i if size > 1 else ''),
489 personality=self['personality'],
490 networks=networks) for i in range(1, 1 + size)]
492 return [self.client.create_server(**servers[0])]
493 self.client.MAX_THREADS = int(self['max_threads'] or 1)
495 r = self.client.async_run(self.client.create_server, servers)
497 except Exception as e:
501 requested_names = [s['name'] for s in servers]
502 spawned_servers = [dict(
504 id=s['id']) for s in self.client.list_servers() if (
505 s['name'] in requested_names)]
506 self.error('Failed to build %s servers' % size)
507 self.error('Found %s matching servers:' % len(spawned_servers))
508 self._print(spawned_servers, out=self._err)
509 self.error('Check if any of these servers should be removed\n')
510 except Exception as ne:
511 self.error('Error (%s) while notifying about errors' % ne)
516 @errors.cyclades.connection
518 @errors.cyclades.flavor_id
519 def _run(self, name, flavor_id, image_id):
520 for r in self._create_cluster(
521 name, flavor_id, image_id, size=self['cluster_size'] or 1):
523 self.error('Create %s: server response was %s' % (name, r))
525 # self._print(self._restruct_server_info(r), self.print_dict)
526 self._print(r, self.print_dict)
528 self._wait(r['id'], r['status'] or 'BUILD')
532 super(self.__class__, self)._run()
533 if self['no_network'] and self['network_configuration']:
534 raise CLIInvalidArgument(
535 'Invalid argument compination', importance=2, details=[
536 'Arguments %s and %s are mutually exclusive' % (
537 self.arguments['no_network'].lvalue,
538 self.arguments['network_configuration'].lvalue)])
540 name=self['server_name'],
541 flavor_id=self['flavor_id'],
542 image_id=self['image_id'])
545 class FirewallProfileArgument(ValueArgument):
547 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
551 return getattr(self, '_value', None)
554 def value(self, new_profile):
556 new_profile = new_profile.upper()
557 if new_profile in self.profiles:
558 self._value = new_profile
560 raise CLIInvalidArgument(
561 'Invalid firewall profile %s' % new_profile,
562 details=['Valid values: %s' % ', '.join(self.profiles)])
565 @command(server_cmds)
566 class server_modify(_init_cyclades, _optional_output_cmd):
567 """Modify attributes of a virtual server"""
570 server_name=ValueArgument('The new name', '--name'),
571 flavor_id=IntArgument('Resize (set another flavor)', '--flavor-id'),
572 firewall_profile=FirewallProfileArgument(
573 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
575 metadata_to_set=KeyValueArgument(
576 'Set metadata in key=value form (can be repeated)',
578 metadata_to_delete=RepeatableArgument(
579 'Delete metadata by key (can be repeated)', '--metadata-del'),
580 public_network_port_id=ValueArgument(
581 'Connection to set new firewall (* for all)', '--port-id'),
584 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
585 'metadata_to_delete']
587 def _set_firewall_profile(self, server_id):
588 vm = self._restruct_server_info(
589 self.client.get_server_details(server_id))
590 ports = [p for p in vm['ports'] if 'firewallProfile' in p]
591 pick_port = self.arguments['public_network_port_id']
593 ports = [p for p in ports if pick_port.value in (
594 '*', '%s' % p['id'])]
596 port_strings = ['Server %s ports to public networks:' % server_id]
598 port_strings.append(' %s' % p['id'])
599 for k in ('network_id', 'ipv4', 'ipv6', 'firewallProfile'):
602 port_strings.append('\t%s: %s' % (k, v))
604 'Multiple public connections on server %s' % (
605 server_id), details=port_strings + [
607 ' %s <port id>' % pick_port.lvalue,
609 ' %s *' % pick_port.lvalue, ])
613 'No *public* networks attached on server %s%s' % (
614 server_id, ' through port %s' % pp if pp else ''),
616 'To see all networks:',
617 ' kamaki network list',
618 'To connect to a network:',
619 ' kamaki network connect <net id> --device-id %s' % (
622 self.error('Set port %s firewall to %s' % (
623 port['id'], self['firewall_profile']))
624 self.client.set_firewall_profile(
626 profile=self['firewall_profile'],
630 @errors.cyclades.connection
631 @errors.cyclades.server_id
632 def _run(self, server_id):
633 if self['server_name'] is not None:
634 self.client.update_server_name((server_id), self['server_name'])
635 if self['flavor_id']:
636 self.client.resize_server(server_id, self['flavor_id'])
637 if self['firewall_profile']:
638 self._set_firewall_profile(server_id)
639 if self['metadata_to_set']:
640 self.client.update_server_metadata(
641 server_id, **self['metadata_to_set'])
642 for key in (self['metadata_to_delete'] or []):
643 errors.cyclades.metadata(
644 self.client.delete_server_metadata)(server_id, key=key)
645 if self['with_output']:
646 self._optional_output(self.client.get_server_details(server_id))
648 def main(self, server_id):
649 super(self.__class__, self)._run()
650 pnpid = self.arguments['public_network_port_id']
651 fp = self.arguments['firewall_profile']
652 if pnpid.value and not fp.value:
653 raise CLIInvalidArgument('Invalid argument compination', details=[
654 'Argument %s should always be combined with %s' % (
655 pnpid.lvalue, fp.lvalue)])
656 self._run(server_id=server_id)
659 @command(server_cmds)
660 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
661 """Delete a virtual server"""
664 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
665 cluster=FlagArgument(
666 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
667 'prefix. In that case, the prefix replaces the server id',
671 def _server_ids(self, server_var):
673 return [s['id'] for s in self.client.list_servers() if (
674 s['name'].startswith(server_var))]
676 @errors.cyclades.server_id
677 def _check_server_id(self, server_id):
680 return [_check_server_id(self, server_id=server_var), ]
683 @errors.cyclades.connection
684 def _run(self, server_var):
685 for server_id in self._server_ids(server_var):
687 details = self.client.get_server_details(server_id)
688 status = details['status']
690 r = self.client.delete_server(server_id)
691 self._optional_output(r)
694 self._wait(server_id, status)
696 def main(self, server_id_or_cluster_prefix):
697 super(self.__class__, self)._run()
698 self._run(server_id_or_cluster_prefix)
701 @command(server_cmds)
702 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
703 """Reboot a virtual server"""
707 'perform a hard reboot (deprecated)', ('-f', '--force')),
708 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
709 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
713 @errors.cyclades.connection
714 @errors.cyclades.server_id
715 def _run(self, server_id):
716 hard_reboot = self['hard']
719 'WARNING: -f/--force will be deprecated in version 0.12\n'
720 '\tIn the future, please use --type=hard instead')
722 if self['type'].lower() in ('soft', ):
724 elif self['type'].lower() in ('hard', ):
727 raise CLISyntaxError(
728 'Invalid reboot type %s' % self['type'],
729 importance=2, details=[
730 '--type values are either SOFT (default) or HARD'])
732 r = self.client.reboot_server(int(server_id), hard_reboot)
733 self._optional_output(r)
736 self._wait(server_id, 'REBOOT')
738 def main(self, server_id):
739 super(self.__class__, self)._run()
740 self._run(server_id=server_id)
743 @command(server_cmds)
744 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
745 """Start an existing virtual server"""
748 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
752 @errors.cyclades.connection
753 @errors.cyclades.server_id
754 def _run(self, server_id):
757 details = self.client.get_server_details(server_id)
758 status = details['status']
759 if status in ('ACTIVE', ):
762 r = self.client.start_server(int(server_id))
763 self._optional_output(r)
766 self._wait(server_id, status)
768 def main(self, server_id):
769 super(self.__class__, self)._run()
770 self._run(server_id=server_id)
773 @command(server_cmds)
774 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
775 """Shutdown an active virtual server"""
778 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
782 @errors.cyclades.connection
783 @errors.cyclades.server_id
784 def _run(self, server_id):
787 details = self.client.get_server_details(server_id)
788 status = details['status']
789 if status in ('STOPPED', ):
792 r = self.client.shutdown_server(int(server_id))
793 self._optional_output(r)
796 self._wait(server_id, status)
798 def main(self, server_id):
799 super(self.__class__, self)._run()
800 self._run(server_id=server_id)
803 @command(server_cmds)
804 class server_console(_init_cyclades, _optional_json):
805 """Create a VMC console and show connection information"""
808 @errors.cyclades.connection
809 @errors.cyclades.server_id
810 def _run(self, server_id):
811 self.error('The following credentials will be invalidated shortly')
813 self.client.get_server_console(server_id), self.print_dict)
815 def main(self, server_id):
816 super(self.__class__, self)._run()
817 self._run(server_id=server_id)
820 @command(server_cmds)
821 class server_wait(_init_cyclades, _server_wait):
822 """Wait for server to change its status (default: BUILD)"""
826 'Wait limit in seconds (default: 60)', '--timeout', default=60),
827 server_status=StatusArgument(
828 'Status to wait for (%s, default: %s)' % (
829 ', '.join(server_states), server_states[0]),
831 valid_states=server_states)
835 @errors.cyclades.connection
836 @errors.cyclades.server_id
837 def _run(self, server_id, current_status):
838 r = self.client.get_server_details(server_id)
839 if r['status'].lower() == current_status.lower():
840 self._wait(server_id, current_status, timeout=self['timeout'])
843 'Server %s: Cannot wait for status %s, '
844 'status is already %s' % (
845 server_id, current_status, r['status']))
847 def main(self, server_id):
848 super(self.__class__, self)._run()
851 current_status=self['server_status'] or 'BUILD')
854 @command(flavor_cmds)
855 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
856 """List available hardware flavors"""
858 PERMANENTS = ('id', 'name')
861 detail=FlagArgument('show detailed output', ('-l', '--details')),
862 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
864 'output results in pages (-n to set items per page, default 10)',
866 enum=FlagArgument('Enumerate results', '--enumerate'),
867 ram=ValueArgument('filter by ram', ('--ram')),
868 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
869 disk=ValueArgument('filter by disk size in GB', ('--disk')),
870 disk_template=ValueArgument(
871 'filter by disk_templace', ('--disk-template'))
874 def _apply_common_filters(self, flavors):
875 common_filters = dict()
877 common_filters['ram'] = self['ram']
879 common_filters['vcpus'] = self['vcpus']
881 common_filters['disk'] = self['disk']
882 if self['disk_template']:
883 common_filters['SNF:disk_template'] = self['disk_template']
884 return filter_dicts_by_dict(flavors, common_filters)
887 @errors.cyclades.connection
889 withcommons = self['ram'] or self['vcpus'] or (
890 self['disk'] or self['disk_template'])
891 detail = self['detail'] or withcommons
892 flavors = self.client.list_flavors(detail)
893 flavors = self._filter_by_name(flavors)
894 flavors = self._filter_by_id(flavors)
896 flavors = self._apply_common_filters(flavors)
897 if not (self['detail'] or (
898 self['json_output'] or self['output_format'])):
899 remove_from_items(flavors, 'links')
900 if detail and not self['detail']:
902 for key in set(flv).difference(self.PERMANENTS):
904 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
907 with_redundancy=self['detail'], with_enumeration=self['enum'],
910 pager(kwargs['out'].getvalue())
913 super(self.__class__, self)._run()
917 @command(flavor_cmds)
918 class flavor_info(_init_cyclades, _optional_json):
919 """Detailed information on a hardware flavor
920 To get a list of available flavors and flavor ids, try /flavor list
924 @errors.cyclades.connection
925 @errors.cyclades.flavor_id
926 def _run(self, flavor_id):
928 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
930 def main(self, flavor_id):
931 super(self.__class__, self)._run()
932 self._run(flavor_id=flavor_id)
935 def _add_name(self, net):
936 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
938 uuids.append(user_id)
940 uuids.append(tenant_id)
942 usernames = self._uuids2usernames(uuids)
944 net['user_id'] += ' (%s)' % usernames[user_id]
946 net['tenant_id'] += ' (%s)' % usernames[tenant_id]