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, errors
43 from base64 import b64encode
44 from os.path import exists
47 server_cmds = CommandTree('server', 'Cyclades/Compute API server commands')
48 flavor_cmds = CommandTree('flavor', 'Cyclades/Compute API flavor commands')
49 network_cmds = CommandTree('network', 'Cyclades/Compute API network commands')
50 _commands = [server_cmds, flavor_cmds, network_cmds]
53 about_authentication = '\nUser Authentication:\
54 \n* to check authentication: /user authenticate\
55 \n* to set authentication token: /config set token <token>'
58 'Defines a file to be injected to VMs personality.',
59 'Personality value syntax: PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]',
60 ' PATH: of local file to be injected',
61 ' SERVER_PATH: destination location inside server Image',
62 ' OWNER: user id of destination file owner',
63 ' GROUP: group id or name to own destination file',
64 ' MODEL: permition in octal (e.g. 0777 or o+rwx)']
67 class _init_cyclades(_command_init):
69 def _run(self, service='compute'):
70 token = self.config.get(service, 'token')\
71 or self.config.get('global', 'token')
72 base_url = self.config.get(service, 'url')\
73 or self.config.get('global', 'url')
74 self.client = CycladesClient(base_url=base_url, token=token)
75 self._set_log_params()
76 self._update_max_threads()
83 class server_list(_init_cyclades):
84 """List Virtual Machines accessible by user"""
86 __doc__ += about_authentication
89 detail=FlagArgument('show detailed output', ('-l', '--details')),
91 'show only items since date (\' d/m/Y H:M:S \')',
93 limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
95 'output results in pages (-n to set items per page, default 10)',
97 enum=FlagArgument('Enumerate results', '--enumerate')
100 def _make_results_pretty(self, servers):
101 for server in servers:
103 if 'attachments' in server:
104 for addr in server['attachments']['values']:
105 ips = addr.pop('values', [])
107 addr['IPv%s' % ip['version']] = ip['addr']
108 if 'firewallProfile' in addr:
109 addr['firewall'] = addr.pop('firewallProfile')
110 addr_dict[addr.pop('id')] = addr
111 server['attachments'] = addr_dict if addr_dict else None
112 if 'metadata' in server:
113 server['metadata'] = server['metadata']['values']
116 @errors.cyclades.connection
117 @errors.cyclades.date
119 servers = self.client.list_servers(self['detail'], self['since'])
121 self._make_results_pretty(servers)
126 page_size=self['limit'] if self['limit'] else 10,
127 with_enumeration=self['enum'])
130 servers[:self['limit'] if self['limit'] else len(servers)],
131 with_enumeration=self['enum'])
134 super(self.__class__, self)._run()
138 @command(server_cmds)
139 class server_info(_init_cyclades):
140 """Detailed information on a Virtual Machine
142 - name, id, status, create/update dates
144 - metadata (e.g. os, superuser) and diagnostics
145 - hardware flavor and os image ids
148 def _print(self, server):
150 if 'attachments' in server:
151 atts = server.pop('attachments')
152 for addr in atts['values']:
153 ips = addr.pop('values', [])
155 addr['IPv%s' % ip['version']] = ip['addr']
156 if 'firewallProfile' in addr:
157 addr['firewall'] = addr.pop('firewallProfile')
158 addr_dict[addr.pop('id')] = addr
159 server['attachments'] = addr_dict if addr_dict else None
160 if 'metadata' in server:
161 server['metadata'] = server['metadata']['values']
162 print_dict(server, ident=1)
165 @errors.cyclades.connection
166 @errors.cyclades.server_id
167 def _run(self, server_id):
168 server = self.client.get_server_details(server_id)
171 def main(self, server_id):
172 super(self.__class__, self)._run()
173 self._run(server_id=server_id)
176 class PersonalityArgument(KeyValueArgument):
179 return self._value if hasattr(self, '_value') else []
182 def value(self, newvalue):
183 if newvalue == self.default:
186 for i, terms in enumerate(newvalue):
187 termlist = terms.split(',')
188 if len(termlist) > 5:
189 msg = 'Wrong number of terms (should be 1 to 5)'
190 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
195 '--personality: File %s does not exist' % path,
197 details=howto_personality)
198 self._value.append(dict(path=path))
199 with open(path) as f:
200 self._value[i]['contents'] = b64encode(f.read())
202 self._value[i]['path'] = termlist[1]
203 self._value[i]['owner'] = termlist[2]
204 self._value[i]['group'] = termlist[3]
205 self._value[i]['mode'] = termlist[4]
210 @command(server_cmds)
211 class server_create(_init_cyclades):
212 """Create a server (aka Virtual Machine)
214 - name: (single quoted text)
215 - flavor id: Hardware flavor. Pick one from: /flavor list
216 - image id: OS images. Pick one from: /image list
220 personality=PersonalityArgument(
221 ' /// '.join(howto_personality),
222 ('-p', '--personality'))
226 @errors.cyclades.connection
228 @errors.cyclades.flavor_id
229 def _run(self, name, flavor_id, image_id):
230 r = self.client.create_server(
237 def main(self, name, flavor_id, image_id):
238 super(self.__class__, self)._run()
239 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
242 @command(server_cmds)
243 class server_rename(_init_cyclades):
244 """Set/update a server (VM) name
245 VM names are not unique, therefore multiple servers may share the same name
249 @errors.cyclades.connection
250 @errors.cyclades.server_id
251 def _run(self, server_id, new_name):
252 self.client.update_server_name(int(server_id), new_name)
254 def main(self, server_id, new_name):
255 super(self.__class__, self)._run()
256 self._run(server_id=server_id, new_name=new_name)
259 @command(server_cmds)
260 class server_delete(_init_cyclades):
261 """Delete a server (VM)"""
264 @errors.cyclades.connection
265 @errors.cyclades.server_id
266 def _run(self, server_id):
267 self.client.delete_server(int(server_id))
269 def main(self, server_id):
270 super(self.__class__, self)._run()
271 self._run(server_id=server_id)
274 @command(server_cmds)
275 class server_reboot(_init_cyclades):
276 """Reboot a server (VM)"""
279 hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
283 @errors.cyclades.connection
284 @errors.cyclades.server_id
285 def _run(self, server_id):
286 self.client.reboot_server(int(server_id), self['hard'])
288 def main(self, server_id):
289 super(self.__class__, self)._run()
290 self._run(server_id=server_id)
293 @command(server_cmds)
294 class server_start(_init_cyclades):
295 """Start an existing server (VM)"""
298 @errors.cyclades.connection
299 @errors.cyclades.server_id
300 def _run(self, server_id):
301 self.client.start_server(int(server_id))
303 def main(self, server_id):
304 super(self.__class__, self)._run()
305 self._run(server_id=server_id)
308 @command(server_cmds)
309 class server_shutdown(_init_cyclades):
310 """Shutdown an active server (VM)"""
313 @errors.cyclades.connection
314 @errors.cyclades.server_id
315 def _run(self, server_id):
316 self.client.shutdown_server(int(server_id))
318 def main(self, server_id):
319 super(self.__class__, self)._run()
320 self._run(server_id=server_id)
323 @command(server_cmds)
324 class server_console(_init_cyclades):
325 """Get a VNC console to access an existing server (VM)
326 Console connection information provided (at least):
327 - host: (url or address) a VNC host
328 - port: (int) the gateway to enter VM on host
329 - password: for VNC authorization
333 @errors.cyclades.connection
334 @errors.cyclades.server_id
335 def _run(self, server_id):
336 r = self.client.get_server_console(int(server_id))
339 def main(self, server_id):
340 super(self.__class__, self)._run()
341 self._run(server_id=server_id)
344 @command(server_cmds)
345 class server_firewall(_init_cyclades):
346 """Set the server (VM) firewall profile on VMs public network
348 - DISABLED: Shutdown firewall
349 - ENABLED: Firewall in normal mode
350 - PROTECTED: Firewall in secure mode
354 @errors.cyclades.connection
355 @errors.cyclades.server_id
356 @errors.cyclades.firewall
357 def _run(self, server_id, profile):
358 self.client.set_firewall_profile(
359 server_id=int(server_id),
360 profile=('%s' % profile).upper())
362 def main(self, server_id, profile):
363 super(self.__class__, self)._run()
364 self._run(server_id=server_id, profile=profile)
367 @command(server_cmds)
368 class server_addr(_init_cyclades):
369 """List the addresses of all network interfaces on a server (VM)"""
372 @errors.cyclades.connection
373 @errors.cyclades.server_id
374 def _run(self, server_id):
375 reply = self.client.list_server_nics(int(server_id))
376 print_list(reply, with_enumeration=len(reply) > 1)
378 def main(self, server_id):
379 super(self.__class__, self)._run()
380 self._run(server_id=server_id)
383 @command(server_cmds)
384 class server_meta(_init_cyclades):
385 """Get a server's metadatum
386 Metadata are formed as key:value pairs where key is used to retrieve them
390 @errors.cyclades.connection
391 @errors.cyclades.server_id
392 @errors.cyclades.metadata
393 def _run(self, server_id, key=''):
394 r = self.client.get_server_metadata(int(server_id), key)
397 def main(self, server_id, key=''):
398 super(self.__class__, self)._run()
399 self._run(server_id=server_id, key=key)
402 @command(server_cmds)
403 class server_setmeta(_init_cyclades):
404 """set server (VM) metadata
405 Metadata are formed as key:value pairs, both needed to set one
409 @errors.cyclades.connection
410 @errors.cyclades.server_id
411 def _run(self, server_id, key, val):
412 metadata = {key: val}
413 r = self.client.update_server_metadata(int(server_id), **metadata)
416 def main(self, server_id, key, val):
417 super(self.__class__, self)._run()
418 self._run(server_id=server_id, key=key, val=val)
421 @command(server_cmds)
422 class server_delmeta(_init_cyclades):
423 """Delete server (VM) metadata"""
426 @errors.cyclades.connection
427 @errors.cyclades.server_id
428 @errors.cyclades.metadata
429 def _run(self, server_id, key):
430 self.client.delete_server_metadata(int(server_id), key)
432 def main(self, server_id, key):
433 super(self.__class__, self)._run()
434 self._run(server_id=server_id, key=key)
437 @command(server_cmds)
438 class server_stats(_init_cyclades):
439 """Get server (VM) statistics"""
442 @errors.cyclades.connection
443 @errors.cyclades.server_id
444 def _run(self, server_id):
445 r = self.client.get_server_stats(int(server_id))
446 print_dict(r, exclude=('serverRef',))
448 def main(self, server_id):
449 super(self.__class__, self)._run()
450 self._run(server_id=server_id)
453 @command(server_cmds)
454 class server_wait(_init_cyclades):
455 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
458 progress_bar=ProgressBarArgument(
459 'do not show progress bar',
460 ('-N', '--no-progress-bar'),
466 @errors.cyclades.connection
467 @errors.cyclades.server_id
468 def _run(self, server_id, currect_status):
469 (progress_bar, wait_cb) = self._safe_progress_bar(
470 'Server %s still in %s mode' % (server_id, currect_status))
473 new_mode = self.client.wait_server(
478 self._safe_progress_bar_finish(progress_bar)
481 self._safe_progress_bar_finish(progress_bar)
483 print('Server %s is now in %s mode' % (server_id, new_mode))
485 raiseCLIError(None, 'Time out')
487 def main(self, server_id, currect_status='BUILD'):
488 super(self.__class__, self)._run()
489 self._run(server_id=server_id, currect_status=currect_status)
492 @command(flavor_cmds)
493 class flavor_list(_init_cyclades):
494 """List available hardware flavors"""
497 detail=FlagArgument('show detailed output', ('-l', '--details')),
498 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
500 'output results in pages (-n to set items per page, default 10)',
502 enum=FlagArgument('Enumerate results', '--enumerate')
506 @errors.cyclades.connection
508 flavors = self.client.list_flavors(self['detail'])
509 pg_size = 10 if self['more'] and not self['limit'] else self['limit']
512 with_redundancy=self['detail'],
514 with_enumeration=self['enum'])
517 super(self.__class__, self)._run()
521 @command(flavor_cmds)
522 class flavor_info(_init_cyclades):
523 """Detailed information on a hardware flavor
524 To get a list of available flavors and flavor ids, try /flavor list
528 @errors.cyclades.connection
529 @errors.cyclades.flavor_id
530 def _run(self, flavor_id):
531 flavor = self.client.get_flavor_details(int(flavor_id))
534 def main(self, flavor_id):
535 super(self.__class__, self)._run()
536 self._run(flavor_id=flavor_id)
539 @command(network_cmds)
540 class network_info(_init_cyclades):
541 """Detailed information on a network
542 To get a list of available networks and network ids, try /network list
546 def _make_result_pretty(self, net):
547 if 'attachments' in net:
548 att = net['attachments']['values']
550 net['attachments'] = att if count else None
553 @errors.cyclades.connection
554 @errors.cyclades.network_id
555 def _run(self, network_id):
556 network = self.client.get_network_details(int(network_id))
557 self._make_result_pretty(network)
558 print_dict(network, exclude=('id'))
560 def main(self, network_id):
561 super(self.__class__, self)._run()
562 self._run(network_id=network_id)
565 @command(network_cmds)
566 class network_list(_init_cyclades):
570 detail=FlagArgument('show detailed output', ('-l', '--details')),
571 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
573 'output results in pages (-n to set items per page, default 10)',
575 enum=FlagArgument('Enumerate results', '--enumerate')
578 def _make_results_pretty(self, nets):
580 network_info._make_result_pretty(net)
583 @errors.cyclades.connection
585 networks = self.client.list_networks(self['detail'])
587 self._make_results_pretty(networks)
591 page_size=self['limit'] or 10, with_enumeration=self['enum'])
594 networks[:self['limit']],
595 with_enumeration=self['enum'])
597 print_items(networks, with_enumeration=self['enum'])
600 super(self.__class__, self)._run()
604 @command(network_cmds)
605 class network_create(_init_cyclades):
606 """Create an (unconnected) network"""
609 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
610 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
611 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
613 'Valid network types are '
614 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
616 default='MAC_FILTERED')
620 @errors.cyclades.connection
621 @errors.cyclades.network_max
622 def _run(self, name):
623 r = self.client.create_network(
626 gateway=self['gateway'],
631 def main(self, name):
632 super(self.__class__, self)._run()
636 @command(network_cmds)
637 class network_rename(_init_cyclades):
638 """Set the name of a network"""
641 @errors.cyclades.connection
642 @errors.cyclades.network_id
643 def _run(self, network_id, new_name):
644 self.client.update_network_name(int(network_id), new_name)
646 def main(self, network_id, new_name):
647 super(self.__class__, self)._run()
648 self._run(network_id=network_id, new_name=new_name)
651 @command(network_cmds)
652 class network_delete(_init_cyclades):
653 """Delete a network"""
656 @errors.cyclades.connection
657 @errors.cyclades.network_id
658 @errors.cyclades.network_in_use
659 def _run(self, network_id):
660 self.client.delete_network(int(network_id))
662 def main(self, network_id):
663 super(self.__class__, self)._run()
664 self._run(network_id=network_id)
667 @command(network_cmds)
668 class network_connect(_init_cyclades):
669 """Connect a server to a network"""
672 @errors.cyclades.connection
673 @errors.cyclades.server_id
674 @errors.cyclades.network_id
675 def _run(self, server_id, network_id):
676 self.client.connect_server(int(server_id), int(network_id))
678 def main(self, server_id, network_id):
679 super(self.__class__, self)._run()
680 self._run(server_id=server_id, network_id=network_id)
683 @command(network_cmds)
684 class network_disconnect(_init_cyclades):
685 """Disconnect a nic that connects a server to a network
686 Nic ids are listed as "attachments" in detailed network information
687 To get detailed network information: /network info <network id>
690 @errors.cyclades.nic_format
691 def _server_id_from_nic(self, nic_id):
692 return nic_id.split('-')[1]
695 @errors.cyclades.connection
696 @errors.cyclades.server_id
697 @errors.cyclades.nic_id
698 def _run(self, nic_id, server_id):
699 if not self.client.disconnect_server(server_id, nic_id):
701 'Network Interface %s not found on server %s' % (
706 def main(self, nic_id):
707 super(self.__class__, self)._run()
708 server_id = self._server_id_from_nic(nic_id=nic_id)
709 self._run(nic_id=nic_id, server_id=server_id)