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
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
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 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):
70 def _run(self, service='compute'):
71 token = self.config.get(service, 'token')\
72 or self.config.get('global', 'token')
73 cyclades_endpoints = self.auth_base.get_service_endpoints(
74 self.config.get('cyclades', 'type'),
75 self.config.get('cyclades', 'version'))
76 base_url = cyclades_endpoints['publicURL']
77 self.client = CycladesClient(base_url=base_url, token=token)
78 self._set_log_params()
79 self._update_max_threads()
86 class server_list(_init_cyclades, _optional_json):
87 """List Virtual Machines accessible by user"""
89 __doc__ += about_authentication
92 detail=FlagArgument('show detailed output', ('-l', '--details')),
94 'show only items since date (\' d/m/Y H:M:S \')',
96 limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
98 'output results in pages (-n to set items per page, default 10)',
100 enum=FlagArgument('Enumerate results', '--enumerate')
104 @errors.cyclades.connection
105 @errors.cyclades.date
107 servers = self.client.list_servers(self['detail'], self['since'])
109 kwargs = dict(with_enumeration=self['enum'])
111 kwargs['page_size'] = self['limit'] if self['limit'] else 10
113 servers = servers[:self['limit']]
114 self._print(servers, **kwargs)
117 super(self.__class__, self)._run()
121 @command(server_cmds)
122 class server_info(_init_cyclades, _optional_json):
123 """Detailed information on a Virtual Machine
125 - name, id, status, create/update dates
127 - metadata (e.g. os, superuser) and diagnostics
128 - hardware flavor and os image ids
132 @errors.cyclades.connection
133 @errors.cyclades.server_id
134 def _run(self, server_id):
135 self._print(self.client.get_server_details(server_id), print_dict)
137 def main(self, server_id):
138 super(self.__class__, self)._run()
139 self._run(server_id=server_id)
142 class PersonalityArgument(KeyValueArgument):
145 return self._value if hasattr(self, '_value') else []
148 def value(self, newvalue):
149 if newvalue == self.default:
152 for i, terms in enumerate(newvalue):
153 termlist = terms.split(',')
154 if len(termlist) > 5:
155 msg = 'Wrong number of terms (should be 1 to 5)'
156 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
161 '--personality: File %s does not exist' % path,
163 details=howto_personality)
164 self._value.append(dict(path=path))
165 with open(path) as f:
166 self._value[i]['contents'] = b64encode(f.read())
168 self._value[i]['path'] = termlist[1]
169 self._value[i]['owner'] = termlist[2]
170 self._value[i]['group'] = termlist[3]
171 self._value[i]['mode'] = termlist[4]
176 @command(server_cmds)
177 class server_create(_init_cyclades, _optional_json):
178 """Create a server (aka Virtual Machine)
180 - name: (single quoted text)
181 - flavor id: Hardware flavor. Pick one from: /flavor list
182 - image id: OS images. Pick one from: /image list
186 personality=PersonalityArgument(
187 (80 * ' ').join(howto_personality), ('-p', '--personality'))
191 @errors.cyclades.connection
193 @errors.cyclades.flavor_id
194 def _run(self, name, flavor_id, image_id):
196 self.client.create_server(
197 name, int(flavor_id), image_id, self['personality']),
200 def main(self, name, flavor_id, image_id):
201 super(self.__class__, self)._run()
202 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
205 @command(server_cmds)
206 class server_rename(_init_cyclades, _optional_output_cmd):
207 """Set/update a server (VM) name
208 VM names are not unique, therefore multiple servers may share the same name
212 @errors.cyclades.connection
213 @errors.cyclades.server_id
214 def _run(self, server_id, new_name):
215 self._optional_output(
216 self.client.update_server_name(int(server_id), new_name))
218 def main(self, server_id, new_name):
219 super(self.__class__, self)._run()
220 self._run(server_id=server_id, new_name=new_name)
223 @command(server_cmds)
224 class server_delete(_init_cyclades, _optional_output_cmd):
225 """Delete a server (VM)"""
228 @errors.cyclades.connection
229 @errors.cyclades.server_id
230 def _run(self, server_id):
231 self._optional_output(self.client.delete_server(int(server_id)))
233 def main(self, server_id):
234 super(self.__class__, self)._run()
235 self._run(server_id=server_id)
238 @command(server_cmds)
239 class server_reboot(_init_cyclades, _optional_output_cmd):
240 """Reboot a server (VM)"""
243 hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
247 @errors.cyclades.connection
248 @errors.cyclades.server_id
249 def _run(self, server_id):
250 self._optional_output(
251 self.client.reboot_server(int(server_id), self['hard']))
253 def main(self, server_id):
254 super(self.__class__, self)._run()
255 self._run(server_id=server_id)
258 @command(server_cmds)
259 class server_start(_init_cyclades, _optional_output_cmd):
260 """Start an existing server (VM)"""
263 @errors.cyclades.connection
264 @errors.cyclades.server_id
265 def _run(self, server_id):
266 self._optional_output(self.client.start_server(int(server_id)))
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_shutdown(_init_cyclades, _optional_output_cmd):
275 """Shutdown an active server (VM)"""
278 @errors.cyclades.connection
279 @errors.cyclades.server_id
280 def _run(self, server_id):
281 self._optional_output(self.client.shutdown_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_console(_init_cyclades, _optional_json):
290 """Get a VNC console to access an existing server (VM)
291 Console connection information provided (at least):
292 - host: (url or address) a VNC host
293 - port: (int) the gateway to enter VM on host
294 - password: for VNC authorization
298 @errors.cyclades.connection
299 @errors.cyclades.server_id
300 def _run(self, server_id):
302 self.client.get_server_console(int(server_id)), print_dict)
304 def main(self, server_id):
305 super(self.__class__, self)._run()
306 self._run(server_id=server_id)
309 @command(server_cmds)
310 class server_firewall(_init_cyclades):
311 """Manage server (VM) firewall profiles for public networks"""
314 @command(server_cmds)
315 class server_firewall_set(_init_cyclades, _optional_output_cmd):
316 """Set the server (VM) firewall profile on VMs public network
318 - DISABLED: Shutdown firewall
319 - ENABLED: Firewall in normal mode
320 - PROTECTED: Firewall in secure mode
324 @errors.cyclades.connection
325 @errors.cyclades.server_id
326 @errors.cyclades.firewall
327 def _run(self, server_id, profile):
328 self._optional_output(self.client.set_firewall_profile(
329 server_id=int(server_id), profile=('%s' % profile).upper()))
331 def main(self, server_id, profile):
332 super(self.__class__, self)._run()
333 self._run(server_id=server_id, profile=profile)
336 @command(server_cmds)
337 class server_firewall_get(_init_cyclades):
338 """Get the server (VM) firewall profile for its public network"""
341 @errors.cyclades.connection
342 @errors.cyclades.server_id
343 def _run(self, server_id):
344 print(self.client.get_firewall_profile(server_id))
346 def main(self, server_id):
347 super(self.__class__, self)._run()
348 self._run(server_id=server_id)
351 @command(server_cmds)
352 class server_addr(_init_cyclades, _optional_json):
353 """List the addresses of all network interfaces on a server (VM)"""
356 enum=FlagArgument('Enumerate results', '--enumerate')
360 @errors.cyclades.connection
361 @errors.cyclades.server_id
362 def _run(self, server_id):
363 reply = self.client.list_server_nics(int(server_id))
365 reply, with_enumeration=self['enum'] and len(reply) > 1)
367 def main(self, server_id):
368 super(self.__class__, self)._run()
369 self._run(server_id=server_id)
372 @command(server_cmds)
373 class server_metadata(_init_cyclades):
374 """Manage Server metadata (key:value pairs of server attributes)"""
377 @command(server_cmds)
378 class server_metadata_list(_init_cyclades, _optional_json):
379 """Get server metadata"""
382 @errors.cyclades.connection
383 @errors.cyclades.server_id
384 @errors.cyclades.metadata
385 def _run(self, server_id, key=''):
387 self.client.get_server_metadata(int(server_id), key), print_dict)
389 def main(self, server_id, key=''):
390 super(self.__class__, self)._run()
391 self._run(server_id=server_id, key=key)
394 @command(server_cmds)
395 class server_metadata_set(_init_cyclades, _optional_json):
396 """Set / update server(VM) metadata
397 Metadata should be given in key/value pairs in key=value format
399 /server metadata set <server id> key1=value1 key2=value2
400 Old, unreferenced metadata will remain intact
404 @errors.cyclades.connection
405 @errors.cyclades.server_id
406 def _run(self, server_id, keyvals):
407 assert keyvals, 'Please, add some metadata ( key=value)'
409 for keyval in keyvals:
410 k, sep, v = keyval.partition('=')
415 'Invalid piece of metadata %s' % keyval,
416 importance=2, details=[
417 'Correct metadata format: key=val',
419 '/server metadata set <server id>'
420 'key1=value1 key2=value2'])
422 self.client.update_server_metadata(int(server_id), **metadata),
425 def main(self, server_id, *key_equals_val):
426 super(self.__class__, self)._run()
427 self._run(server_id=server_id, keyvals=key_equals_val)
430 @command(server_cmds)
431 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
432 """Delete server (VM) metadata"""
435 @errors.cyclades.connection
436 @errors.cyclades.server_id
437 @errors.cyclades.metadata
438 def _run(self, server_id, key):
439 self._optional_output(
440 self.client.delete_server_metadata(int(server_id), key))
442 def main(self, server_id, key):
443 super(self.__class__, self)._run()
444 self._run(server_id=server_id, key=key)
447 @command(server_cmds)
448 class server_stats(_init_cyclades, _optional_json):
449 """Get server (VM) statistics"""
452 @errors.cyclades.connection
453 @errors.cyclades.server_id
454 def _run(self, server_id):
455 self._print(self.client.get_server_stats(int(server_id)), print_dict)
457 def main(self, server_id):
458 super(self.__class__, self)._run()
459 self._run(server_id=server_id)
462 @command(server_cmds)
463 class server_wait(_init_cyclades):
464 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
467 progress_bar=ProgressBarArgument(
468 'do not show progress bar',
469 ('-N', '--no-progress-bar'),
475 @errors.cyclades.connection
476 @errors.cyclades.server_id
477 def _run(self, server_id, currect_status):
478 (progress_bar, wait_cb) = self._safe_progress_bar(
479 'Server %s still in %s mode' % (server_id, currect_status))
482 new_mode = self.client.wait_server(
487 self._safe_progress_bar_finish(progress_bar)
490 self._safe_progress_bar_finish(progress_bar)
492 print('Server %s is now in %s mode' % (server_id, new_mode))
494 raiseCLIError(None, 'Time out')
496 def main(self, server_id, currect_status='BUILD'):
497 super(self.__class__, self)._run()
498 self._run(server_id=server_id, currect_status=currect_status)
501 @command(flavor_cmds)
502 class flavor_list(_init_cyclades, _optional_json):
503 """List available hardware flavors"""
506 detail=FlagArgument('show detailed output', ('-l', '--details')),
507 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
509 'output results in pages (-n to set items per page, default 10)',
511 enum=FlagArgument('Enumerate results', '--enumerate')
515 @errors.cyclades.connection
517 flavors = self.client.list_flavors(self['detail'])
518 pg_size = 10 if self['more'] and not self['limit'] else self['limit']
521 with_redundancy=self['detail'],
523 with_enumeration=self['enum'])
526 super(self.__class__, self)._run()
530 @command(flavor_cmds)
531 class flavor_info(_init_cyclades, _optional_json):
532 """Detailed information on a hardware flavor
533 To get a list of available flavors and flavor ids, try /flavor list
537 @errors.cyclades.connection
538 @errors.cyclades.flavor_id
539 def _run(self, flavor_id):
541 self.client.get_flavor_details(int(flavor_id)), print_dict)
543 def main(self, flavor_id):
544 super(self.__class__, self)._run()
545 self._run(flavor_id=flavor_id)
548 @command(network_cmds)
549 class network_info(_init_cyclades, _optional_json):
550 """Detailed information on a network
551 To get a list of available networks and network ids, try /network list
555 @errors.cyclades.connection
556 @errors.cyclades.network_id
557 def _run(self, network_id):
558 network = self.client.get_network_details(int(network_id))
559 self._print(network, print_dict, exclude=('id'))
561 def main(self, network_id):
562 super(self.__class__, self)._run()
563 self._run(network_id=network_id)
566 @command(network_cmds)
567 class network_list(_init_cyclades, _optional_json):
571 detail=FlagArgument('show detailed output', ('-l', '--details')),
572 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
574 'output results in pages (-n to set items per page, default 10)',
576 enum=FlagArgument('Enumerate results', '--enumerate')
580 @errors.cyclades.connection
582 networks = self.client.list_networks(self['detail'])
583 kwargs = dict(with_enumeration=self['enum'])
585 kwargs['page_size'] = self['limit'] or 10
587 networks = networks[:self['limit']]
588 self._print(networks, **kwargs)
591 super(self.__class__, self)._run()
595 @command(network_cmds)
596 class network_create(_init_cyclades, _optional_json):
597 """Create an (unconnected) network"""
600 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
601 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
602 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
604 'Valid network types are '
605 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
607 default='MAC_FILTERED')
611 @errors.cyclades.connection
612 @errors.cyclades.network_max
613 def _run(self, name):
614 self._print(self.client.create_network(
617 gateway=self['gateway'],
619 type=self['type']), print_dict)
621 def main(self, name):
622 super(self.__class__, self)._run()
626 @command(network_cmds)
627 class network_rename(_init_cyclades, _optional_output_cmd):
628 """Set the name of a network"""
631 @errors.cyclades.connection
632 @errors.cyclades.network_id
633 def _run(self, network_id, new_name):
634 self._optional_output(
635 self.client.update_network_name(int(network_id), new_name))
637 def main(self, network_id, new_name):
638 super(self.__class__, self)._run()
639 self._run(network_id=network_id, new_name=new_name)
642 @command(network_cmds)
643 class network_delete(_init_cyclades, _optional_output_cmd):
644 """Delete a network"""
647 @errors.cyclades.connection
648 @errors.cyclades.network_id
649 @errors.cyclades.network_in_use
650 def _run(self, network_id):
651 self._optional_output(self.client.delete_network(int(network_id)))
653 def main(self, network_id):
654 super(self.__class__, self)._run()
655 self._run(network_id=network_id)
658 @command(network_cmds)
659 class network_connect(_init_cyclades, _optional_output_cmd):
660 """Connect a server to a network"""
663 @errors.cyclades.connection
664 @errors.cyclades.server_id
665 @errors.cyclades.network_id
666 def _run(self, server_id, network_id):
667 self._optional_output(
668 self.client.connect_server(int(server_id), int(network_id)))
670 def main(self, server_id, network_id):
671 super(self.__class__, self)._run()
672 self._run(server_id=server_id, network_id=network_id)
675 @command(network_cmds)
676 class network_disconnect(_init_cyclades):
677 """Disconnect a nic that connects a server to a network
678 Nic ids are listed as "attachments" in detailed network information
679 To get detailed network information: /network info <network id>
682 @errors.cyclades.nic_format
683 def _server_id_from_nic(self, nic_id):
684 return nic_id.split('-')[1]
687 @errors.cyclades.connection
688 @errors.cyclades.server_id
689 @errors.cyclades.nic_id
690 def _run(self, nic_id, server_id):
691 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
692 if not num_of_disconnected:
694 'Network Interface %s not found on server %s' % (
698 print('Disconnected %s connections' % num_of_disconnected)
700 def main(self, nic_id):
701 super(self.__class__, self)._run()
702 server_id = self._server_id_from_nic(nic_id=nic_id)
703 self._run(nic_id=nic_id, server_id=server_id)