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
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, ClientError
45 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
46 from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
47 from kamaki.cli.commands import _command_init, errors, addLogSettings
48 from kamaki.cli.commands import (
49 _optional_output_cmd, _optional_json, _name_filter, _id_filter)
52 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
53 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
54 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
55 ip_cmds = CommandTree('ip', 'Cyclades/Compute API floating ip commands')
56 _commands = [server_cmds, flavor_cmds, network_cmds, ip_cmds]
59 about_authentication = '\nUser Authentication:\
60 \n* to check authentication: /user authenticate\
61 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
64 'Defines a file to be injected to virtual servers file system.',
65 'syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
66 ' [local-path=]PATH: local file to be injected (relative or absolute)',
67 ' [server-path=]SERVER_PATH: destination location inside server Image',
68 ' [owner=]OWNER: virtual servers user id for the remote file',
69 ' [group=]GROUP: virtual servers group id or name for the remote file',
70 ' [mode=]MODE: permission in octal (e.g., 0777 or o+rwx)',
71 'e.g., -p /tmp/my.file,owner=root,mode=0777']
74 class _service_wait(object):
76 wait_arguments = dict(
77 progress_bar=ProgressBarArgument(
78 'do not show progress bar', ('-N', '--no-progress-bar'), False)
82 self, service, service_id, status_method, current_status,
83 countdown=True, timeout=60):
84 (progress_bar, wait_cb) = self._safe_progress_bar(
85 '%s %s: status is still %s' % (
86 service, service_id, current_status),
87 countdown=countdown, timeout=timeout)
90 new_mode = status_method(
91 service_id, current_status, max_wait=timeout, wait_cb=wait_cb)
93 self.error('%s %s: status is now %s' % (
94 service, service_id, new_mode))
96 self.error('%s %s: status is still %s' % (
97 service, service_id, current_status))
98 except KeyboardInterrupt:
99 self.error('\n- canceled')
101 self._safe_progress_bar_finish(progress_bar)
104 class _server_wait(_service_wait):
106 def _wait(self, server_id, current_status, timeout=60):
107 super(_server_wait, self)._wait(
108 'Server', server_id, self.client.wait_server, current_status,
109 countdown=(current_status not in ('BUILD', )),
110 timeout=timeout if current_status not in ('BUILD', ) else 100)
113 class _network_wait(_service_wait):
115 def _wait(self, net_id, current_status, timeout=60):
116 super(_network_wait, self)._wait(
117 'Network', net_id, self.client.wait_network, current_status,
121 class _firewall_wait(_service_wait):
123 def _wait(self, server_id, current_status, timeout=60):
124 super(_firewall_wait, self)._wait(
125 'Firewall of server',
126 server_id, self.client.wait_firewall, current_status,
130 class _init_cyclades(_command_init):
133 def _run(self, service='compute'):
134 if getattr(self, 'cloud', None):
135 base_url = self._custom_url(service) or self._custom_url(
138 token = self._custom_token(service) or self._custom_token(
139 'cyclades') or self.config.get_cloud('token')
140 self.client = CycladesClient(base_url=base_url, token=token)
143 self.cloud = 'default'
144 if getattr(self, 'auth_base', False):
145 cyclades_endpoints = self.auth_base.get_service_endpoints(
146 self._custom_type('cyclades') or 'compute',
147 self._custom_version('cyclades') or '')
148 base_url = cyclades_endpoints['publicURL']
149 token = self.auth_base.token
150 self.client = CycladesClient(base_url=base_url, token=token)
152 raise CLIBaseUrlError(service='cyclades')
158 @command(server_cmds)
159 class server_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
160 """List virtual servers accessible by user
161 Use filtering arguments (e.g., --name-like) to manage long server lists
164 PERMANENTS = ('id', 'name')
167 detail=FlagArgument('show detailed output', ('-l', '--details')),
169 'show only items since date (\' d/m/Y H:M:S \')',
172 'limit number of listed virtual servers', ('-n', '--number')),
174 'output results in pages (-n to set items per page, default 10)',
176 enum=FlagArgument('Enumerate results', '--enumerate'),
177 flavor_id=ValueArgument('filter by flavor id', ('--flavor-id')),
178 image_id=ValueArgument('filter by image id', ('--image-id')),
179 user_id=ValueArgument('filter by user id', ('--user-id')),
180 user_name=ValueArgument('filter by user name', ('--user-name')),
181 status=ValueArgument(
182 'filter by status (ACTIVE, STOPPED, REBOOT, ERROR, etc.)',
184 meta=KeyValueArgument('filter by metadata key=values', ('--metadata')),
185 meta_like=KeyValueArgument(
186 'print only if in key=value, the value is part of actual value',
187 ('--metadata-like')),
190 def _add_user_name(self, servers):
191 uuids = self._uuids2usernames(list(set(
192 [srv['user_id'] for srv in servers] +
193 [srv['tenant_id'] for srv in servers])))
195 srv['user_id'] += ' (%s)' % uuids[srv['user_id']]
196 srv['tenant_id'] += ' (%s)' % uuids[srv['tenant_id']]
199 def _apply_common_filters(self, servers):
200 common_filters = dict()
202 common_filters['status'] = self['status']
203 if self['user_id'] or self['user_name']:
204 uuid = self['user_id'] or self._username2uuid(self['user_name'])
205 common_filters['user_id'] = uuid
206 return filter_dicts_by_dict(servers, common_filters)
208 def _filter_by_image(self, servers):
209 iid = self['image_id']
210 return [srv for srv in servers if srv['image']['id'] == iid]
212 def _filter_by_flavor(self, servers):
213 fid = self['flavor_id']
214 return [srv for srv in servers if (
215 '%s' % srv['image']['id'] == '%s' % fid)]
217 def _filter_by_metadata(self, servers):
220 if not 'metadata' in srv:
222 meta = [dict(srv['metadata'])]
224 meta = filter_dicts_by_dict(meta, self['meta'])
225 if meta and self['meta_like']:
226 meta = filter_dicts_by_dict(
227 meta, self['meta_like'], exact_match=False)
229 new_servers.append(srv)
233 @errors.cyclades.connection
234 @errors.cyclades.date
236 withimage = bool(self['image_id'])
237 withflavor = bool(self['flavor_id'])
238 withmeta = bool(self['meta'] or self['meta_like'])
240 self['status'] or self['user_id'] or self['user_name'])
241 detail = self['detail'] or (
242 withimage or withflavor or withmeta or withcommons)
243 servers = self.client.list_servers(detail, self['since'])
245 servers = self._filter_by_name(servers)
246 servers = self._filter_by_id(servers)
247 servers = self._apply_common_filters(servers)
249 servers = self._filter_by_image(servers)
251 servers = self._filter_by_flavor(servers)
253 servers = self._filter_by_metadata(servers)
255 if self['detail'] and not (
256 self['json_output'] or self['output_format']):
257 servers = self._add_user_name(servers)
258 elif not (self['detail'] or (
259 self['json_output'] or self['output_format'])):
260 remove_from_items(servers, 'links')
261 if detail and not self['detail']:
263 for key in set(srv).difference(self.PERMANENTS):
265 kwargs = dict(with_enumeration=self['enum'])
267 kwargs['out'] = StringIO()
270 servers = servers[:self['limit']]
271 self._print(servers, **kwargs)
273 pager(kwargs['out'].getvalue())
276 super(self.__class__, self)._run()
280 @command(server_cmds)
281 class server_info(_init_cyclades, _optional_json):
282 """Detailed information on a Virtual Machine
284 - name, id, status, create/update dates
286 - metadata (e.g., os, superuser) and diagnostics
287 - hardware flavor and os image ids
291 @errors.cyclades.connection
292 @errors.cyclades.server_id
293 def _run(self, server_id):
294 vm = self.client.get_server_details(server_id)
295 uuids = self._uuids2usernames([vm['user_id'], vm['tenant_id']])
296 vm['user_id'] += ' (%s)' % uuids[vm['user_id']]
297 vm['tenant_id'] += ' (%s)' % uuids[vm['tenant_id']]
298 self._print(vm, self.print_dict)
300 def main(self, server_id):
301 super(self.__class__, self)._run()
302 self._run(server_id=server_id)
305 class PersonalityArgument(KeyValueArgument):
308 ('local-path', 'contents'),
309 ('server-path', 'path'),
316 return self._value if hasattr(self, '_value') else []
319 def value(self, newvalue):
320 if newvalue == self.default:
322 self._value, input_dict = [], {}
323 for i, terms in enumerate(newvalue):
324 termlist = terms.split(',')
325 if len(termlist) > len(self.terms):
326 msg = 'Wrong number of terms (1<=terms<=%s)' % len(self.terms)
327 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
329 for k, v in self.terms:
331 for item in termlist:
332 if item.lower().startswith(prefix):
333 input_dict[k] = item[len(k) + 1:]
337 termlist.remove(item)
340 path = input_dict['local-path']
342 path = termlist.pop(0)
344 raise CLIInvalidArgument(
345 '--personality: No local path specified',
346 details=howto_personality)
349 raise CLIInvalidArgument(
350 '--personality: File %s does not exist' % path,
351 details=howto_personality)
353 self._value.append(dict(path=path))
354 with open(path) as f:
355 self._value[i]['contents'] = b64encode(f.read())
356 for k, v in self.terms[1:]:
358 self._value[i][v] = input_dict[k]
361 self._value[i][v] = termlist.pop(0)
366 @command(server_cmds)
367 class server_create(_init_cyclades, _optional_json, _server_wait):
368 """Create a server (aka Virtual Machine)
370 - name: (single quoted text)
371 - flavor id: Hardware flavor. Pick one from: /flavor list
372 - image id: OS images. Pick one from: /image list
376 personality=PersonalityArgument(
377 (80 * ' ').join(howto_personality), ('-p', '--personality')),
378 wait=FlagArgument('Wait server to build', ('-w', '--wait'))
382 @errors.cyclades.connection
384 @errors.cyclades.flavor_id
385 def _run(self, name, flavor_id, image_id):
386 r = self.client.create_server(
387 name, int(flavor_id), image_id, personality=self['personality'])
388 usernames = self._uuids2usernames([r['user_id'], r['tenant_id']])
389 r['user_id'] += ' (%s)' % usernames[r['user_id']]
390 r['tenant_id'] += ' (%s)' % usernames[r['tenant_id']]
391 self._print(r, self.print_dict)
393 self._wait(r['id'], r['status'])
395 def main(self, name, flavor_id, image_id):
396 super(self.__class__, self)._run()
397 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
400 @command(server_cmds)
401 class server_rename(_init_cyclades, _optional_output_cmd):
402 """Set/update a virtual server name
403 virtual server names are not unique, therefore multiple servers may share
408 @errors.cyclades.connection
409 @errors.cyclades.server_id
410 def _run(self, server_id, new_name):
411 self._optional_output(
412 self.client.update_server_name(int(server_id), new_name))
414 def main(self, server_id, new_name):
415 super(self.__class__, self)._run()
416 self._run(server_id=server_id, new_name=new_name)
419 @command(server_cmds)
420 class server_delete(_init_cyclades, _optional_output_cmd, _server_wait):
421 """Delete a virtual server"""
424 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
428 @errors.cyclades.connection
429 @errors.cyclades.server_id
430 def _run(self, server_id):
433 details = self.client.get_server_details(server_id)
434 status = details['status']
436 r = self.client.delete_server(int(server_id))
437 self._optional_output(r)
440 self._wait(server_id, status)
442 def main(self, server_id):
443 super(self.__class__, self)._run()
444 self._run(server_id=server_id)
447 @command(server_cmds)
448 class server_reboot(_init_cyclades, _optional_output_cmd, _server_wait):
449 """Reboot a virtual server"""
453 'perform a hard reboot (deprecated)', ('-f', '--force')),
454 type=ValueArgument('SOFT or HARD - default: SOFT', ('--type')),
455 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
459 @errors.cyclades.connection
460 @errors.cyclades.server_id
461 def _run(self, server_id):
462 hard_reboot = self['hard']
465 'WARNING: -f/--force will be deprecated in version 0.12\n'
466 '\tIn the future, please use --type=hard instead')
468 if self['type'].lower() in ('soft', ):
470 elif self['type'].lower() in ('hard', ):
473 raise CLISyntaxError(
474 'Invalid reboot type %s' % self['type'],
475 importance=2, details=[
476 '--type values are either SOFT (default) or HARD'])
478 r = self.client.reboot_server(int(server_id), hard_reboot)
479 self._optional_output(r)
482 self._wait(server_id, 'REBOOT')
484 def main(self, server_id):
485 super(self.__class__, self)._run()
486 self._run(server_id=server_id)
489 @command(server_cmds)
490 class server_start(_init_cyclades, _optional_output_cmd, _server_wait):
491 """Start an existing virtual server"""
494 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
498 @errors.cyclades.connection
499 @errors.cyclades.server_id
500 def _run(self, server_id):
503 details = self.client.get_server_details(server_id)
504 status = details['status']
505 if status in ('ACTIVE', ):
508 r = self.client.start_server(int(server_id))
509 self._optional_output(r)
512 self._wait(server_id, status)
514 def main(self, server_id):
515 super(self.__class__, self)._run()
516 self._run(server_id=server_id)
519 @command(server_cmds)
520 class server_shutdown(_init_cyclades, _optional_output_cmd, _server_wait):
521 """Shutdown an active virtual server"""
524 wait=FlagArgument('Wait server to be destroyed', ('-w', '--wait'))
528 @errors.cyclades.connection
529 @errors.cyclades.server_id
530 def _run(self, server_id):
533 details = self.client.get_server_details(server_id)
534 status = details['status']
535 if status in ('STOPPED', ):
538 r = self.client.shutdown_server(int(server_id))
539 self._optional_output(r)
542 self._wait(server_id, status)
544 def main(self, server_id):
545 super(self.__class__, self)._run()
546 self._run(server_id=server_id)
549 @command(server_cmds)
550 class server_console(_init_cyclades, _optional_json):
551 """Get a VNC console to access an existing virtual server
552 Console connection information provided (at least):
553 - host: (url or address) a VNC host
554 - port: (int) the gateway to enter virtual server on host
555 - password: for VNC authorization
559 @errors.cyclades.connection
560 @errors.cyclades.server_id
561 def _run(self, server_id):
563 self.client.get_server_console(int(server_id)), self.print_dict)
565 def main(self, server_id):
566 super(self.__class__, self)._run()
567 self._run(server_id=server_id)
570 @command(server_cmds)
571 class server_resize(_init_cyclades, _optional_output_cmd):
572 """Set a different flavor for an existing server
573 To get server ids and flavor ids:
579 @errors.cyclades.connection
580 @errors.cyclades.server_id
581 @errors.cyclades.flavor_id
582 def _run(self, server_id, flavor_id):
583 self._optional_output(self.client.resize_server(server_id, flavor_id))
585 def main(self, server_id, flavor_id):
586 super(self.__class__, self)._run()
587 self._run(server_id=server_id, flavor_id=flavor_id)
590 @command(server_cmds)
591 class server_firewall(_init_cyclades):
592 """Manage virtual server firewall profiles for public networks"""
595 @command(server_cmds)
596 class server_firewall_set(
597 _init_cyclades, _optional_output_cmd, _firewall_wait):
598 """Set the firewall profile on virtual server public network
600 - DISABLED: Shutdown firewall
601 - ENABLED: Firewall in normal mode
602 - PROTECTED: Firewall in secure mode
606 wait=FlagArgument('Wait server firewall to build', ('-w', '--wait')),
608 'Set wait timeout in seconds (default: 60)', '--timeout',
613 @errors.cyclades.connection
614 @errors.cyclades.server_id
615 @errors.cyclades.firewall
616 def _run(self, server_id, profile):
617 if self['timeout'] and not self['wait']:
618 raise CLIInvalidArgument('Invalid use of --timeout', details=[
619 'Timeout is used only along with -w/--wait'])
620 old_profile = self.client.get_firewall_profile(server_id)
621 if old_profile.lower() == profile.lower():
622 self.error('Firewall of server %s: allready in status %s' % (
623 server_id, old_profile))
625 self._optional_output(self.client.set_firewall_profile(
626 server_id=int(server_id), profile=('%s' % profile).upper()))
628 self._wait(server_id, old_profile, timeout=self['timeout'])
630 def main(self, server_id, profile):
631 super(self.__class__, self)._run()
632 self._run(server_id=server_id, profile=profile)
635 @command(server_cmds)
636 class server_firewall_get(_init_cyclades):
637 """Get the firewall profile for a virtual servers' public network"""
640 @errors.cyclades.connection
641 @errors.cyclades.server_id
642 def _run(self, server_id):
643 self.writeln(self.client.get_firewall_profile(server_id))
645 def main(self, server_id):
646 super(self.__class__, self)._run()
647 self._run(server_id=server_id)
650 @command(server_cmds)
651 class server_addr(_init_cyclades, _optional_json):
652 """List the addresses of all network interfaces on a virtual server"""
655 enum=FlagArgument('Enumerate results', '--enumerate')
659 @errors.cyclades.connection
660 @errors.cyclades.server_id
661 def _run(self, server_id):
662 reply = self.client.list_server_nics(int(server_id))
663 self._print(reply, with_enumeration=self['enum'] and (reply) > 1)
665 def main(self, server_id):
666 super(self.__class__, self)._run()
667 self._run(server_id=server_id)
670 @command(server_cmds)
671 class server_metadata(_init_cyclades):
672 """Manage Server metadata (key:value pairs of server attributes)"""
675 @command(server_cmds)
676 class server_metadata_list(_init_cyclades, _optional_json):
677 """Get server metadata"""
680 @errors.cyclades.connection
681 @errors.cyclades.server_id
682 @errors.cyclades.metadata
683 def _run(self, server_id, key=''):
685 self.client.get_server_metadata(int(server_id), key),
688 def main(self, server_id, key=''):
689 super(self.__class__, self)._run()
690 self._run(server_id=server_id, key=key)
693 @command(server_cmds)
694 class server_metadata_set(_init_cyclades, _optional_json):
695 """Set / update virtual server metadata
696 Metadata should be given in key/value pairs in key=value format
697 For example: /server metadata set <server id> key1=value1 key2=value2
698 Old, unreferenced metadata will remain intact
702 @errors.cyclades.connection
703 @errors.cyclades.server_id
704 def _run(self, server_id, keyvals):
705 assert keyvals, 'Please, add some metadata ( key=value)'
707 for keyval in keyvals:
708 k, sep, v = keyval.partition('=')
713 'Invalid piece of metadata %s' % keyval,
714 importance=2, details=[
715 'Correct metadata format: key=val',
717 '/server metadata set <server id>'
718 'key1=value1 key2=value2'])
720 self.client.update_server_metadata(int(server_id), **metadata),
723 def main(self, server_id, *key_equals_val):
724 super(self.__class__, self)._run()
725 self._run(server_id=server_id, keyvals=key_equals_val)
728 @command(server_cmds)
729 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
730 """Delete virtual server metadata"""
733 @errors.cyclades.connection
734 @errors.cyclades.server_id
735 @errors.cyclades.metadata
736 def _run(self, server_id, key):
737 self._optional_output(
738 self.client.delete_server_metadata(int(server_id), key))
740 def main(self, server_id, key):
741 super(self.__class__, self)._run()
742 self._run(server_id=server_id, key=key)
745 @command(server_cmds)
746 class server_stats(_init_cyclades, _optional_json):
747 """Get virtual server statistics"""
750 @errors.cyclades.connection
751 @errors.cyclades.server_id
752 def _run(self, server_id):
754 self.client.get_server_stats(int(server_id)), self.print_dict)
756 def main(self, server_id):
757 super(self.__class__, self)._run()
758 self._run(server_id=server_id)
761 @command(server_cmds)
762 class server_wait(_init_cyclades, _server_wait):
763 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
767 'Wait limit in seconds (default: 60)', '--timeout', default=60)
771 @errors.cyclades.connection
772 @errors.cyclades.server_id
773 def _run(self, server_id, current_status):
774 r = self.client.get_server_details(server_id)
775 if r['status'].lower() == current_status.lower():
776 self._wait(server_id, current_status, timeout=self['timeout'])
779 'Server %s: Cannot wait for status %s, '
780 'status is already %s' % (
781 server_id, current_status, r['status']))
783 def main(self, server_id, current_status='BUILD'):
784 super(self.__class__, self)._run()
785 self._run(server_id=server_id, current_status=current_status)
788 @command(server_cmds)
789 class server_cluster_create(_init_cyclades):
790 """Create a cluster of virtual servers
791 All new servers will be named as <prefix><increment> e.g.,
792 mycluster1, mycluster2, etc.
793 All servers in the cluster will run the same image on the same hardware
798 @errors.cyclades.connection
800 @errors.cyclades.flavor_id
801 @errors.cyclades.cluster_size
802 def _run(self, prefix, image_id, flavor_id, size):
804 name='%s%s' % (prefix, i),
806 image_id=image_id) for i in range(int(size))]
807 self.client.create_cluster(servers)
809 def main(self, prefix, image_id, flavor_id, size):
810 super(self.__class__, self)._run()
811 self._run(prefix, image_id=image_id, flavor_id=flavor_id, size=size)
814 @command(server_cmds)
815 class server_cluster_delete(_init_cyclades):
816 """Remove all servers that belong to a virtual cluster
817 A virtual cluster consists of the virtual servers with the same name prefix
818 ATTENTION: make sure you want to delete all servers of that prefix
819 To get a list of your servers: /server list
823 @errors.cyclades.connection
824 def _run(self, prefix):
825 servers = [s['id'] for s in self.client.list_servers() if (
826 s['name'].startswith(prefix))]
827 self.client.delete_cluster(servers)
829 def main(self, prefix):
830 super(self.__class__, self)._run()
834 @command(flavor_cmds)
835 class flavor_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
836 """List available hardware flavors"""
838 PERMANENTS = ('id', 'name')
841 detail=FlagArgument('show detailed output', ('-l', '--details')),
842 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
844 'output results in pages (-n to set items per page, default 10)',
846 enum=FlagArgument('Enumerate results', '--enumerate'),
847 ram=ValueArgument('filter by ram', ('--ram')),
848 vcpus=ValueArgument('filter by number of VCPUs', ('--vcpus')),
849 disk=ValueArgument('filter by disk size in GB', ('--disk')),
850 disk_template=ValueArgument(
851 'filter by disk_templace', ('--disk-template'))
854 def _apply_common_filters(self, flavors):
855 common_filters = dict()
857 common_filters['ram'] = self['ram']
859 common_filters['vcpus'] = self['vcpus']
861 common_filters['disk'] = self['disk']
862 if self['disk_template']:
863 common_filters['SNF:disk_template'] = self['disk_template']
864 return filter_dicts_by_dict(flavors, common_filters)
867 @errors.cyclades.connection
869 withcommons = self['ram'] or self['vcpus'] or (
870 self['disk'] or self['disk_template'])
871 detail = self['detail'] or withcommons
872 flavors = self.client.list_flavors(detail)
873 flavors = self._filter_by_name(flavors)
874 flavors = self._filter_by_id(flavors)
876 flavors = self._apply_common_filters(flavors)
877 if not (self['detail'] or (
878 self['json_output'] or self['output_format'])):
879 remove_from_items(flavors, 'links')
880 if detail and not self['detail']:
882 for key in set(flv).difference(self.PERMANENTS):
884 kwargs = dict(out=StringIO(), title=()) if self['more'] else {}
887 with_redundancy=self['detail'], with_enumeration=self['enum'],
890 pager(kwargs['out'].getvalue())
893 super(self.__class__, self)._run()
897 @command(flavor_cmds)
898 class flavor_info(_init_cyclades, _optional_json):
899 """Detailed information on a hardware flavor
900 To get a list of available flavors and flavor ids, try /flavor list
904 @errors.cyclades.connection
905 @errors.cyclades.flavor_id
906 def _run(self, flavor_id):
908 self.client.get_flavor_details(int(flavor_id)), self.print_dict)
910 def main(self, flavor_id):
911 super(self.__class__, self)._run()
912 self._run(flavor_id=flavor_id)
915 def _add_name(self, net):
916 user_id, tenant_id, uuids = net['user_id'], net['tenant_id'], []
918 uuids.append(user_id)
920 uuids.append(tenant_id)
922 usernames = self._uuids2usernames(uuids)
924 net['user_id'] += ' (%s)' % usernames[user_id]
926 net['tenant_id'] += ' (%s)' % usernames[tenant_id]
929 @command(network_cmds)
930 class network_info(_init_cyclades, _optional_json):
931 """Detailed information on a network
932 To get a list of available networks and network ids, try /network list
936 @errors.cyclades.connection
937 @errors.cyclades.network_id
938 def _run(self, network_id):
939 network = self.client.get_network_details(int(network_id))
940 _add_name(self, network)
941 self._print(network, self.print_dict, exclude=('id'))
943 def main(self, network_id):
944 super(self.__class__, self)._run()
945 self._run(network_id=network_id)
948 @command(network_cmds)
949 class network_list(_init_cyclades, _optional_json, _name_filter, _id_filter):
952 PERMANENTS = ('id', 'name')
955 detail=FlagArgument('show detailed output', ('-l', '--details')),
956 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
958 'output results in pages (-n to set items per page, default 10)',
960 enum=FlagArgument('Enumerate results', '--enumerate'),
961 status=ValueArgument('filter by status', ('--status')),
962 public=FlagArgument('only public networks', ('--public')),
963 private=FlagArgument('only private networks', ('--private')),
964 dhcp=FlagArgument('show networks with dhcp', ('--with-dhcp')),
965 no_dhcp=FlagArgument('show networks without dhcp', ('--without-dhcp')),
966 user_id=ValueArgument('filter by user id', ('--user-id')),
967 user_name=ValueArgument('filter by user name', ('--user-name')),
968 gateway=ValueArgument('filter by gateway (IPv4)', ('--gateway')),
969 gateway6=ValueArgument('filter by gateway (IPv6)', ('--gateway6')),
970 cidr=ValueArgument('filter by cidr (IPv4)', ('--cidr')),
971 cidr6=ValueArgument('filter by cidr (IPv6)', ('--cidr6')),
972 type=ValueArgument('filter by type', ('--type')),
975 def _apply_common_filters(self, networks):
976 common_filter = dict()
980 common_filter['public'] = self['public']
981 elif self['private']:
982 common_filter['public'] = False
986 common_filter['dhcp'] = True
987 elif self['no_dhcp']:
988 common_filter['dhcp'] = False
989 if self['user_id'] or self['user_name']:
990 uuid = self['user_id'] or self._username2uuid(self['user_name'])
991 common_filter['user_id'] = uuid
992 for term in ('status', 'gateway', 'gateway6', 'cidr', 'cidr6', 'type'):
994 common_filter[term] = self[term]
995 return filter_dicts_by_dict(networks, common_filter)
997 def _add_name(self, networks, key='user_id'):
998 uuids = self._uuids2usernames(
999 list(set([net[key] for net in networks])))
1000 for net in networks:
1001 v = net.get(key, None)
1003 net[key] += ' (%s)' % uuids[v]
1007 @errors.cyclades.connection
1011 'status', 'public', 'private', 'user_id', 'user_name', 'type',
1012 'gateway', 'gateway6', 'cidr', 'cidr6', 'dhcp', 'no_dhcp'):
1016 detail = self['detail'] or withcommons
1017 networks = self.client.list_networks(detail)
1018 networks = self._filter_by_name(networks)
1019 networks = self._filter_by_id(networks)
1021 networks = self._apply_common_filters(networks)
1022 if not (self['detail'] or (
1023 self['json_output'] or self['output_format'])):
1024 remove_from_items(networks, 'links')
1025 if detail and not self['detail']:
1026 for net in networks:
1027 for key in set(net).difference(self.PERMANENTS):
1029 if self['detail'] and not (
1030 self['json_output'] or self['output_format']):
1031 self._add_name(networks)
1032 self._add_name(networks, 'tenant_id')
1033 kwargs = dict(with_enumeration=self['enum'])
1035 kwargs['out'] = StringIO()
1036 kwargs['title'] = ()
1038 networks = networks[:self['limit']]
1039 self._print(networks, **kwargs)
1041 pager(kwargs['out'].getvalue())
1044 super(self.__class__, self)._run()
1048 @command(network_cmds)
1049 class network_create(_init_cyclades, _optional_json, _network_wait):
1050 """Create an (unconnected) network"""
1053 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
1054 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
1055 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
1057 'Valid network types are '
1058 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
1060 default='MAC_FILTERED'),
1061 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1065 @errors.cyclades.connection
1066 @errors.cyclades.network_max
1067 def _run(self, name):
1068 r = self.client.create_network(
1071 gateway=self['gateway'],
1075 self._print(r, self.print_dict)
1076 if self['wait'] and r['status'] in ('PENDING', ):
1077 self._wait(r['id'], 'PENDING')
1079 def main(self, name):
1080 super(self.__class__, self)._run()
1084 @command(network_cmds)
1085 class network_rename(_init_cyclades, _optional_output_cmd):
1086 """Set the name of a network"""
1089 @errors.cyclades.connection
1090 @errors.cyclades.network_id
1091 def _run(self, network_id, new_name):
1092 self._optional_output(
1093 self.client.update_network_name(int(network_id), new_name))
1095 def main(self, network_id, new_name):
1096 super(self.__class__, self)._run()
1097 self._run(network_id=network_id, new_name=new_name)
1100 @command(network_cmds)
1101 class network_delete(_init_cyclades, _optional_output_cmd, _network_wait):
1102 """Delete a network"""
1105 wait=FlagArgument('Wait network to build', ('-w', '--wait'))
1109 @errors.cyclades.connection
1110 @errors.cyclades.network_in_use
1111 @errors.cyclades.network_id
1112 def _run(self, network_id):
1115 r = self.client.get_network_details(network_id)
1116 status = r['status']
1117 if status in ('DELETED', ):
1120 r = self.client.delete_network(int(network_id))
1121 self._optional_output(r)
1124 self._wait(network_id, status)
1126 def main(self, network_id):
1127 super(self.__class__, self)._run()
1128 self._run(network_id=network_id)
1131 @command(network_cmds)
1132 class network_connect(_init_cyclades, _optional_output_cmd):
1133 """Connect a server to a network"""
1136 @errors.cyclades.connection
1137 @errors.cyclades.server_id
1138 @errors.cyclades.network_id
1139 def _run(self, server_id, network_id):
1140 self._optional_output(
1141 self.client.connect_server(int(server_id), int(network_id)))
1143 def main(self, server_id, network_id):
1144 super(self.__class__, self)._run()
1145 self._run(server_id=server_id, network_id=network_id)
1148 @command(network_cmds)
1149 class network_disconnect(_init_cyclades):
1150 """Disconnect a nic that connects a server to a network
1151 Nic ids are listed as "attachments" in detailed network information
1152 To get detailed network information: /network info <network id>
1155 @errors.cyclades.nic_format
1156 def _server_id_from_nic(self, nic_id):
1157 return nic_id.split('-')[1]
1160 @errors.cyclades.connection
1161 @errors.cyclades.server_id
1162 @errors.cyclades.nic_id
1163 def _run(self, nic_id, server_id):
1164 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
1165 if not num_of_disconnected:
1167 'Network Interface %s not found on server %s' % (
1170 print('Disconnected %s connections' % num_of_disconnected)
1172 def main(self, nic_id):
1173 super(self.__class__, self)._run()
1174 server_id = self._server_id_from_nic(nic_id=nic_id)
1175 self._run(nic_id=nic_id, server_id=server_id)
1178 @command(network_cmds)
1179 class network_wait(_init_cyclades, _network_wait):
1180 """Wait for server to finish [PENDING, ACTIVE, DELETED]"""
1183 timeout=IntArgument(
1184 'Wait limit in seconds (default: 60)', '--timeout', default=60)
1188 @errors.cyclades.connection
1189 @errors.cyclades.network_id
1190 def _run(self, network_id, current_status):
1191 net = self.client.get_network_details(network_id)
1192 if net['status'].lower() == current_status.lower():
1193 self._wait(network_id, current_status, timeout=self['timeout'])
1196 'Network %s: Cannot wait for status %s, '
1197 'status is already %s' % (
1198 network_id, current_status, net['status']))
1200 def main(self, network_id, current_status='PENDING'):
1201 super(self.__class__, self)._run()
1202 self._run(network_id=network_id, current_status=current_status)
1206 class ip_pools(_init_cyclades, _optional_json):
1207 """List pools of floating IPs"""
1210 @errors.cyclades.connection
1212 r = self.client.get_floating_ip_pools()
1213 self._print(r if self['json_output'] or self['output_format'] else r[
1214 'floating_ip_pools'])
1217 super(self.__class__, self)._run()
1222 class ip_list(_init_cyclades, _optional_json):
1223 """List reserved floating IPs"""
1226 @errors.cyclades.connection
1228 r = self.client.get_floating_ips()
1229 self._print(r if self['json_output'] or self['output_format'] else r[
1233 super(self.__class__, self)._run()
1238 class ip_info(_init_cyclades, _optional_json):
1239 """Details for an IP"""
1242 @errors.cyclades.connection
1244 self._print(self.client.get_floating_ip(ip), self.print_dict)
1247 super(self.__class__, self)._run()
1252 class ip_reserve(_init_cyclades, _optional_json):
1253 """Reserve a floating IP
1254 An IP is reserved from an IP pool. The default IP pool is chosen
1255 automatically, but there is the option if specifying an explicit IP pool.
1258 arguments = dict(pool=ValueArgument('Source IP pool', ('--pool'), None))
1261 @errors.cyclades.connection
1262 def _run(self, ip=None):
1263 self._print([self.client.alloc_floating_ip(self['pool'], ip)])
1265 def main(self, requested_IP=None):
1266 super(self.__class__, self)._run()
1267 self._run(ip=requested_IP)
1271 class ip_release(_init_cyclades, _optional_output_cmd):
1272 """Release a floating IP
1273 The release IP is "returned" to the IP pool it came from.
1277 @errors.cyclades.connection
1279 self._optional_output(self.client.delete_floating_ip(ip))
1282 super(self.__class__, self)._run()
1287 class ip_attach(_init_cyclades, _optional_output_cmd):
1288 """Attach a floating IP to a server
1292 @errors.cyclades.connection
1293 @errors.cyclades.server_id
1294 def _run(self, server_id, ip):
1295 self._optional_output(self.client.attach_floating_ip(server_id, ip))
1297 def main(self, server_id, IP):
1298 super(self.__class__, self)._run()
1299 self._run(server_id=server_id, ip=IP)
1303 class ip_detach(_init_cyclades, _optional_output_cmd):
1304 """Detach a floating IP from a server
1308 @errors.cyclades.connection
1309 @errors.cyclades.server_id
1310 def _run(self, server_id, ip):
1311 self._optional_output(self.client.detach_floating_ip(server_id, ip))
1313 def main(self, server_id, IP):
1314 super(self.__class__, self)._run()
1315 self._run(server_id=server_id, ip=IP)