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 kamaki.cli import command
35 from kamaki.cli.command_tree import CommandTree
36 from kamaki.cli.utils import print_dict, remove_from_items
37 from kamaki.cli.errors import raiseCLIError, CLISyntaxError, CLIBaseUrlError
38 from kamaki.clients.cyclades import CycladesClient, ClientError
39 from kamaki.cli.argument import FlagArgument, ValueArgument, KeyValueArgument
40 from kamaki.cli.argument import ProgressBarArgument, DateArgument, IntArgument
41 from kamaki.cli.commands import _command_init, errors, addLogSettings
42 from kamaki.cli.commands import _optional_output_cmd, _optional_json
44 from base64 import b64encode
45 from os.path import exists
48 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
49 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
50 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
51 _commands = [server_cmds, flavor_cmds, network_cmds]
54 about_authentication = '\nUser Authentication:\
55 \n* to check authentication: /user authenticate\
56 \n* to set authentication token: /config set cloud.<cloud>.token <token>'
59 'Defines a file to be injected to VMs personality.',
60 'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
61 ' PATH: of local file to be injected',
62 ' SERVER_PATH: destination location inside server Image',
63 ' OWNER: user id of destination file owner',
64 ' GROUP: group id or name to own destination file',
65 ' MODEL: permition in octal (e.g. 0777 or o+rwx)']
68 class _init_cyclades(_command_init):
71 def _run(self, service='compute'):
72 if getattr(self, 'cloud', None):
73 base_url = self._custom_url(service)\
74 or self._custom_url('cyclades')
76 token = self._custom_token(service)\
77 or self._custom_token('cyclades')\
78 or self.config.get_cloud('token')
79 self.client = CycladesClient(
80 base_url=base_url, token=token)
83 self.cloud = 'default'
84 if getattr(self, 'auth_base', False):
85 cyclades_endpoints = self.auth_base.get_service_endpoints(
86 self._custom_type('cyclades') or 'compute',
87 self._custom_version('cyclades') or '')
88 base_url = cyclades_endpoints['publicURL']
89 token = self.auth_base.token
90 self.client = CycladesClient(base_url=base_url, token=token)
92 raise CLIBaseUrlError(service='cyclades')
99 class server_list(_init_cyclades, _optional_json):
100 """List Virtual Machines accessible by user"""
102 __doc__ += about_authentication
105 detail=FlagArgument('show detailed output', ('-l', '--details')),
107 'show only items since date (\' d/m/Y H:M:S \')',
109 limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
111 'output results in pages (-n to set items per page, default 10)',
113 enum=FlagArgument('Enumerate results', '--enumerate')
117 @errors.cyclades.connection
118 @errors.cyclades.date
120 servers = self.client.list_servers(self['detail'], self['since'])
121 if not (self['detail'] or self['json_output']):
122 remove_from_items(servers, 'links')
124 kwargs = dict(with_enumeration=self['enum'])
126 kwargs['page_size'] = self['limit'] if self['limit'] else 10
128 servers = servers[:self['limit']]
129 self._print(servers, **kwargs)
132 super(self.__class__, self)._run()
136 @command(server_cmds)
137 class server_info(_init_cyclades, _optional_json):
138 """Detailed information on a Virtual Machine
140 - name, id, status, create/update dates
142 - metadata (e.g. os, superuser) and diagnostics
143 - hardware flavor and os image ids
147 @errors.cyclades.connection
148 @errors.cyclades.server_id
149 def _run(self, server_id):
150 self._print(self.client.get_server_details(server_id), print_dict)
152 def main(self, server_id):
153 super(self.__class__, self)._run()
154 self._run(server_id=server_id)
157 class PersonalityArgument(KeyValueArgument):
160 return self._value if hasattr(self, '_value') else []
163 def value(self, newvalue):
164 if newvalue == self.default:
167 for i, terms in enumerate(newvalue):
168 termlist = terms.split(',')
169 if len(termlist) > 5:
170 msg = 'Wrong number of terms (should be 1 to 5)'
171 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
176 '--personality: File %s does not exist' % path,
178 details=howto_personality)
179 self._value.append(dict(path=path))
180 with open(path) as f:
181 self._value[i]['contents'] = b64encode(f.read())
183 self._value[i]['path'] = termlist[1]
184 self._value[i]['owner'] = termlist[2]
185 self._value[i]['group'] = termlist[3]
186 self._value[i]['mode'] = termlist[4]
191 @command(server_cmds)
192 class server_create(_init_cyclades, _optional_json):
193 """Create a server (aka Virtual Machine)
195 - name: (single quoted text)
196 - flavor id: Hardware flavor. Pick one from: /flavor list
197 - image id: OS images. Pick one from: /image list
201 personality=PersonalityArgument(
202 (80 * ' ').join(howto_personality), ('-p', '--personality'))
206 @errors.cyclades.connection
208 @errors.cyclades.flavor_id
209 def _run(self, name, flavor_id, image_id):
211 self.client.create_server(
212 name, int(flavor_id), image_id, self['personality']),
215 def main(self, name, flavor_id, image_id):
216 super(self.__class__, self)._run()
217 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
220 @command(server_cmds)
221 class server_rename(_init_cyclades, _optional_output_cmd):
222 """Set/update a server (VM) name
223 VM names are not unique, therefore multiple servers may share the same name
227 @errors.cyclades.connection
228 @errors.cyclades.server_id
229 def _run(self, server_id, new_name):
230 self._optional_output(
231 self.client.update_server_name(int(server_id), new_name))
233 def main(self, server_id, new_name):
234 super(self.__class__, self)._run()
235 self._run(server_id=server_id, new_name=new_name)
238 @command(server_cmds)
239 class server_delete(_init_cyclades, _optional_output_cmd):
240 """Delete a server (VM)"""
243 @errors.cyclades.connection
244 @errors.cyclades.server_id
245 def _run(self, server_id):
246 self._optional_output(self.client.delete_server(int(server_id)))
248 def main(self, server_id):
249 super(self.__class__, self)._run()
250 self._run(server_id=server_id)
253 @command(server_cmds)
254 class server_reboot(_init_cyclades, _optional_output_cmd):
255 """Reboot a server (VM)"""
258 hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
262 @errors.cyclades.connection
263 @errors.cyclades.server_id
264 def _run(self, server_id):
265 self._optional_output(
266 self.client.reboot_server(int(server_id), self['hard']))
268 def main(self, server_id):
269 super(self.__class__, self)._run()
270 self._run(server_id=server_id)
273 @command(server_cmds)
274 class server_start(_init_cyclades, _optional_output_cmd):
275 """Start an existing server (VM)"""
278 @errors.cyclades.connection
279 @errors.cyclades.server_id
280 def _run(self, server_id):
281 self._optional_output(self.client.start_server(int(server_id)))
283 def main(self, server_id):
284 super(self.__class__, self)._run()
285 self._run(server_id=server_id)
288 @command(server_cmds)
289 class server_shutdown(_init_cyclades, _optional_output_cmd):
290 """Shutdown an active server (VM)"""
293 @errors.cyclades.connection
294 @errors.cyclades.server_id
295 def _run(self, server_id):
296 self._optional_output(self.client.shutdown_server(int(server_id)))
298 def main(self, server_id):
299 super(self.__class__, self)._run()
300 self._run(server_id=server_id)
303 @command(server_cmds)
304 class server_console(_init_cyclades, _optional_json):
305 """Get a VNC console to access an existing server (VM)
306 Console connection information provided (at least):
307 - host: (url or address) a VNC host
308 - port: (int) the gateway to enter VM on host
309 - password: for VNC authorization
313 @errors.cyclades.connection
314 @errors.cyclades.server_id
315 def _run(self, server_id):
317 self.client.get_server_console(int(server_id)), print_dict)
319 def main(self, server_id):
320 super(self.__class__, self)._run()
321 self._run(server_id=server_id)
324 @command(server_cmds)
325 class server_firewall(_init_cyclades):
326 """Manage server (VM) firewall profiles for public networks"""
329 @command(server_cmds)
330 class server_firewall_set(_init_cyclades, _optional_output_cmd):
331 """Set the server (VM) firewall profile on VMs public network
333 - DISABLED: Shutdown firewall
334 - ENABLED: Firewall in normal mode
335 - PROTECTED: Firewall in secure mode
339 @errors.cyclades.connection
340 @errors.cyclades.server_id
341 @errors.cyclades.firewall
342 def _run(self, server_id, profile):
343 self._optional_output(self.client.set_firewall_profile(
344 server_id=int(server_id), profile=('%s' % profile).upper()))
346 def main(self, server_id, profile):
347 super(self.__class__, self)._run()
348 self._run(server_id=server_id, profile=profile)
351 @command(server_cmds)
352 class server_firewall_get(_init_cyclades):
353 """Get the server (VM) firewall profile for its public network"""
356 @errors.cyclades.connection
357 @errors.cyclades.server_id
358 def _run(self, server_id):
359 print(self.client.get_firewall_profile(server_id))
361 def main(self, server_id):
362 super(self.__class__, self)._run()
363 self._run(server_id=server_id)
366 @command(server_cmds)
367 class server_addr(_init_cyclades, _optional_json):
368 """List the addresses of all network interfaces on a server (VM)"""
371 enum=FlagArgument('Enumerate results', '--enumerate')
375 @errors.cyclades.connection
376 @errors.cyclades.server_id
377 def _run(self, server_id):
378 reply = self.client.list_server_nics(int(server_id))
380 reply, with_enumeration=self['enum'] and len(reply) > 1)
382 def main(self, server_id):
383 super(self.__class__, self)._run()
384 self._run(server_id=server_id)
387 @command(server_cmds)
388 class server_metadata(_init_cyclades):
389 """Manage Server metadata (key:value pairs of server attributes)"""
392 @command(server_cmds)
393 class server_metadata_list(_init_cyclades, _optional_json):
394 """Get server metadata"""
397 @errors.cyclades.connection
398 @errors.cyclades.server_id
399 @errors.cyclades.metadata
400 def _run(self, server_id, key=''):
402 self.client.get_server_metadata(int(server_id), key), print_dict)
404 def main(self, server_id, key=''):
405 super(self.__class__, self)._run()
406 self._run(server_id=server_id, key=key)
409 @command(server_cmds)
410 class server_metadata_set(_init_cyclades, _optional_json):
411 """Set / update server(VM) metadata
412 Metadata should be given in key/value pairs in key=value format
413 For example: /server metadata set <server id> key1=value1 key2=value2
414 Old, unreferenced metadata will remain intact
418 @errors.cyclades.connection
419 @errors.cyclades.server_id
420 def _run(self, server_id, keyvals):
421 assert keyvals, 'Please, add some metadata ( key=value)'
423 for keyval in keyvals:
424 k, sep, v = keyval.partition('=')
429 'Invalid piece of metadata %s' % keyval,
430 importance=2, details=[
431 'Correct metadata format: key=val',
433 '/server metadata set <server id>'
434 'key1=value1 key2=value2'])
436 self.client.update_server_metadata(int(server_id), **metadata),
439 def main(self, server_id, *key_equals_val):
440 super(self.__class__, self)._run()
441 self._run(server_id=server_id, keyvals=key_equals_val)
444 @command(server_cmds)
445 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
446 """Delete server (VM) metadata"""
449 @errors.cyclades.connection
450 @errors.cyclades.server_id
451 @errors.cyclades.metadata
452 def _run(self, server_id, key):
453 self._optional_output(
454 self.client.delete_server_metadata(int(server_id), key))
456 def main(self, server_id, key):
457 super(self.__class__, self)._run()
458 self._run(server_id=server_id, key=key)
461 @command(server_cmds)
462 class server_stats(_init_cyclades, _optional_json):
463 """Get server (VM) statistics"""
466 @errors.cyclades.connection
467 @errors.cyclades.server_id
468 def _run(self, server_id):
469 self._print(self.client.get_server_stats(int(server_id)), print_dict)
471 def main(self, server_id):
472 super(self.__class__, self)._run()
473 self._run(server_id=server_id)
476 @command(server_cmds)
477 class server_wait(_init_cyclades):
478 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
481 progress_bar=ProgressBarArgument(
482 'do not show progress bar',
483 ('-N', '--no-progress-bar'),
489 @errors.cyclades.connection
490 @errors.cyclades.server_id
491 def _run(self, server_id, currect_status):
492 (progress_bar, wait_cb) = self._safe_progress_bar(
493 'Server %s still in %s mode' % (server_id, currect_status))
496 new_mode = self.client.wait_server(
501 self._safe_progress_bar_finish(progress_bar)
504 self._safe_progress_bar_finish(progress_bar)
506 print('Server %s is now in %s mode' % (server_id, new_mode))
508 raiseCLIError(None, 'Time out')
510 def main(self, server_id, currect_status='BUILD'):
511 super(self.__class__, self)._run()
512 self._run(server_id=server_id, currect_status=currect_status)
515 @command(flavor_cmds)
516 class flavor_list(_init_cyclades, _optional_json):
517 """List available hardware flavors"""
520 detail=FlagArgument('show detailed output', ('-l', '--details')),
521 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
523 'output results in pages (-n to set items per page, default 10)',
525 enum=FlagArgument('Enumerate results', '--enumerate')
529 @errors.cyclades.connection
531 flavors = self.client.list_flavors(self['detail'])
532 if not (self['detail'] or self['json_output']):
533 remove_from_items(flavors, 'links')
534 pg_size = 10 if self['more'] and not self['limit'] else self['limit']
537 with_redundancy=self['detail'],
539 with_enumeration=self['enum'])
542 super(self.__class__, self)._run()
546 @command(flavor_cmds)
547 class flavor_info(_init_cyclades, _optional_json):
548 """Detailed information on a hardware flavor
549 To get a list of available flavors and flavor ids, try /flavor list
553 @errors.cyclades.connection
554 @errors.cyclades.flavor_id
555 def _run(self, flavor_id):
557 self.client.get_flavor_details(int(flavor_id)), print_dict)
559 def main(self, flavor_id):
560 super(self.__class__, self)._run()
561 self._run(flavor_id=flavor_id)
564 @command(network_cmds)
565 class network_info(_init_cyclades, _optional_json):
566 """Detailed information on a network
567 To get a list of available networks and network ids, try /network list
571 @errors.cyclades.connection
572 @errors.cyclades.network_id
573 def _run(self, network_id):
574 network = self.client.get_network_details(int(network_id))
575 self._print(network, print_dict, exclude=('id'))
577 def main(self, network_id):
578 super(self.__class__, self)._run()
579 self._run(network_id=network_id)
582 @command(network_cmds)
583 class network_list(_init_cyclades, _optional_json):
587 detail=FlagArgument('show detailed output', ('-l', '--details')),
588 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
590 'output results in pages (-n to set items per page, default 10)',
592 enum=FlagArgument('Enumerate results', '--enumerate')
596 @errors.cyclades.connection
598 networks = self.client.list_networks(self['detail'])
599 if not (self['detail'] or self['json_output']):
600 remove_from_items(networks, 'links')
601 kwargs = dict(with_enumeration=self['enum'])
603 kwargs['page_size'] = self['limit'] or 10
605 networks = networks[:self['limit']]
606 self._print(networks, **kwargs)
609 super(self.__class__, self)._run()
613 @command(network_cmds)
614 class network_create(_init_cyclades, _optional_json):
615 """Create an (unconnected) network"""
618 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
619 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
620 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
622 'Valid network types are '
623 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
625 default='MAC_FILTERED')
629 @errors.cyclades.connection
630 @errors.cyclades.network_max
631 def _run(self, name):
632 self._print(self.client.create_network(
635 gateway=self['gateway'],
637 type=self['type']), print_dict)
639 def main(self, name):
640 super(self.__class__, self)._run()
644 @command(network_cmds)
645 class network_rename(_init_cyclades, _optional_output_cmd):
646 """Set the name of a network"""
649 @errors.cyclades.connection
650 @errors.cyclades.network_id
651 def _run(self, network_id, new_name):
652 self._optional_output(
653 self.client.update_network_name(int(network_id), new_name))
655 def main(self, network_id, new_name):
656 super(self.__class__, self)._run()
657 self._run(network_id=network_id, new_name=new_name)
660 @command(network_cmds)
661 class network_delete(_init_cyclades, _optional_output_cmd):
662 """Delete a network"""
665 @errors.cyclades.connection
666 @errors.cyclades.network_id
667 @errors.cyclades.network_in_use
668 def _run(self, network_id):
669 self._optional_output(self.client.delete_network(int(network_id)))
671 def main(self, network_id):
672 super(self.__class__, self)._run()
673 self._run(network_id=network_id)
676 @command(network_cmds)
677 class network_connect(_init_cyclades, _optional_output_cmd):
678 """Connect a server to a network"""
681 @errors.cyclades.connection
682 @errors.cyclades.server_id
683 @errors.cyclades.network_id
684 def _run(self, server_id, network_id):
685 self._optional_output(
686 self.client.connect_server(int(server_id), int(network_id)))
688 def main(self, server_id, network_id):
689 super(self.__class__, self)._run()
690 self._run(server_id=server_id, network_id=network_id)
693 @command(network_cmds)
694 class network_disconnect(_init_cyclades):
695 """Disconnect a nic that connects a server to a network
696 Nic ids are listed as "attachments" in detailed network information
697 To get detailed network information: /network info <network id>
700 @errors.cyclades.nic_format
701 def _server_id_from_nic(self, nic_id):
702 return nic_id.split('-')[1]
705 @errors.cyclades.connection
706 @errors.cyclades.server_id
707 @errors.cyclades.nic_id
708 def _run(self, nic_id, server_id):
709 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
710 if not num_of_disconnected:
712 'Network Interface %s not found on server %s' % (
716 print('Disconnected %s connections' % num_of_disconnected)
718 def main(self, nic_id):
719 super(self.__class__, self)._run()
720 server_id = self._server_id_from_nic(nic_id=nic_id)
721 self._run(nic_id=nic_id, server_id=server_id)