Get endpoint urls for all CLI operations
[kamaki] / kamaki / cli / commands / cyclades.py
1 # Copyright 2011-2013 GRNET S.A. All rights reserved.
2 #
3 # Redistribution and use in source and binary forms, with or
4 # without modification, are permitted provided that the following
5 # conditions are met:
6 #
7 #   1. Redistributions of source code must retain the above
8 #      copyright notice, this list of conditions and the following
9 #      disclaimer.
10 #
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.
15 #
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.
28 #
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.
33
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
43
44 from base64 import b64encode
45 from os.path import exists
46
47
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]
52
53
54 about_authentication = '\nUser Authentication:\
55     \n* to check authentication: /user authenticate\
56     \n* to set authentication token: /config set token <token>'
57
58 howto_personality = [
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)']
66
67
68 class _init_cyclades(_command_init):
69     @errors.generic.all
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()
80
81     def main(self):
82         self._run()
83
84
85 @command(server_cmds)
86 class server_list(_init_cyclades, _optional_json):
87     """List Virtual Machines accessible by user"""
88
89     __doc__ += about_authentication
90
91     arguments = dict(
92         detail=FlagArgument('show detailed output', ('-l', '--details')),
93         since=DateArgument(
94             'show only items since date (\' d/m/Y H:M:S \')',
95             '--since'),
96         limit=IntArgument('limit number of listed VMs', ('-n', '--number')),
97         more=FlagArgument(
98             'output results in pages (-n to set items per page, default 10)',
99             '--more'),
100         enum=FlagArgument('Enumerate results', '--enumerate')
101     )
102
103     @errors.generic.all
104     @errors.cyclades.connection
105     @errors.cyclades.date
106     def _run(self):
107         servers = self.client.list_servers(self['detail'], self['since'])
108
109         kwargs = dict(with_enumeration=self['enum'])
110         if self['more']:
111             kwargs['page_size'] = self['limit'] if self['limit'] else 10
112         elif self['limit']:
113             servers = servers[:self['limit']]
114         self._print(servers, **kwargs)
115
116     def main(self):
117         super(self.__class__, self)._run()
118         self._run()
119
120
121 @command(server_cmds)
122 class server_info(_init_cyclades, _optional_json):
123     """Detailed information on a Virtual Machine
124     Contains:
125     - name, id, status, create/update dates
126     - network interfaces
127     - metadata (e.g. os, superuser) and diagnostics
128     - hardware flavor and os image ids
129     """
130
131     @errors.generic.all
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)
136
137     def main(self, server_id):
138         super(self.__class__, self)._run()
139         self._run(server_id=server_id)
140
141
142 class PersonalityArgument(KeyValueArgument):
143     @property
144     def value(self):
145         return self._value if hasattr(self, '_value') else []
146
147     @value.setter
148     def value(self, newvalue):
149         if newvalue == self.default:
150             return self.value
151         self._value = []
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)
157             path = termlist[0]
158             if not exists(path):
159                 raiseCLIError(
160                     None,
161                     '--personality: File %s does not exist' % path,
162                     importance=1,
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())
167             try:
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]
172             except IndexError:
173                 pass
174
175
176 @command(server_cmds)
177 class server_create(_init_cyclades, _optional_json):
178     """Create a server (aka Virtual Machine)
179     Parameters:
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
183     """
184
185     arguments = dict(
186         personality=PersonalityArgument(
187             (80 * ' ').join(howto_personality), ('-p', '--personality'))
188     )
189
190     @errors.generic.all
191     @errors.cyclades.connection
192     @errors.plankton.id
193     @errors.cyclades.flavor_id
194     def _run(self, name, flavor_id, image_id):
195         self._print(
196             self.client.create_server(
197                 name, int(flavor_id), image_id, self['personality']),
198             print_dict)
199
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)
203
204
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
209     """
210
211     @errors.generic.all
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))
217
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)
221
222
223 @command(server_cmds)
224 class server_delete(_init_cyclades, _optional_output_cmd):
225     """Delete a server (VM)"""
226
227     @errors.generic.all
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)))
232
233     def main(self, server_id):
234         super(self.__class__, self)._run()
235         self._run(server_id=server_id)
236
237
238 @command(server_cmds)
239 class server_reboot(_init_cyclades, _optional_output_cmd):
240     """Reboot a server (VM)"""
241
242     arguments = dict(
243         hard=FlagArgument('perform a hard reboot', ('-f', '--force'))
244     )
245
246     @errors.generic.all
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']))
252
253     def main(self, server_id):
254         super(self.__class__, self)._run()
255         self._run(server_id=server_id)
256
257
258 @command(server_cmds)
259 class server_start(_init_cyclades, _optional_output_cmd):
260     """Start an existing server (VM)"""
261
262     @errors.generic.all
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)))
267
268     def main(self, server_id):
269         super(self.__class__, self)._run()
270         self._run(server_id=server_id)
271
272
273 @command(server_cmds)
274 class server_shutdown(_init_cyclades, _optional_output_cmd):
275     """Shutdown an active server (VM)"""
276
277     @errors.generic.all
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)))
282
283     def main(self, server_id):
284         super(self.__class__, self)._run()
285         self._run(server_id=server_id)
286
287
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
295     """
296
297     @errors.generic.all
298     @errors.cyclades.connection
299     @errors.cyclades.server_id
300     def _run(self, server_id):
301         self._print(
302             self.client.get_server_console(int(server_id)), print_dict)
303
304     def main(self, server_id):
305         super(self.__class__, self)._run()
306         self._run(server_id=server_id)
307
308
309 @command(server_cmds)
310 class server_firewall(_init_cyclades):
311     """Manage server (VM) firewall profiles for public networks"""
312
313
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
317     Values for profile:
318     - DISABLED: Shutdown firewall
319     - ENABLED: Firewall in normal mode
320     - PROTECTED: Firewall in secure mode
321     """
322
323     @errors.generic.all
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()))
330
331     def main(self, server_id, profile):
332         super(self.__class__, self)._run()
333         self._run(server_id=server_id, profile=profile)
334
335
336 @command(server_cmds)
337 class server_firewall_get(_init_cyclades):
338     """Get the server (VM) firewall profile for its public network"""
339
340     @errors.generic.all
341     @errors.cyclades.connection
342     @errors.cyclades.server_id
343     def _run(self, server_id):
344         print(self.client.get_firewall_profile(server_id))
345
346     def main(self, server_id):
347         super(self.__class__, self)._run()
348         self._run(server_id=server_id)
349
350
351 @command(server_cmds)
352 class server_addr(_init_cyclades, _optional_json):
353     """List the addresses of all network interfaces on a server (VM)"""
354
355     arguments = dict(
356         enum=FlagArgument('Enumerate results', '--enumerate')
357     )
358
359     @errors.generic.all
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))
364         self._print(
365             reply, with_enumeration=self['enum'] and len(reply) > 1)
366
367     def main(self, server_id):
368         super(self.__class__, self)._run()
369         self._run(server_id=server_id)
370
371
372 @command(server_cmds)
373 class server_metadata(_init_cyclades):
374     """Manage Server metadata (key:value pairs of server attributes)"""
375
376
377 @command(server_cmds)
378 class server_metadata_list(_init_cyclades, _optional_json):
379     """Get server metadata"""
380
381     @errors.generic.all
382     @errors.cyclades.connection
383     @errors.cyclades.server_id
384     @errors.cyclades.metadata
385     def _run(self, server_id, key=''):
386         self._print(
387             self.client.get_server_metadata(int(server_id), key), print_dict)
388
389     def main(self, server_id, key=''):
390         super(self.__class__, self)._run()
391         self._run(server_id=server_id, key=key)
392
393
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
398     For example:
399         /server metadata set <server id> key1=value1 key2=value2
400     Old, unreferenced metadata will remain intact
401     """
402
403     @errors.generic.all
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)'
408         metadata = dict()
409         for keyval in keyvals:
410             k, sep, v = keyval.partition('=')
411             if sep and k:
412                 metadata[k] = v
413             else:
414                 raiseCLIError(
415                     'Invalid piece of metadata %s' % keyval,
416                     importance=2, details=[
417                         'Correct metadata format: key=val',
418                         'For example:',
419                         '/server metadata set <server id>'
420                         'key1=value1 key2=value2'])
421         self._print(
422             self.client.update_server_metadata(int(server_id), **metadata),
423             print_dict)
424
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)
428
429
430 @command(server_cmds)
431 class server_metadata_delete(_init_cyclades, _optional_output_cmd):
432     """Delete server (VM) metadata"""
433
434     @errors.generic.all
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))
441
442     def main(self, server_id, key):
443         super(self.__class__, self)._run()
444         self._run(server_id=server_id, key=key)
445
446
447 @command(server_cmds)
448 class server_stats(_init_cyclades, _optional_json):
449     """Get server (VM) statistics"""
450
451     @errors.generic.all
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)
456
457     def main(self, server_id):
458         super(self.__class__, self)._run()
459         self._run(server_id=server_id)
460
461
462 @command(server_cmds)
463 class server_wait(_init_cyclades):
464     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
465
466     arguments = dict(
467         progress_bar=ProgressBarArgument(
468             'do not show progress bar',
469             ('-N', '--no-progress-bar'),
470             False
471         )
472     )
473
474     @errors.generic.all
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))
480
481         try:
482             new_mode = self.client.wait_server(
483                 server_id,
484                 currect_status,
485                 wait_cb=wait_cb)
486         except Exception:
487             self._safe_progress_bar_finish(progress_bar)
488             raise
489         finally:
490             self._safe_progress_bar_finish(progress_bar)
491         if new_mode:
492             print('Server %s is now in %s mode' % (server_id, new_mode))
493         else:
494             raiseCLIError(None, 'Time out')
495
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)
499
500
501 @command(flavor_cmds)
502 class flavor_list(_init_cyclades, _optional_json):
503     """List available hardware flavors"""
504
505     arguments = dict(
506         detail=FlagArgument('show detailed output', ('-l', '--details')),
507         limit=IntArgument('limit # of listed flavors', ('-n', '--number')),
508         more=FlagArgument(
509             'output results in pages (-n to set items per page, default 10)',
510             '--more'),
511         enum=FlagArgument('Enumerate results', '--enumerate')
512     )
513
514     @errors.generic.all
515     @errors.cyclades.connection
516     def _run(self):
517         flavors = self.client.list_flavors(self['detail'])
518         pg_size = 10 if self['more'] and not self['limit'] else self['limit']
519         self._print(
520             flavors,
521             with_redundancy=self['detail'],
522             page_size=pg_size,
523             with_enumeration=self['enum'])
524
525     def main(self):
526         super(self.__class__, self)._run()
527         self._run()
528
529
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
534     """
535
536     @errors.generic.all
537     @errors.cyclades.connection
538     @errors.cyclades.flavor_id
539     def _run(self, flavor_id):
540         self._print(
541             self.client.get_flavor_details(int(flavor_id)), print_dict)
542
543     def main(self, flavor_id):
544         super(self.__class__, self)._run()
545         self._run(flavor_id=flavor_id)
546
547
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
552     """
553
554     @errors.generic.all
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'))
560
561     def main(self, network_id):
562         super(self.__class__, self)._run()
563         self._run(network_id=network_id)
564
565
566 @command(network_cmds)
567 class network_list(_init_cyclades, _optional_json):
568     """List networks"""
569
570     arguments = dict(
571         detail=FlagArgument('show detailed output', ('-l', '--details')),
572         limit=IntArgument('limit # of listed networks', ('-n', '--number')),
573         more=FlagArgument(
574             'output results in pages (-n to set items per page, default 10)',
575             '--more'),
576         enum=FlagArgument('Enumerate results', '--enumerate')
577     )
578
579     @errors.generic.all
580     @errors.cyclades.connection
581     def _run(self):
582         networks = self.client.list_networks(self['detail'])
583         kwargs = dict(with_enumeration=self['enum'])
584         if self['more']:
585             kwargs['page_size'] = self['limit'] or 10
586         elif self['limit']:
587             networks = networks[:self['limit']]
588         self._print(networks, **kwargs)
589
590     def main(self):
591         super(self.__class__, self)._run()
592         self._run()
593
594
595 @command(network_cmds)
596 class network_create(_init_cyclades, _optional_json):
597     """Create an (unconnected) network"""
598
599     arguments = dict(
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'),
603         type=ValueArgument(
604             'Valid network types are '
605             'CUSTOM, IP_LESS_ROUTED, MAC_FILTERED (default), PHYSICAL_VLAN',
606             '--with-type',
607             default='MAC_FILTERED')
608     )
609
610     @errors.generic.all
611     @errors.cyclades.connection
612     @errors.cyclades.network_max
613     def _run(self, name):
614         self._print(self.client.create_network(
615             name,
616             cidr=self['cidr'],
617             gateway=self['gateway'],
618             dhcp=self['dhcp'],
619             type=self['type']), print_dict)
620
621     def main(self, name):
622         super(self.__class__, self)._run()
623         self._run(name)
624
625
626 @command(network_cmds)
627 class network_rename(_init_cyclades, _optional_output_cmd):
628     """Set the name of a network"""
629
630     @errors.generic.all
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))
636
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)
640
641
642 @command(network_cmds)
643 class network_delete(_init_cyclades, _optional_output_cmd):
644     """Delete a network"""
645
646     @errors.generic.all
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)))
652
653     def main(self, network_id):
654         super(self.__class__, self)._run()
655         self._run(network_id=network_id)
656
657
658 @command(network_cmds)
659 class network_connect(_init_cyclades, _optional_output_cmd):
660     """Connect a server to a network"""
661
662     @errors.generic.all
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)))
669
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)
673
674
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>
680     """
681
682     @errors.cyclades.nic_format
683     def _server_id_from_nic(self, nic_id):
684         return nic_id.split('-')[1]
685
686     @errors.generic.all
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:
693             raise ClientError(
694                 'Network Interface %s not found on server %s' % (
695                     nic_id,
696                     server_id),
697                 status=404)
698         print('Disconnected %s connections' % num_of_disconnected)
699
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)