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 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'])
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
411 For example: /server metadata set <server id> key1=value1 key2=value2
412 Old, unreferenced metadata will remain intact
416 @errors.cyclades.connection
417 @errors.cyclades.server_id
418 def _run(self, server_id, keyvals):
419 assert keyvals, 'Please, add some metadata ( key=value)'
421 for keyval in keyvals:
422 k, sep, v = keyval.partition('=')
427 'Invalid piece of metadata %s' % keyval,
428 importance=2, details=[
429 'Correct metadata format: key=val',
431 '/server metadata set <server id>'
432 'key1=value1 key2=value2'])
434 self.client.update_server_metadata(int(server_id), **metadata),
437 def main(self, server_id, *key_equals_val):
438 super(self.__class__, self)._run()
439 self._run(server_id=server_id, keyvals=key_equals_val)
442 @command(server_cmds)
443 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
444 """Delete server (VM) metadata"""
447 @errors.cyclades.connection
448 @errors.cyclades.server_id
449 @errors.cyclades.metadata
450 def _run(self, server_id, key):
451 self._optional_output(
452 self.client.delete_server_metadata(int(server_id), key))
454 def main(self, server_id, key):
455 super(self.__class__, self)._run()
456 self._run(server_id=server_id, key=key)
459 @command(server_cmds)
460 class server_stats(_init_cyclades, _optional_json):
461 """Get server (VM) statistics"""
464 @errors.cyclades.connection
465 @errors.cyclades.server_id
466 def _run(self, server_id):
467 self._print(self.client.get_server_stats(int(server_id)), print_dict)
469 def main(self, server_id):
470 super(self.__class__, self)._run()
471 self._run(server_id=server_id)
474 @command(server_cmds)
475 class server_wait(_init_cyclades):
476 """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
479 progress_bar=ProgressBarArgument(
480 'do not show progress bar',
481 ('-N', '--no-progress-bar'),
487 @errors.cyclades.connection
488 @errors.cyclades.server_id
489 def _run(self, server_id, currect_status):
490 (progress_bar, wait_cb) = self._safe_progress_bar(
491 'Server %s still in %s mode' % (server_id, currect_status))
494 new_mode = self.client.wait_server(
499 self._safe_progress_bar_finish(progress_bar)
502 self._safe_progress_bar_finish(progress_bar)
504 print('Server %s is now in %s mode' % (server_id, new_mode))
506 raiseCLIError(None, 'Time out')
508 def main(self, server_id, currect_status='BUILD'):
509 super(self.__class__, self)._run()
510 self._run(server_id=server_id, currect_status=currect_status)
513 @command(flavor_cmds)
514 class flavor_list(_init_cyclades, _optional_json):
515 """List available hardware flavors"""
518 detail=FlagArgument('show detailed output', ('-l', '--details')),
519 limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
521 'output results in pages (-n to set items per page, default 10)',
523 enum=FlagArgument('Enumerate results', '--enumerate')
527 @errors.cyclades.connection
529 flavors = self.client.list_flavors(self['detail'])
530 pg_size = 10 if self['more'] and not self['limit'] else self['limit']
533 with_redundancy=self['detail'],
535 with_enumeration=self['enum'])
538 super(self.__class__, self)._run()
542 @command(flavor_cmds)
543 class flavor_info(_init_cyclades, _optional_json):
544 """Detailed information on a hardware flavor
545 To get a list of available flavors and flavor ids, try /flavor list
549 @errors.cyclades.connection
550 @errors.cyclades.flavor_id
551 def _run(self, flavor_id):
553 self.client.get_flavor_details(int(flavor_id)), print_dict)
555 def main(self, flavor_id):
556 super(self.__class__, self)._run()
557 self._run(flavor_id=flavor_id)
560 @command(network_cmds)
561 class network_info(_init_cyclades, _optional_json):
562 """Detailed information on a network
563 To get a list of available networks and network ids, try /network list
567 @errors.cyclades.connection
568 @errors.cyclades.network_id
569 def _run(self, network_id):
570 network = self.client.get_network_details(int(network_id))
571 self._print(network, print_dict, exclude=('id'))
573 def main(self, network_id):
574 super(self.__class__, self)._run()
575 self._run(network_id=network_id)
578 @command(network_cmds)
579 class network_list(_init_cyclades, _optional_json):
583 detail=FlagArgument('show detailed output', ('-l', '--details')),
584 limit=IntArgument('limit # of listed networks', ('-n', '--number')),
586 'output results in pages (-n to set items per page, default 10)',
588 enum=FlagArgument('Enumerate results', '--enumerate')
592 @errors.cyclades.connection
594 networks = self.client.list_networks(self['detail'])
595 kwargs = dict(with_enumeration=self['enum'])
597 kwargs['page_size'] = self['limit'] or 10
599 networks = networks[:self['limit']]
600 self._print(networks, **kwargs)
603 super(self.__class__, self)._run()
607 @command(network_cmds)
608 class network_create(_init_cyclades, _optional_json):
609 """Create an (unconnected) network"""
612 cidr=ValueArgument('explicitly set cidr', '--with-cidr'),
613 gateway=ValueArgument('explicitly set gateway', '--with-gateway'),
614 dhcp=FlagArgument('Use dhcp (default: off)', '--with-dhcp'),
616 'Valid network types are '
617 'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
619 default='MAC_FILTERED')
623 @errors.cyclades.connection
624 @errors.cyclades.network_max
625 def _run(self, name):
626 self._print(self.client.create_network(
629 gateway=self['gateway'],
631 type=self['type']), print_dict)
633 def main(self, name):
634 super(self.__class__, self)._run()
638 @command(network_cmds)
639 class network_rename(_init_cyclades, _optional_output_cmd):
640 """Set the name of a network"""
643 @errors.cyclades.connection
644 @errors.cyclades.network_id
645 def _run(self, network_id, new_name):
646 self._optional_output(
647 self.client.update_network_name(int(network_id), new_name))
649 def main(self, network_id, new_name):
650 super(self.__class__, self)._run()
651 self._run(network_id=network_id, new_name=new_name)
654 @command(network_cmds)
655 class network_delete(_init_cyclades, _optional_output_cmd):
656 """Delete a network"""
659 @errors.cyclades.connection
660 @errors.cyclades.network_id
661 @errors.cyclades.network_in_use
662 def _run(self, network_id):
663 self._optional_output(self.client.delete_network(int(network_id)))
665 def main(self, network_id):
666 super(self.__class__, self)._run()
667 self._run(network_id=network_id)
670 @command(network_cmds)
671 class network_connect(_init_cyclades, _optional_output_cmd):
672 """Connect a server to a network"""
675 @errors.cyclades.connection
676 @errors.cyclades.server_id
677 @errors.cyclades.network_id
678 def _run(self, server_id, network_id):
679 self._optional_output(
680 self.client.connect_server(int(server_id), int(network_id)))
682 def main(self, server_id, network_id):
683 super(self.__class__, self)._run()
684 self._run(server_id=server_id, network_id=network_id)
687 @command(network_cmds)
688 class network_disconnect(_init_cyclades):
689 """Disconnect a nic that connects a server to a network
690 Nic ids are listed as "attachments" in detailed network information
691 To get detailed network information: /network info <network id>
694 @errors.cyclades.nic_format
695 def _server_id_from_nic(self, nic_id):
696 return nic_id.split('-')[1]
699 @errors.cyclades.connection
700 @errors.cyclades.server_id
701 @errors.cyclades.nic_id
702 def _run(self, nic_id, server_id):
703 num_of_disconnected = self.client.disconnect_server(server_id, nic_id)
704 if not num_of_disconnected:
706 'Network Interface %s not found on server %s' % (
710 print('Disconnected %s connections' % num_of_disconnected)
712 def main(self, nic_id):
713 super(self.__class__, self)._run()
714 server_id = self._server_id_from_nic(nic_id=nic_id)
715 self._run(nic_id=nic_id, server_id=server_id)