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, print_list, print_items
37 from kamaki.cli.errors import raiseCLIError, CLISyntaxError
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
43 from base64 import b64encode
44 from os.path import exists
47 server_cmds = CommandTree('server',
48 'Compute/Cyclades API server commands')
49 flavor_cmds = CommandTree('flavor',
50 'Compute/Cyclades API flavor commands')
51 image_cmds = CommandTree('image',
52 'Compute/Cyclades or Glance API image commands')
53 network_cmds = CommandTree('network',
54 'Compute/Cyclades API network commands')
55 _commands = [server_cmds, flavor_cmds, image_cmds, network_cmds]
58 about_authentication = '\n User Authentication:\
59 \n to check authentication: /astakos authenticate\
60 \n to set authentication token: /config set token <token>'
63 'Defines a file to be injected to VMs personality.',
64 'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
65 ' PATH: of local file to be injected',
66 ' SERVER_PATH: destination location inside server Image',
67 ' OWNER: user id of destination file owner',
68 ' GROUP: group id or name to own destination file',
69 ' MODEL: permition in octal (e.g. 0777 or o+rwx)']
72 def raise_if_connection_error(err, base_url='compute.url'):
74 raiseCLIError(err, 'Authorization failed', details=[
75 'Make sure a valid token is provided:',
76 ' to check if the token is valid: /astakos authenticate',
77 ' to set a token: /config set [.server.]token <token>',
78 ' to get current token: /config get [server.]token'])
79 elif err.status in range(-12, 200) + [403, 500]:
80 raiseCLIError(err, details=[
81 'Check if service is up or set to %s' % base_url,
82 ' to get service url: /config get %s' % base_url,
83 ' to set service url: /config set %s <URL>' % base_url]
87 class _init_cyclades(_command_init):
88 def _run(self, service='compute'):
89 token = self.config.get(service, 'token')\
90 or self.config.get('global', 'token')
91 base_url = self.config.get(service, 'url')\
92 or self.config.get('global', 'url')
93 self.client = CycladesClient(base_url=base_url, token=token)
95 def main(self, service='compute'):
100 class server_list(_init_cyclades):
101 """List Virtual Machines accessible by user
104 __doc__ += about_authentication
107 detail=FlagArgument('show detailed output', '-l'),
109 'show only items since date (\' d/m/Y H:M:S \')',
111 limit=IntArgument('limit the number of VMs to list', '-n'),
113 'output results in pages (-n to set items per page, default 10)',
117 def _make_results_pretty(self, servers):
118 for server in servers:
120 if 'attachments' in server:
121 for addr in server['attachments']['values']:
122 ips = addr.pop('values', [])
124 addr['IPv%s' % ip['version']] = ip['addr']
125 if 'firewallProfile' in addr:
126 addr['firewall'] = addr.pop('firewallProfile')
127 addr_dict[addr.pop('id')] = addr
128 server['attachments'] = addr_dict if addr_dict else None
129 if 'metadata' in server:
130 server['metadata'] = server['metadata']['values']
133 super(self.__class__, self).main()
135 servers = self.client.list_servers(self['detail'], self['since'])
137 self._make_results_pretty(servers)
138 except ClientError as ce:
139 if ce.status == 400 and 'changes-since' in ('%s' % ce):
141 'Incorrect date format for --since',
142 details=['Accepted date format: d/m/y'])
143 raise_if_connection_error(ce)
145 except Exception as err:
150 page_size=self['limit'] if self['limit'] else 10)
153 servers[:self['limit'] if self['limit'] else len(servers)])
156 @command(server_cmds)
157 class server_info(_init_cyclades):
158 """Detailed information on a Virtual Machine
160 - name, id, status, create/update dates
162 - metadata (e.g. os, superuser) and diagnostics
163 - hardware flavor and os image ids
167 def _print(self, server):
169 if 'attachments' in server:
170 atts = server.pop('attachments')
171 for addr in atts['values']:
172 ips = addr.pop('values', [])
174 addr['IPv%s' % ip['version']] = ip['addr']
175 if 'firewallProfile' in addr:
176 addr['firewall'] = addr.pop('firewallProfile')
177 addr_dict[addr.pop('id')] = addr
178 server['attachments'] = addr_dict if addr_dict else None
179 if 'metadata' in server:
180 server['metadata'] = server['metadata']['values']
181 print_dict(server, ident=1)
183 def main(self, server_id):
184 super(self.__class__, self).main()
186 server = self.client.get_server_details(int(server_id))
187 except ValueError as err:
188 raiseCLIError(err, 'Server id must be a positive integer', 1)
189 except ClientError as ce:
191 raiseCLIError(ce, 'Server with id %s not found' % server_id)
192 raise_if_connection_error(ce)
194 except Exception as err:
199 class PersonalityArgument(KeyValueArgument):
202 return self._value if hasattr(self, '_value') else []
205 def value(self, newvalue):
206 if newvalue == self.default:
209 for i, terms in enumerate(newvalue):
210 termlist = terms.split(',')
211 if len(termlist) > 5:
213 CLISyntaxError('Wrong number of terms (should be 1 to 5)'),
214 details=howto_personality)
218 '--personality: File %s does not exist' % path,
220 details=howto_personality)
221 self._value.append(dict(path=path))
222 with open(path) as f:
223 self._value[i]['contents'] = b64encode(f.read())
225 self._value[i]['path'] = termlist[1]
226 self._value[i]['owner'] = termlist[2]
227 self._value[i]['group'] = termlist[3]
228 self._value[i]['mode'] = termlist[4]
233 @command(server_cmds)
234 class server_create(_init_cyclades):
235 """Create a server (aka Virtual Machine)
237 - name: (single quoted text)
238 - flavor id: Hardware flavor. Pick one from: /flavor list
239 - image id: OS images. Pick one from: /image list
243 personality=PersonalityArgument(
244 ' _ _ _ '.join(howto_personality),
245 parsed_name='--personality')
248 def main(self, name, flavor_id, image_id):
249 super(self.__class__, self).main()
252 reply = self.client.create_server(
258 except ClientError as ce:
260 msg = ('%s' % ce).lower()
263 'Flavor id %s not found' % flavor_id,
264 details=['How to pick a valid flavor id:',
265 ' - get a list of flavor ids: /flavor list',
266 ' - details on a flavor: /flavor info <flavor id>'])
269 'Image id %s not found' % image_id,
270 details=['How to pick a valid image id:',
271 ' - get a list of image ids: /image list',
272 ' - details on an image: /image info <image id>'])
273 raise_if_connection_error(ce)
275 except ValueError as err:
276 raiseCLIError(err, 'Invalid flavor id %s ' % flavor_id,
277 details='Flavor id must be a positive integer',
279 except Exception as err:
280 raiseCLIError(err, 'Syntax error: %s\n' % err, importance=1)
284 @command(server_cmds)
285 class server_rename(_init_cyclades):
286 """Set/update a server (VM) name
287 VM names are not unique, therefore multiple server may share the same name
290 def main(self, server_id, new_name):
291 super(self.__class__, self).main()
293 self.client.update_server_name(int(server_id), new_name)
294 except ClientError as ce:
296 raiseCLIError(ce, 'Server with id %s not found' % server_id)
297 raise_if_connection_error(ce)
299 except ValueError as err:
300 raiseCLIError(err, 'Invalid server id %s ' % server_id,
301 details=['Server id must be positive integer\n'],
305 @command(server_cmds)
306 class server_delete(_init_cyclades):
307 """Delete a server (VM)"""
309 def main(self, server_id):
310 super(self.__class__, self).main()
312 self.client.delete_server(int(server_id))
313 except ClientError as ce:
315 raiseCLIError(ce, 'Server with id %s not found' % server_id)
316 raise_if_connection_error(ce)
318 except ValueError as err:
319 raiseCLIError(err, 'Invalid server id %s ' % server_id,
320 details=['Server id must be positive integer\n'],
322 except Exception as err:
326 @command(server_cmds)
327 class server_reboot(_init_cyclades):
328 """Reboot a server (VM)"""
331 hard=FlagArgument('perform a hard reboot', '-f')
334 def main(self, server_id):
335 super(self.__class__, self).main()
337 self.client.reboot_server(int(server_id), self['hard'])
338 except ClientError as ce:
340 raiseCLIError(ce, 'Server with id %s not found' % server_id)
341 raise_if_connection_error(ce)
343 except ValueError as err:
344 raiseCLIError(err, 'Invalid server id %s ' % server_id,
345 details=['Server id must be positive integer\n'],
347 except Exception as err:
351 @command(server_cmds)
352 class server_start(_init_cyclades):
353 """Start an existing server (VM)"""
355 def main(self, server_id):
356 super(self.__class__, self).main()
358 self.client.start_server(int(server_id))
359 except ClientError as ce:
361 raiseCLIError(ce, 'Server with id %s not found' % server_id)
362 raise_if_connection_error(ce)
364 except ValueError as err:
365 raiseCLIError(err, 'Invalid server id %s ' % server_id,
366 details=['Server id must be positive integer\n'],
368 except Exception as err:
372 @command(server_cmds)
373 class server_shutdown(_init_cyclades):
374 """Shutdown an active server (VM)"""
376 def main(self, server_id):
377 super(self.__class__, self).main()
379 self.client.shutdown_server(int(server_id))
380 except ClientError as ce:
382 raiseCLIError(ce, 'Server with id %s not found' % server_id)
383 raise_if_connection_error(ce)
385 except ValueError as err:
386 raiseCLIError(err, 'Invalid server id %s ' % server_id,
387 details=['Server id must be positive integer\n'],
389 except Exception as err:
393 @command(server_cmds)
394 class server_console(_init_cyclades):
395 """Get a VNC console to access an existing server (VM)
396 Console connection information provided (at least):
397 - host: (url or address) a VNC host
398 - port: (int) the gateway to enter VM on host
399 - password: for VNC authorization
402 def main(self, server_id):
403 super(self.__class__, self).main()
405 reply = self.client.get_server_console(int(server_id))
406 except ClientError as ce:
408 raiseCLIError(ce, 'Server with id %s not found' % server_id)
409 raise_if_connection_error(ce)
411 except ValueError as err:
412 raiseCLIError(err, 'Invalid server id %s ' % server_id,
413 details=['Server id must be positive integer\n'],
415 except Exception as err:
420 @command(server_cmds)
421 class server_firewall(_init_cyclades):
422 """Set the server (VM) firewall profile on VMs public network
424 - DISABLED: Shutdown firewall
425 - ENABLED: Firewall in normal mode
426 - PROTECTED: Firewall in secure mode
429 def main(self, server_id, profile):
430 super(self.__class__, self).main()
432 self.client.set_firewall_profile(
434 unicode(profile).upper())
435 except ClientError as ce:
436 if ce.status == 400 and 'firewall' in '%s' % ce:
438 '%s is an unsupported firewall profile' % profile)
439 elif ce.status == 404:
440 raiseCLIError(ce, 'Server with id %s not found' % server_id)
441 raise_if_connection_error(ce)
443 except ValueError as err:
444 raiseCLIError(err, 'Invalid server id %s ' % server_id,
445 details=['Server id must be positive integer\n'],
447 except Exception as err:
451 @command(server_cmds)
452 class server_addr(_init_cyclades):
453 """List the addresses of all network interfaces on a server (VM)"""
455 def main(self, server_id):
456 super(self.__class__, self).main()
458 reply = self.client.list_server_nics(int(server_id))
459 except ClientError as ce:
461 raiseCLIError(ce, 'Server with id %s not found' % server_id)
462 raise_if_connection_error(ce)
464 except ValueError as err:
465 raiseCLIError(err, 'Invalid server id %s ' % server_id,
466 details=['Server id must be positive integer\n'],
468 except Exception as err:
470 print_list(reply, with_enumeration=len(reply) > 1)
473 @command(server_cmds)
474 class server_meta(_init_cyclades):
475 """Get a server's metadatum
476 Metadata are formed as key:value pairs where key is used to retrieve them
479 def main(self, server_id, key=''):
480 super(self.__class__, self).main()
482 reply = self.client.get_server_metadata(int(server_id), key)
483 except ClientError as ce:
485 msg = 'No metadata with key %s' % key\
486 if 'Metadata' in '%s' % ce\
487 else 'Server with id %s not found' % server_id
488 raiseCLIError(ce, msg)
489 raise_if_connection_error(ce)
491 except ValueError as err:
492 raiseCLIError(err, 'Invalid server id %s ' % server_id,
493 details=['Server id must be positive integer\n'],
495 except Exception as err:
500 @command(server_cmds)
501 class server_setmeta(_init_cyclades):
502 """set server (VM) metadata
503 Metadata are formed as key:value pairs, both needed to set one
506 def main(self, server_id, key, val):
507 super(self.__class__, self).main()
508 metadata = {key: val}
510 reply = self.client.update_server_metadata(int(server_id),
512 except ClientError as ce:
514 raiseCLIError(ce, 'Server with id %s not found' % server_id)
515 raise_if_connection_error(ce)
517 except ValueError as err:
518 raiseCLIError(err, 'Invalid server id %s ' % server_id,
519 details=['Server id must be positive integer\n'],
521 except Exception as err:
526 @command(server_cmds)
527 class server_delmeta(_init_cyclades):
528 """Delete server (VM) metadata"""
530 def main(self, server_id, key):
531 super(self.__class__, self).main()
533 self.client.delete_server_metadata(int(server_id), key)
534 except ClientError as ce:
536 msg = 'No metadata with key %s' % key\
537 if 'Metadata' in '%s' % ce\
538 else 'Server with id %s not found' % server_id
539 raiseCLIError(ce, msg)
540 raise_if_connection_error(ce)
542 except ValueError as err:
543 raiseCLIError(err, 'Invalid server id %s ' % server_id,
544 details=['Server id must be positive integer\n'],
546 except Exception as err:
550 @command(server_cmds)
551 class server_stats(_init_cyclades):
552 """Get server (VM) statistics"""
554 def main(self, server_id):
555 super(self.__class__, self).main()
557 reply = self.client.get_server_stats(int(server_id))
558 except ClientError as ce:
560 raiseCLIError(ce, 'Server with id %s not found' % server_id)
561 raise_if_connection_error(ce)
563 except ValueError as err:
564 raiseCLIError(err, 'Invalid server id %s ' % server_id,
565 details=['Server id must be positive integer\n'],
567 except Exception as err:
569 print_dict(reply, exclude=('serverRef',))
572 @command(server_cmds)
573 class server_wait(_init_cyclades):
574 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
577 progress_bar=ProgressBarArgument(
578 'do not show progress bar',
584 def main(self, server_id, currect_status='BUILD'):
585 super(self.__class__, self).main()
587 progress_bar = self.arguments['progress_bar']
588 wait_cb = progress_bar.get_generator(\
589 'Server %s still in %s mode' % (server_id, currect_status))
590 except ValueError as err:
591 raiseCLIError(err, 'Invalid server id %s ' % server_id,
592 details=['Server id must be positive integer\n'],
597 new_mode = self.client.wait_server(server_id,
600 progress_bar.finish()
601 except KeyboardInterrupt:
603 progress_bar.finish()
605 except ClientError as ce:
606 progress_bar.finish()
608 raiseCLIError(ce, 'Server with id %s not found' % server_id)
609 raise_if_connection_error(ce)
612 print('Server %s is now in %s mode' % (server_id, new_mode))
614 raiseCLIError(None, 'Time out')
617 @command(flavor_cmds)
618 class flavor_list(_init_cyclades):
619 """List available hardware flavors"""
622 detail=FlagArgument('show detailed output', '-l'),
623 limit=IntArgument('limit the number of flavors to list', '-n'),
625 'output results in pages (-n to set items per page, default 10)',
630 super(self.__class__, self).main()
632 flavors = self.client.list_flavors(self['detail'])
633 except ClientError as ce:
634 raise_if_connection_error(ce)
636 except Exception as err:
641 with_redundancy=self['detail'],
642 page_size=self['limit'] if self['limit'] else 10)
646 with_redundancy=self['detail'],
647 page_size=self['limit'])
650 @command(flavor_cmds)
651 class flavor_info(_init_cyclades):
652 """Detailed information on a hardware flavor
653 To get a list of available flavors and flavor ids, try /flavor list
656 def main(self, flavor_id):
657 super(self.__class__, self).main()
659 flavor = self.client.get_flavor_details(int(flavor_id))
660 except ClientError as ce:
661 raise_if_connection_error(ce)
663 except ValueError as err:
665 'Invalid flavor id %s' % flavor_id,
667 details=['Flavor id must be possitive integer'])
668 except Exception as err:
673 @command(network_cmds)
674 class network_info(_init_cyclades):
675 """Detailed information on a network
676 To get a list of available networks and network ids, try /network list
680 def _make_result_pretty(self, net):
681 if 'attachments' in net:
682 att = net['attachments']['values']
684 net['attachments'] = att if count else None
686 def main(self, network_id):
687 super(self.__class__, self).main()
689 network = self.client.get_network_details(int(network_id))
690 self._make_result_pretty(network)
691 except ClientError as ce:
692 raise_if_connection_error(ce)
695 'No network found with id %s' % network_id,
696 details=['To see a detailed list of available network ids',
697 ' try /network list'])
699 except ValueError as ve:
701 'Invalid network_id %s' % network_id,
703 details=['Network id must be a possitive integer'])
704 except Exception as err:
709 @command(network_cmds)
710 class network_list(_init_cyclades):
714 detail=FlagArgument('show detailed output', '-l'),
715 limit=IntArgument('limit the number of networks in list', '-n'),
717 'output results in pages (-n to set items per page, default 10)',
721 def _make_results_pretty(self, nets):
723 network_info._make_result_pretty(net)
726 super(self.__class__, self).main()
728 networks = self.client.list_networks(self['detail'])
730 self._make_results_pretty(networks)
731 except ClientError as ce:
732 raise_if_connection_error(ce)
735 'No networks found on server %s' % self.client.base_url,
737 'Please, check if service url is correctly set',
738 ' to get current service url: /config get compute.url',
739 ' to set service url: /config set compute.url <URL>'])
741 except Exception as err:
744 print_items(networks,
745 page_size=self['limit'] if self['limit'] else 10)
747 print_items(networks[:self['limit']])
749 print_items(networks)
752 @command(network_cmds)
753 class network_create(_init_cyclades):
754 """Create an (unconnected) network"""
757 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
758 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
759 dhcp=ValueArgument('explicitly set dhcp', '--with-dhcp'),
760 type=ValueArgument('explicitly set type', '--with-type')
763 def main(self, name):
764 super(self.__class__, self).main()
766 reply = self.client.create_network(name,
768 gateway=self['gateway'],
771 except ClientError as ce:
772 raise_if_connection_error(ce)
775 'Cannot create another network',
776 details=['Maximum number of networks reached'])
778 except Exception as err:
783 @command(network_cmds)
784 class network_rename(_init_cyclades):
785 """Set the name of a network"""
787 def main(self, network_id, new_name):
788 super(self.__class__, self).main()
790 self.client.update_network_name(int(network_id), new_name)
791 except ClientError as ce:
792 raise_if_connection_error(ce)
795 'No network found with id %s' % network_id,
796 details=['To see a detailed list of available network ids',
797 ' try /network list'])
799 except ValueError as ve:
801 'Invalid network_id %s' % network_id,
803 details=['Network id must be a possitive integer'])
804 except Exception as err:
808 @command(network_cmds)
809 class network_delete(_init_cyclades):
810 """Delete a network"""
812 def main(self, network_id):
813 super(self.__class__, self).main()
815 self.client.delete_network(int(network_id))
816 except ClientError as ce:
817 raise_if_connection_error(ce)
820 'Network with id %s is in use' % network_id,
822 'Disconnect all nics/VMs of this network first',
823 ' to get nics: /network info %s' % network_id,
824 ' (under "attachments" section)',
825 ' to disconnect: /network disconnect <nic id>'])
826 elif ce.status == 404:
828 'No network found with id %s' % network_id,
829 details=['To see a detailed list of available network ids',
830 ' try /network list'])
832 except ValueError as ve:
834 'Invalid network_id %s' % network_id,
836 details=['Network id must be a possitive integer'])
837 except Exception as err:
841 @command(network_cmds)
842 class network_connect(_init_cyclades):
843 """Connect a server to a network"""
845 def main(self, server_id, network_id):
846 super(self.__class__, self).main()
848 network_id = int(network_id)
849 server_id = int(server_id)
850 self.client.connect_server(server_id, network_id)
851 except ClientError as ce:
852 raise_if_connection_error(ce)
854 (thename, theid) = ('server', server_id)\
855 if 'server' in ('%s' % ce).lower()\
856 else ('network', network_id)
858 'No %s found with id %s' % (thename, theid),
860 'To see a detailed list of available %s ids' % thename,
861 ' try /%s list' % thename])
863 except ValueError as ve:
864 (thename, theid) = ('server', server_id)\
865 if isinstance(network_id, int) else ('network', network_id)
867 'Invalid %s id %s' % (thename, theid),
869 details=['The %s id must be a possitive integer' % thename,
870 ' to get available %s ids: /%s list' % (thename, thename)])
871 except Exception as err:
875 @command(network_cmds)
876 class network_disconnect(_init_cyclades):
877 """Disconnect a nic that connects a server to a network
878 Nic ids are listed as "attachments" in detailed network information
879 To get detailed network information: /network info <network id>
882 def main(self, nic_id):
883 super(self.__class__, self).main()
885 server_id = nic_id.split('-')[1]
886 if not self.client.disconnect_server(server_id, nic_id):
887 raise ClientError('Network Interface not found', status=404)
888 except ClientError as ce:
889 raise_if_connection_error(ce)
891 if 'server' in ('%s' % ce).lower():
893 'No server found with id %s' % (server_id),
895 'To see a detailed list of available server ids',
896 ' try /server list'])
898 'No nic %s in server with id %s' % (nic_id, server_id),
900 'To see a list of nic ids for server %s try:' % server_id,
901 ' /server addr %s' % server_id])
903 except IndexError as err:
905 'Nic %s is of incorrect format' % nic_id,
907 details=['nid_id format: nic-<server_id>-<nic_index>',
908 ' to get nic ids of a network: /network info <net_id>',
909 ' they are listed under the "attachments" section'])
910 except Exception as err: