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 _network_wait(_service_wait):
114 def _wait(self, net_id, current_status, timeout=60):
115 super(_network_wait, self)._wait(
116 'Network', net_id, self.client.wait_network, current_status,
120 class _firewall_wait(_service_wait):
122 def _wait(self, server_id, current_status, timeout=60):
123 super(_firewall_wait, self)._wait(
124 'Firewall of server',
125 server_id, self.client.wait_firewall, current_status,
129 class _init_cyclades(_command_init):
132 def _run(self, service='compute'):
133 if getattr(self, 'cloud', None):
134 base_url = self._custom_url(service) or self._custom_url(
137 token = self._custom_token(service) or self._custom_token(
138 'cyclades') or self.config.get_cloud('token')
139 self.client = CycladesClient(base_url=base_url, token=token)
142 self.cloud = 'default'
143 if getattr(self, 'auth_base', False):
144 cyclades_endpoints = self.auth_base.get_service_endpoints(
145 self._custom_type('cyclades') or 'compute',
146 self._custom_version('cyclades') or '')
147 base_url = cyclades_endpoints['publicURL']
148 token = self.auth_base.token
149 self.client = CycladesClient(base_url=base_url, token=token)
151 raise CLIBaseUrlError(service='cyclades')
157 @command(server_cmds)
158 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
159 """List virtual servers accessible by user
160 Use filtering arguments (e.g., --name-like) to manage long server lists
163 PERMANENTS = ('id', 'name')
166 detail=FlagArgument('show detailed output', ('-l', '--details')),
168 'show only items since date (\' d/m/Y H:M:S \')',
171 'limit number of listed virtual servers', ('-n', '--number')),
173 'output results in pages (-n to set items per page, default 10)',
175 enum=FlagArgument('Enumerate results', '--enumerate'),
176 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
177 image_id=ValueArgument('filter by image id', ('--image-id')),
178 user_id=ValueArgument('filter by user id', ('--user-id')),
179 user_name=ValueArgument('filter by user name', ('--user-name')),
180 status=ValueArgument(
181 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
183 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
184 meta_like=KeyValueArgument(
185 'print only if in key=value, the value is part of actual value',
186 ('--metadata-like')),
189 def _add_user_name(self, servers):
190 uuids = self._uuids2usernames(list(set(
191 [srv['user_id'] for srv in servers] +
192 [srv['tenant_id'] for srv in servers])))
194 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
195 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
198 def _apply_common_filters(self, servers):
199 common_filters = dict()
201 common_filters['status'] = self['status']
202 if self['user_id'] or self['user_name']:
203 uuid = self['user_id'] or self._username2uuid(self['user_name'])
204 common_filters['user_id'] = uuid
205 return filter_dicts_by_dict(servers, common_filters)
207 def _filter_by_image(self, servers):
208 iid = self['image_id']
209 return [srv for srv in servers if srv['image']['id'] == iid]
211 def _filter_by_flavor(self, servers):
212 fid = self['flavor_id']
213 return [srv for srv in servers if (
214 '%s' % srv['image']['id'] == '%s' % fid)]
216 def _filter_by_metadata(self, servers):
219 if not 'metadata' in srv:
221 meta = [dict(srv['metadata'])]
223 meta = filter_dicts_by_dict(meta, self['meta'])
224 if meta and self['meta_like']:
225 meta = filter_dicts_by_dict(
226 meta, self['meta_like'], exact_match=False)
228 new_servers.append(srv)
232 @errors.cyclades.connection
233 @errors.cyclades.date
235 withimage = bool(self['image_id'])
236 withflavor = bool(self['flavor_id'])
237 withmeta = bool(self['meta'] or self['meta_like'])
239 self['status'] or self['user_id'] or self['user_name'])
240 detail = self['detail'] or (
241 withimage or withflavor or withmeta or withcommons)
242 servers = self.client.list_servers(detail, self['since'])
244 servers = self._filter_by_name(servers)
245 servers = self._filter_by_id(servers)
246 servers = self._apply_common_filters(servers)
248 servers = self._filter_by_image(servers)
250 servers = self._filter_by_flavor(servers)
252 servers = self._filter_by_metadata(servers)
254 if self['detail'] and not (
255 self['json_output'] or self['output_format']):
256 servers = self._add_user_name(servers)
257 elif not (self['detail'] or (
258 self['json_output'] or self['output_format'])):
259 remove_from_items(servers, 'links')
260 if detail and not self['detail']:
262 for key in set(srv).difference(self.PERMANENTS):
264 kwargs = dict(with_enumeration=self['enum'])
266 kwargs['out'] = StringIO()
269 servers = servers[:self['limit']]
270 self._print(servers, **kwargs)
272 pager(kwargs['out'].getvalue())
275 super(self.__class__, self)._run()
279 @command(server_cmds)
280 class server_info(_init_cyclades, _optional_json):
281 """Detailed information on a Virtual Machine
283 - name, id, status, create/update dates
285 - metadata (e.g., os, superuser) and diagnostics
286 - hardware flavor and os image ids
290 @errors.cyclades.connection
291 @errors.cyclades.server_id
292 def _run(self, server_id):
293 vm = self.client.get_server_details(server_id)
294 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
295 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
296 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
297 self._print(vm, self.print_dict)
299 def main(self, server_id):
300 super(self.__class__, self)._run()
301 self._run(server_id=server_id)
304 class PersonalityArgument(KeyValueArgument):
307 ('local-path', 'contents'),
308 ('server-path', 'path'),
315 return getattr(self, '_value', [])
318 def value(self, newvalue):
319 if newvalue == self.default:
321 self._value, input_dict = [], {}
322 for i, terms in enumerate(newvalue):
323 termlist = terms.split(',')
324 if len(termlist) > len(self.terms):
325 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
326 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
328 for k, v in self.terms:
330 for item in termlist:
331 if item.lower().startswith(prefix):
332 input_dict[k] = item[len(k) + 1:]
336 termlist.remove(item)
339 path = input_dict['local-path']
341 path = termlist.pop(0)
343 raise CLIInvalidArgument(
344 '--personality: No local path specified',
345 details=howto_personality)
348 raise CLIInvalidArgument(
349 '--personality: File %s does not exist' % path,
350 details=howto_personality)
352 self._value.append(dict(path=path))
353 with open(expanduser(path)) as f:
354 self._value[i]['contents'] = b64encode(f.read())
355 for k, v in self.terms[1:]:
357 self._value[i][v] = input_dict[k]
360 self._value[i][v] = termlist.pop(0)
363 if k in ('mode', ) and self._value[i][v]:
365 self._value[i][v] = int(self._value[i][v], 8)
366 except ValueError as ve:
367 raise CLIInvalidArgument(
368 'Personality mode must be in octal', details=[
372 class NetworkIpArgument(RepeatableArgument):
376 return getattr(self, '_value', [])
379 def value(self, new_value):
380 for v in (new_value or []):
381 net_and_ip = v.split(',')
382 if len(net_and_ip) < 2:
383 raise CLIInvalidArgument(
384 'Value "%s" is missing parts' % v,
385 details=['Correct format: %s NETWORK_ID,IP' % (
386 self.parsed_name[0])])
387 self._value = getattr(self, '_value', list())
389 dict(network=net_and_ip[0], fixed_ip=net_and_ip[1]))
392 @command(server_cmds)
393 class server_create(_init_cyclades, _optional_json, _server_wait):
394 """Create a server (aka Virtual Machine)"""
397 server_name=ValueArgument('The name of the new server', '--name'),
398 flavor_id=IntArgument('The ID of the hardware flavor', '--flavor-id'),
399 image_id=ValueArgument('The ID of the hardware image', '--image-id'),
400 personality=PersonalityArgument(
401 (80 * ' ').join(howto_personality), ('-p', '--personality')),
402 wait=FlagArgument('Wait server to build', ('-w', '--wait')),
403 cluster_size=IntArgument(
404 'Create a cluster of servers of this size. In this case, the name'
405 'parameter is the prefix of each server in the cluster (e.g.,'
408 max_threads=IntArgument(
409 'Max threads in cluster mode (default 1)', '--threads'),
410 network_id=RepeatableArgument(
411 'Connect server to network (can be repeated)', '--network'),
412 network_id_and_ip=NetworkIpArgument(
413 'Connect server to network w. floating ip ( NETWORK_ID,IP )'
415 '--network-with-ip'),
416 automatic_ip=FlagArgument(
417 'Automatically assign an IP to the server', '--automatic-ip')
419 required = ('server_name', 'flavor_id', 'image_id')
421 @errors.cyclades.cluster_size
422 def _create_cluster(self, prefix, flavor_id, image_id, size):
423 if self['automatic_ip']:
426 networks = [dict(network=netid) for netid in (
427 (self['network_id'] or []) + (self['network_id_and_ip'] or [])
430 name='%s%s' % (prefix, i if size > 1 else ''),
433 personality=self['personality'],
434 networks=networks) for i in range(1, 1 + size)]
436 return [self.client.create_server(**servers[0])]
437 self.client.MAX_THREADS = int(self['max_threads'] or 1)
439 r = self.client.async_run(self.client.create_server, servers)
441 except Exception as e:
445 requested_names = [s['name'] for s in servers]
446 spawned_servers = [dict(
448 id=s['id']) for s in self.client.list_servers() if (
449 s['name'] in requested_names)]
450 self.error('Failed to build %s servers' % size)
451 self.error('Found %s matching servers:' % len(spawned_servers))
452 self._print(spawned_servers, out=self._err)
453 self.error('Check if any of these servers should be removed\n')
454 except Exception as ne:
455 self.error('Error (%s) while notifying about errors' % ne)
460 @errors.cyclades.connection
462 @errors.cyclades.flavor_id
463 def _run(self, name, flavor_id, image_id):
464 for r in self._create_cluster(
465 name, flavor_id, image_id, size=self['cluster_size'] or 1):
467 self.error('Create %s: server response was %s' % (name, r))
469 usernames = self._uuids2usernames(
470 [r['user_id'], r['tenant_id']])
471 r['user_id'] += ' (%s)' % usernames[r['user_id']]
472 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
473 self._print(r, self.print_dict)
475 self._wait(r['id'], r['status'])
479 super(self.__class__, self)._run()
480 if self['automatic_ip'] and (
481 self['network_id'] or self['network_id_and_ip']):
482 raise CLIInvalidArgument('Invalid argument combination', details=[
483 'Argument %s should not be combined with other' % (
484 self.arguments['automatic_ip'].lvalue),
485 'network-related arguments i.e., %s or %s' % (
486 self.arguments['network_id'].lvalue,
487 self.arguments['network_id_and_ip'].lvalue)])
489 name=self['server_name'],
490 flavor_id=self['flavor_id'],
491 image_id=self['image_id'])
494 class FirewallProfileArgument(ValueArgument):
496 profiles = ('DISABLED', 'ENABLED', 'PROTECTED')
500 return getattr(self, '_value', None)
503 def value(self, new_profile):
505 new_profile = new_profile.upper()
506 if new_profile in self.profiles:
507 self._value = new_profile
509 raise CLIInvalidArgument(
510 'Invalid firewall profile %s' % new_profile,
511 details=['Valid values: %s' % ', '.join(self.profiles)])
514 @command(server_cmds)
515 class server_modify(_init_cyclades, _optional_output_cmd):
516 """Modify attributes of a virtual server"""
519 server_name=ValueArgument('The new name', '--name'),
520 flavor_id=IntArgument('Set a different flavor', '--flavor-id'),
521 firewall_profile=FirewallProfileArgument(
522 'Valid values: %s' % (', '.join(FirewallProfileArgument.profiles)),
524 metadata_to_set=KeyValueArgument(
525 'Set metadata in key=value form (can be repeated)',
527 metadata_to_delete=RepeatableArgument(
528 'Delete metadata by key (can be repeated)', '--metadata-del')
531 'server_name', 'flavor_id', 'firewall_profile', 'metadata_to_set',
532 'metadata_to_delete']
535 @errors.cyclades.connection
536 @errors.cyclades.server_id
537 def _run(self, server_id):
538 if self['server_name']:
539 self.client.update_server_name((server_id), self['server_name'])
540 if self['flavor_id']:
541 self.client.resize_server(server_id, self['flavor_id'])
542 if self['firewall_profile']:
543 self.client.set_firewall_profile(
544 server_id=server_id, profile=self['firewall_profile'])
545 if self['metadata_to_set']:
546 self.client.update_server_metadata(
547 server_id, **self['metadata_to_set'])
548 for key in self['metadata_to_delete']:
549 errors.cyclades.metadata(
550 self.client.delete_server_metadata)(server_id, key=key)
551 if self['with_output']:
552 self._optional_output(self.client.get_server_details(server_id))
554 def main(self, server_id):
555 super(self.__class__, self)._run()
556 self._run(server_id=server_id)
559 @command(server_cmds)
560 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
561 """Delete a virtual server"""
564 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait')),
565 cluster=FlagArgument(
566 '(DANGEROUS) Delete all virtual servers prefixed with the cluster '
567 'prefix. In that case, the prefix replaces the server id',
571 def _server_ids(self, server_var):
573 return [s['id'] for s in self.client.list_servers() if (
574 s['name'].startswith(server_var))]
576 @errors.cyclades.server_id
577 def _check_server_id(self, server_id):
580 return [_check_server_id(self, server_id=server_var), ]
583 @errors.cyclades.connection
584 def _run(self, server_var):
585 for server_id in self._server_ids(server_var):
587 details = self.client.get_server_details(server_id)
588 status = details['status']
590 r = self.client.delete_server(server_id)
591 self._optional_output(r)
594 self._wait(server_id, status)
596 def main(self, server_id_or_cluster_prefix):
597 super(self.__class__, self)._run()
598 self._run(server_id_or_cluster_prefix)
601 @command(server_cmds)
602 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
603 """Reboot a virtual server"""
607 'perform a hard reboot (deprecated)', ('-f', '--force')),
608 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
609 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
613 @errors.cyclades.connection
614 @errors.cyclades.server_id
615 def _run(self, server_id):
616 hard_reboot = self['hard']
619 'WARNING: -f/--force will be deprecated in version 0.12\n'
620 '\tIn the future, please use --type=hard instead')
622 if self['type'].lower() in ('soft', ):
624 elif self['type'].lower() in ('hard', ):
627 raise CLISyntaxError(
628 'Invalid reboot type %s' % self['type'],
629 importance=2, details=[
630 '--type values are either SOFT (default) or HARD'])
632 r = self.client.reboot_server(int(server_id), hard_reboot)
633 self._optional_output(r)
636 self._wait(server_id, 'REBOOT')
638 def main(self, server_id):
639 super(self.__class__, self)._run()
640 self._run(server_id=server_id)
643 @command(server_cmds)
644 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
645 """Start an existing virtual server"""
648 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
652 @errors.cyclades.connection
653 @errors.cyclades.server_id
654 def _run(self, server_id):
657 details = self.client.get_server_details(server_id)
658 status = details['status']
659 if status in ('ACTIVE', ):
662 r = self.client.start_server(int(server_id))
663 self._optional_output(r)
666 self._wait(server_id, status)
668 def main(self, server_id):
669 super(self.__class__, self)._run()
670 self._run(server_id=server_id)
673 @command(server_cmds)
674 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
675 """Shutdown an active virtual server"""
678 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
682 @errors.cyclades.connection
683 @errors.cyclades.server_id
684 def _run(self, server_id):
687 details = self.client.get_server_details(server_id)
688 status = details['status']
689 if status in ('STOPPED', ):
692 r = self.client.shutdown_server(int(server_id))
693 self._optional_output(r)
696 self._wait(server_id, status)
698 def main(self, server_id):
699 super(self.__class__, self)._run()
700 self._run(server_id=server_id)
703 @command(server_cmds)
704 class server_console(_init_cyclades, _optional_json):
705 """Get a VNC console to access an existing virtual server
706 Console connection information provided (at least):
707 - host: (url or address) a VNC host
708 - port: (int) the gateway to enter virtual server on host
709 - password: for VNC authorization
713 @errors.cyclades.connection
714 @errors.cyclades.server_id
715 def _run(self, server_id):
717 self.client.get_server_console(int(server_id)), self.print_dict)
719 def main(self, server_id):
720 super(self.__class__, self)._run()
721 self._run(server_id=server_id)
724 @command(server_cmds)
725 class server_addr(_init_cyclades, _optional_json):
726 """List the addresses of all network interfaces on a virtual server"""
729 enum=FlagArgument('Enumerate results', '--enumerate')
733 @errors.cyclades.connection
734 @errors.cyclades.server_id
735 def _run(self, server_id):
736 reply = self.client.list_server_nics(int(server_id))
737 self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
739 def main(self, server_id):
740 super(self.__class__, self)._run()
741 self._run(server_id=server_id)
744 @command(server_cmds)
745 class server_stats(_init_cyclades, _optional_json):
746 """Get virtual server statistics"""
749 @errors.cyclades.connection
750 @errors.cyclades.server_id
751 def _run(self, server_id):
753 self.client.get_server_stats(int(server_id)), self.print_dict)
755 def main(self, server_id):
756 super(self.__class__, self)._run()
757 self._run(server_id=server_id)
760 @command(server_cmds)
761 class server_wait(_init_cyclades, _server_wait):
762 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
766 'Wait limit in seconds (default: 60)', '--timeout', default=60)
770 @errors.cyclades.connection
771 @errors.cyclades.server_id
772 def _run(self, server_id, current_status):
773 r = self.client.get_server_details(server_id)
774 if r['status'].lower() == current_status.lower():
775 self._wait(server_id, current_status, timeout=self['timeout'])
778 'Server %s: Cannot wait for status %s, '
779 'status is already %s' % (
780 server_id, current_status, r['status']))
782 def main(self, server_id, current_status='BUILD'):
783 super(self.__class__, self)._run()
784 self._run(server_id=server_id, current_status=current_status)
787 @command(flavor_cmds)
788 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
789 """List available hardware flavors"""
791 PERMANENTS = ('id', 'name')
794 detail=FlagArgument('show detailed output', ('-l', '--details')),
795 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
797 'output results in pages (-n to set items per page, default 10)',
799 enum=FlagArgument('Enumerate results', '--enumerate'),
800 ram=ValueArgument('filter by ram', ('--ram')),
801 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
802 disk=ValueArgument('filter by disk size in GB', ('--disk')),
803 disk_template=ValueArgument(
804 'filter by disk_templace', ('--disk-template'))
807 def _apply_common_filters(self, flavors):
808 common_filters = dict()
810 common_filters['ram'] = self['ram']
812 common_filters['vcpus'] = self['vcpus']
814 common_filters['disk'] = self['disk']
815 if self['disk_template']:
816 common_filters['SNF:disk_template'] = self['disk_template']
817 return filter_dicts_by_dict(flavors, common_filters)
820 @errors.cyclades.connection
822 withcommons = self['ram'] or self['vcpus'] or (
823 self['disk'] or self['disk_template'])
824 detail = self['detail'] or withcommons
825 flavors = self.client.list_flavors(detail)
826 flavors = self._filter_by_name(flavors)
827 flavors = self._filter_by_id(flavors)
829 flavors = self._apply_common_filters(flavors)
830 if not (self['detail'] or (
831 self['json_output'] or self['output_format'])):
832 remove_from_items(flavors, 'links')
833 if detail and not self['detail']:
835 for key in set(flv).difference(self.PERMANENTS):
837 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
840 with_redundancy=self['detail'], with_enumeration=self['enum'],
843 pager(kwargs['out'].getvalue())
846 super(self.__class__, self)._run()
850 @command(flavor_cmds)
851 class flavor_info(_init_cyclades, _optional_json):
852 """Detailed information on a hardware flavor
853 To get a list of available flavors and flavor ids, try /flavor list
857 @errors.cyclades.connection
858 @errors.cyclades.flavor_id
859 def _run(self, flavor_id):
861 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
863 def main(self, flavor_id):
864 super(self.__class__, self)._run()
865 self._run(flavor_id=flavor_id)
868 def _add_name(self, net):
869 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
871 uuids.append(user_id)
873 uuids.append(tenant_id)
875 usernames = self._uuids2usernames(uuids)
877 net['user_id'] += ' (%s)' % usernames[user_id]
879 net['tenant_id'] += ' (%s)' % usernames[tenant_id]