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, 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 remote.default.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_remote('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'])
122 kwargs = dict(with_enumeration=self['enum'])
124 kwargs['page_size'] = self['limit'] if self['limit'] else 10
126 servers = servers[:self['limit']]
127 self._print(servers, **kwargs)
130 super(self.__class__, self)._run()
134 @command(server_cmds)
135 class server_info(_init_cyclades, _optional_json):
136 """Detailed information on a Virtual Machine
138 - name, id, status, create/update dates
140 - metadata (e.g. os, superuser) and diagnostics
141 - hardware flavor and os image ids
145 @errors.cyclades.connection
146 @errors.cyclades.server_id
147 def _run(self, server_id):
148 self._print(self.client.get_server_details(server_id), print_dict)
150 def main(self, server_id):
151 super(self.__class__, self)._run()
152 self._run(server_id=server_id)
155 class PersonalityArgument(KeyValueArgument):
158 return self._value if hasattr(self, '_value') else []
161 def value(self, newvalue):
162 if newvalue == self.default:
165 for i, terms in enumerate(newvalue):
166 termlist = terms.split(',')
167 if len(termlist) > 5:
168 msg = 'Wrong number of terms (should be 1 to 5)'
169 raiseCLIError(CLISyntaxError(msg), details=howto_personality)
174 '--personality: File %s does not exist' % path,
176 details=howto_personality)
177 self._value.append(dict(path=path))
178 with open(path) as f:
179 self._value[i]['contents'] = b64encode(f.read())
181 self._value[i]['path'] = termlist[1]
182 self._value[i]['owner'] = termlist[2]
183 self._value[i]['group'] = termlist[3]
184 self._value[i]['mode'] = termlist[4]
189 @command(server_cmds)
190 class server_create(_init_cyclades, _optional_json):
191 """Create a server (aka Virtual Machine)
193 - name: (single quoted text)
194 - flavor id: Hardware flavor. Pick one from: /flavor list
195 - image id: OS images. Pick one from: /image list
199 personality=PersonalityArgument(
200 (80 * ' ').join(howto_personality), ('-p', '--personality'))
204 @errors.cyclades.connection
206 @errors.cyclades.flavor_id
207 def _run(self, name, flavor_id, image_id):
209 self.client.create_server(
210 name, int(flavor_id), image_id, self['personality']),
213 def main(self, name, flavor_id, image_id):
214 super(self.__class__, self)._run()
215 self._run(name=name, flavor_id=flavor_id, image_id=image_id)
218 @command(server_cmds)
219 class server_rename(_init_cyclades, _optional_output_cmd):
220 """Set/update a server (VM) name
221 VM names are not unique, therefore multiple servers may share the same name
225 @errors.cyclades.connection
226 @errors.cyclades.server_id
227 def _run(self, server_id, new_name):
228 self._optional_output(
229 self.client.update_server_name(int(server_id), new_name))
231 def main(self, server_id, new_name):
232 super(self.__class__, self)._run()
233 self._run(server_id=server_id, new_name=new_name)
236 @command(server_cmds)
237 class server_delete(_init_cyclades, _optional_output_cmd):
238 """Delete a server (VM)"""
241 @errors.cyclades.connection
242 @errors.cyclades.server_id
243 def _run(self, server_id):
244 self._optional_output(self.client.delete_server(int(server_id)))
246 def main(self, server_id):
247 super(self.__class__, self)._run()
248 self._run(server_id=server_id)
251 @command(server_cmds)
252 class server_reboot(_init_cyclades, _optional_output_cmd):
253 """Reboot a server (VM)"""
256 hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
260 @errors.cyclades.connection
261 @errors.cyclades.server_id
262 def _run(self, server_id):
263 self._optional_output(
264 self.client.reboot_server(int(server_id), self['hard']))
266 def main(self, server_id):
267 super(self.__class__, self)._run()
268 self._run(server_id=server_id)
271 @command(server_cmds)
272 class server_start(_init_cyclades, _optional_output_cmd):
273 """Start an existing server (VM)"""
276 @errors.cyclades.connection
277 @errors.cyclades.server_id
278 def _run(self, server_id):
279 self._optional_output(self.client.start_server(int(server_id)))
281 def main(self, server_id):
282 super(self.__class__, self)._run()
283 self._run(server_id=server_id)
286 @command(server_cmds)
287 class server_shutdown(_init_cyclades, _optional_output_cmd):
288 """Shutdown an active server (VM)"""
291 @errors.cyclades.connection
292 @errors.cyclades.server_id
293 def _run(self, server_id):
294 self._optional_output(self.client.shutdown_server(int(server_id)))
296 def main(self, server_id):
297 super(self.__class__, self)._run()
298 self._run(server_id=server_id)
301 @command(server_cmds)
302 class server_console(_init_cyclades, _optional_json):
303 """Get a VNC console to access an existing server (VM)
304 Console connection information provided (at least):
305 - host: (url or address) a VNC host
306 - port: (int) the gateway to enter VM on host
307 - password: for VNC authorization
311 @errors.cyclades.connection
312 @errors.cyclades.server_id
313 def _run(self, server_id):
315 self.client.get_server_console(int(server_id)), print_dict)
317 def main(self, server_id):
318 super(self.__class__, self)._run()
319 self._run(server_id=server_id)
322 @command(server_cmds)
323 class server_firewall(_init_cyclades):
324 """Manage server (VM) firewall profiles for public networks"""
327 @command(server_cmds)
328 class server_firewall_set(_init_cyclades, _optional_output_cmd):
329 """Set the server (VM) firewall profile on VMs public network
331 - DISABLED: Shutdown firewall
332 - ENABLED: Firewall in normal mode
333 - PROTECTED: Firewall in secure mode
337 @errors.cyclades.connection
338 @errors.cyclades.server_id
339 @errors.cyclades.firewall
340 def _run(self, server_id, profile):
341 self._optional_output(self.client.set_firewall_profile(
342 server_id=int(server_id), profile=('%s' % profile).upper()))
344 def main(self, server_id, profile):
345 super(self.__class__, self)._run()
346 self._run(server_id=server_id, profile=profile)
349 @command(server_cmds)
350 class server_firewall_get(_init_cyclades):
351 """Get the server (VM) firewall profile for its public network"""
354 @errors.cyclades.connection
355 @errors.cyclades.server_id
356 def _run(self, server_id):
357 print(self.client.get_firewall_profile(server_id))
359 def main(self, server_id):
360 super(self.__class__, self)._run()
361 self._run(server_id=server_id)
364 @command(server_cmds)
365 class server_addr(_init_cyclades, _optional_json):
366 """List the addresses of all network interfaces on a server (VM)"""
369 enum=FlagArgument('Enumerate results', '--enumerate')
373 @errors.cyclades.connection
374 @errors.cyclades.server_id
375 def _run(self, server_id):
376 reply = self.client.list_server_nics(int(server_id))
378 reply, with_enumeration=self['enum'] and len(reply) > 1)
380 def main(self, server_id):
381 super(self.__class__, self)._run()
382 self._run(server_id=server_id)
385 @command(server_cmds)
386 class server_metadata(_init_cyclades):
387 """Manage Server metadata (key:value pairs of server attributes)"""
390 @command(server_cmds)
391 class server_metadata_list(_init_cyclades, _optional_json):
392 """Get server metadata"""
395 @errors.cyclades.connection
396 @errors.cyclades.server_id
397 @errors.cyclades.metadata
398 def _run(self, server_id, key=''):
400 self.client.get_server_metadata(int(server_id), key), print_dict)
402 def main(self, server_id, key=''):
403 super(self.__class__, self)._run()
404 self._run(server_id=server_id, key=key)
407 @command(server_cmds)
408 class server_metadata_set(_init_cyclades, _optional_json):
409 """Set / update server(VM) metadata
410 Metadata should be given in key/value pairs in key=value format
412 /server metadata set <server id> key1=value1 key2=value2
413 Old, unreferenced metadata will remain intact
417 @errors.cyclades.connection
418 @errors.cyclades.server_id
419 def _run(self, server_id, keyvals):
420 assert keyvals, 'Please, add some metadata ( key=value)'
422 for keyval in keyvals:
423 k, sep, v = keyval.partition('=')
428 'Invalid piece of metadata %s' % keyval,
429 importance=2, details=[
430 'Correct metadata format: key=val',
432 '/server metadata set <server id>'
433 'key1=value1 key2=value2'])
435 self.client.update_server_metadata(int(server_id), **metadata),
438 def main(self, server_id, *key_equals_val):
439 super(self.__class__, self)._run()
440 self._run(server_id=server_id, keyvals=key_equals_val)
443 @command(server_cmds)
444 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
445 """Delete server (VM) metadata"""
448 @errors.cyclades.connection
449 @errors.cyclades.server_id
450 @errors.cyclades.metadata
451 def _run(self, server_id, key):
452 self._optional_output(
453 self.client.delete_server_metadata(int(server_id), key))
455 def main(self, server_id, key):
456 super(self.__class__, self)._run()
457 self._run(server_id=server_id, key=key)
460 @command(server_cmds)
461 class server_stats(_init_cyclades, _optional_json):
462 """Get server (VM) statistics"""
465 @errors.cyclades.connection
466 @errors.cyclades.server_id
467 def _run(self, server_id):
468 self._print(self.client.get_server_stats(int(server_id)), print_dict)
470 def main(self, server_id):
471 super(self.__class__, self)._run()
472 self._run(server_id=server_id)
475 @command(server_cmds)
476 class server_wait(_init_cyclades):
477 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
480 progress_bar=ProgressBarArgument(
481 'do not show progress bar',
482 ('-N', '--no-progress-bar'),
488 @errors.cyclades.connection
489 @errors.cyclades.server_id
490 def _run(self, server_id, currect_status):
491 (progress_bar, wait_cb) = self._safe_progress_bar(
492 'Server %s still in %s mode' % (server_id, currect_status))
495 new_mode = self.client.wait_server(
500 self._safe_progress_bar_finish(progress_bar)
503 self._safe_progress_bar_finish(progress_bar)
505 print('Server %s is now in %s mode' % (server_id, new_mode))
507 raiseCLIError(None, 'Time out')
509 def main(self, server_id, currect_status='BUILD'):
510 super(self.__class__, self)._run()
511 self._run(server_id=server_id, currect_status=currect_status)
514 @command(flavor_cmds)
515 class flavor_list(_init_cyclades, _optional_json):
516 """List available hardware flavors"""
519 detail=FlagArgument('show detailed output', ('-l', '--details')),
520 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
522 'output results in pages (-n to set items per page, default 10)',
524 enum=FlagArgument('Enumerate results', '--enumerate')
528 @errors.cyclades.connection
530 flavors = self.client.list_flavors(self['detail'])
531 pg_size = 10 if self['more'] and not self['limit'] else self['limit']
534 with_redundancy=self['detail'],
536 with_enumeration=self['enum'])
539 super(self.__class__, self)._run()
543 @command(flavor_cmds)
544 class flavor_info(_init_cyclades, _optional_json):
545 """Detailed information on a hardware flavor
546 To get a list of available flavors and flavor ids, try /flavor list
550 @errors.cyclades.connection
551 @errors.cyclades.flavor_id
552 def _run(self, flavor_id):
554 self.client.get_flavor_details(int(flavor_id)), print_dict)
556 def main(self, flavor_id):
557 super(self.__class__, self)._run()
558 self._run(flavor_id=flavor_id)
561 @command(network_cmds)
562 class network_info(_init_cyclades, _optional_json):
563 """Detailed information on a network
564 To get a list of available networks and network ids, try /network list
568 @errors.cyclades.connection
569 @errors.cyclades.network_id
570 def _run(self, network_id):
571 network = self.client.get_network_details(int(network_id))
572 self._print(network, print_dict, exclude=('id'))
574 def main(self, network_id):
575 super(self.__class__, self)._run()
576 self._run(network_id=network_id)
579 @command(network_cmds)
580 class network_list(_init_cyclades, _optional_json):
584 detail=FlagArgument('show detailed output', ('-l', '--details')),
585 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
587 'output results in pages (-n to set items per page, default 10)',
589 enum=FlagArgument('Enumerate results', '--enumerate')
593 @errors.cyclades.connection
595 networks = self.client.list_networks(self['detail'])
596 kwargs = dict(with_enumeration=self['enum'])
598 kwargs['page_size'] = self['limit'] or 10
600 networks = networks[:self['limit']]
601 self._print(networks, **kwargs)
604 super(self.__class__, self)._run()
608 @command(network_cmds)
609 class network_create(_init_cyclades, _optional_json):
610 """Create an (unconnected) network"""
613 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
614 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
615 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
617 'Valid network types are '
618 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
620 default='MAC_FILTERED')
624 @errors.cyclades.connection
625 @errors.cyclades.network_max
626 def _run(self, name):
627 self._print(self.client.create_network(
630 gateway=self['gateway'],
632 type=self['type']), print_dict)
634 def main(self, name):
635 super(self.__class__, self)._run()
639 @command(network_cmds)
640 class network_rename(_init_cyclades, _optional_output_cmd):
641 """Set the name of a network"""
644 @errors.cyclades.connection
645 @errors.cyclades.network_id
646 def _run(self, network_id, new_name):
647 self._optional_output(
648 self.client.update_network_name(int(network_id), new_name))
650 def main(self, network_id, new_name):
651 super(self.__class__, self)._run()
652 self._run(network_id=network_id, new_name=new_name)
655 @command(network_cmds)
656 class network_delete(_init_cyclades, _optional_output_cmd):
657 """Delete a network"""
660 @errors.cyclades.connection
661 @errors.cyclades.network_id
662 @errors.cyclades.network_in_use
663 def _run(self, network_id):
664 self._optional_output(self.client.delete_network(int(network_id)))
666 def main(self, network_id):
667 super(self.__class__, self)._run()
668 self._run(network_id=network_id)
671 @command(network_cmds)
672 class network_connect(_init_cyclades, _optional_output_cmd):
673 """Connect a server to a network"""
676 @errors.cyclades.connection
677 @errors.cyclades.server_id
678 @errors.cyclades.network_id
679 def _run(self, server_id, network_id):
680 self._optional_output(
681 self.client.connect_server(int(server_id), int(network_id)))
683 def main(self, server_id, network_id):
684 super(self.__class__, self)._run()
685 self._run(server_id=server_id, network_id=network_id)
688 @command(network_cmds)
689 class network_disconnect(_init_cyclades):
690 """Disconnect a nic that connects a server to a network
691 Nic ids are listed as "attachments" in detailed network information
692 To get detailed network information: /network info <network id>
695 @errors.cyclades.nic_format
696 def _server_id_from_nic(self, nic_id):
697 return nic_id.split('-')[1]
700 @errors.cyclades.connection
701 @errors.cyclades.server_id
702 @errors.cyclades.nic_id
703 def _run(self, nic_id, server_id):
704 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
705 if not num_of_disconnected:
707 'Network Interface %s not found on server %s' % (
711 print('Disconnected %s connections' % num_of_disconnected)
713 def main(self, nic_id):
714 super(self.__class__, self)._run()
715 server_id = self._server_id_from_nic(nic_id=nic_id)
716 self._run(nic_id=nic_id, server_id=server_id)