Introduce enumrated list/dict print
[kamaki] / kamaki / cli / commands / cyclades_cli.py
1 # Copyright 2012 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, print_list, bold
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
41 from kamaki.cli.commands import _command_init
42
43 from base64 import b64encode
44 from os.path import exists
45
46
47 server_cmds = CommandTree('server',
48     'Compute/Cyclades API server commands')
49 flavor_cmds = CommandTree('flavor',
50     'Compute/Cyclades API flavor commands')
51 image_cmds = CommandTree('image',
52     'Compute/Cyclades or Glance API image commands')
53 network_cmds = CommandTree('network',
54     'Compute/Cyclades API network commands')
55 _commands = [server_cmds, flavor_cmds, image_cmds, network_cmds]
56
57
58 class _init_cyclades(_command_init):
59     def main(self, service='compute'):
60         token = self.config.get(service, 'token')\
61             or self.config.get('global', 'token')
62         base_url = self.config.get(service, 'url')\
63             or self.config.get('global', 'url')
64         self.client = CycladesClient(base_url=base_url, token=token)
65
66
67 @command(server_cmds)
68 class server_list(_init_cyclades):
69     """List servers"""
70
71     def __init__(self, arguments={}):
72         super(server_list, self).__init__(arguments)
73         self.arguments['detail'] = FlagArgument('show detailed output', '-l')
74
75     def _info_print(self, server):
76         addr_dict = {}
77         if 'attachments' in server:
78             for addr in server['attachments']['values']:
79                 ips = addr.pop('values', [])
80                 for ip in ips:
81                     addr['IPv%s' % ip['version']] = ip['addr']
82                 if 'firewallProfile' in addr:
83                     addr['firewall'] = addr.pop('firewallProfile')
84                 addr_dict[addr.pop('id')] = addr
85             server['attachments'] = addr_dict if addr_dict is not {} else None
86         if 'metadata' in server:
87             server['metadata'] = server['metadata']['values']
88         print_dict(server, ident=1)
89
90     def _print(self, servers):
91         for server in servers:
92             sname = server.pop('name')
93             sid = server.pop('id')
94             print('%s (%s)' % (sid, bold(sname)))
95             if self.get_argument('detail'):
96                 self._info_print(server)
97                 print(' ')
98
99     def main(self):
100         super(self.__class__, self).main()
101         try:
102             servers = self.client.list_servers(self.get_argument('detail'))
103             self._print(servers)
104         except Exception as err:
105             raiseCLIError(err)
106
107
108 @command(server_cmds)
109 class server_info(_init_cyclades):
110     """Get server details"""
111
112     @classmethod
113     def _print(self, server):
114         addr_dict = {}
115         if 'attachments' in server:
116             atts = server.pop('attachments')
117             for addr in atts['values']:
118                 ips = addr.pop('values', [])
119                 for ip in ips:
120                     addr['IPv%s' % ip['version']] = ip['addr']
121                 if 'firewallProfile' in addr:
122                     addr['firewall'] = addr.pop('firewallProfile')
123                 addr_dict[addr.pop('id')] = addr
124             server['attachments'] = addr_dict if addr_dict else None
125         if 'metadata' in server:
126             server['metadata'] = server['metadata']['values']
127         print_dict(server, ident=1)
128
129     def main(self, server_id):
130         super(self.__class__, self).main()
131         try:
132             server = self.client.get_server_details(int(server_id))
133         except ValueError as err:
134             raiseCLIError(err, 'Server id must be positive integer', 1)
135         except Exception as err:
136             raiseCLIError(err)
137         self._print(server)
138
139
140 class PersonalityArgument(KeyValueArgument):
141     @property
142     def value(self):
143         return self._value if hasattr(self, '_value') else []
144
145     @value.setter
146     def value(self, newvalue):
147         if newvalue == self.default:
148             return self.value
149         self._value = []
150         for i, terms in enumerate(newvalue):
151             termlist = terms.split(',')
152             if len(termlist) > 5:
153                 raiseCLIError(CLISyntaxError(details='Wrong number of terms'\
154                 + ' ("PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]"'))
155             path = termlist[0]
156             if not exists(path):
157                 raiseCLIError(None, "File %s does not exist" % path, 1)
158             self._value.append(dict(path=path))
159             with open(path) as f:
160                 self._value[i]['contents'] = b64encode(f.read())
161             try:
162                 self._value[i]['path'] = termlist[1]
163                 self._value[i]['owner'] = termlist[2]
164                 self._value[i]['group'] = termlist[3]
165                 self._value[i]['mode'] = termlist[4]
166             except IndexError:
167                 pass
168
169
170 @command(server_cmds)
171 class server_create(_init_cyclades):
172     """Create a server"""
173
174     def __init__(self, arguments={}):
175         super(server_create, self).__init__(arguments)
176         self.arguments['personality'] = PersonalityArgument(\
177             'add one or more personality files ( ' +\
178             '"PATH,[SERVER_PATH,[OWNER,[GROUP,[MODE]]]]" )',
179             parsed_name='--personality')
180
181     def main(self, name, flavor_id, image_id):
182         super(self.__class__, self).main()
183
184         try:
185             reply = self.client.create_server(name,
186                 int(flavor_id),
187                 image_id,
188                 self.get_argument('personality'))
189         except ClientError as err:
190             raiseCLIError(err)
191         except ValueError as err:
192             raiseCLIError(err, 'Invalid flavor id %s ' % flavor_id,
193                 details='Flavor id must be a positive integer',
194                 importance=1)
195         except Exception as err:
196             raiseCLIError(err, 'Syntax error: %s\n' % err, importance=1)
197         print_dict(reply)
198
199
200 @command(server_cmds)
201 class server_rename(_init_cyclades):
202     """Update a server's name"""
203
204     def main(self, server_id, new_name):
205         super(self.__class__, self).main()
206         try:
207             self.client.update_server_name(int(server_id), new_name)
208         except ClientError as err:
209             raiseCLIError(err)
210         except ValueError as err:
211             raiseCLIError(err, 'Invalid server id %s ' % server_id,
212                 details='Server id must be positive integer\n',
213                 importance=1)
214
215
216 @command(server_cmds)
217 class server_delete(_init_cyclades):
218     """Delete a server"""
219
220     def main(self, server_id):
221         super(self.__class__, self).main()
222         try:
223             self.client.delete_server(int(server_id))
224         except ValueError as err:
225             raiseCLIError(err, 'Server id must be positive integer', 1)
226         except Exception as err:
227             raiseCLIError(err)
228
229
230 @command(server_cmds)
231 class server_reboot(_init_cyclades):
232     """Reboot a server"""
233
234     def __init__(self, arguments={}):
235         super(server_reboot, self).__init__(arguments)
236         self.arguments['hard'] = FlagArgument('perform a hard reboot', '-f')
237
238     def main(self, server_id):
239         super(self.__class__, self).main()
240         try:
241             self.client.reboot_server(int(server_id),
242                 self.get_argument('hard'))
243         except ValueError as err:
244             raiseCLIError(err, 'Server id must be positive integer', 1)
245         except Exception as err:
246             raiseCLIError(err)
247
248
249 @command(server_cmds)
250 class server_start(_init_cyclades):
251     """Start a server"""
252
253     def main(self, server_id):
254         super(self.__class__, self).main()
255         try:
256             self.client.start_server(int(server_id))
257         except ValueError as err:
258             raiseCLIError(err, 'Server id must be positive integer', 1)
259         except Exception as err:
260             raiseCLIError(err)
261
262
263 @command(server_cmds)
264 class server_shutdown(_init_cyclades):
265     """Shutdown a server"""
266
267     def main(self, server_id):
268         super(self.__class__, self).main()
269         try:
270             self.client.shutdown_server(int(server_id))
271         except ValueError as err:
272             raiseCLIError(err, 'Server id must be positive integer', 1)
273         except Exception as err:
274             raiseCLIError(err)
275
276
277 @command(server_cmds)
278 class server_console(_init_cyclades):
279     """Get a VNC console"""
280
281     def main(self, server_id):
282         super(self.__class__, self).main()
283         try:
284             reply = self.client.get_server_console(int(server_id))
285         except ValueError as err:
286             raiseCLIError(err, 'Server id must be positive integer', 1)
287         except Exception as err:
288             raiseCLIError(err)
289         print_dict(reply)
290
291
292 @command(server_cmds)
293 class server_firewall(_init_cyclades):
294     """Set the server's firewall profile"""
295
296     def main(self, server_id, profile):
297         super(self.__class__, self).main()
298         try:
299             self.client.set_firewall_profile(int(server_id), profile)
300         except ValueError as err:
301             raiseCLIError(err, 'Server id must be positive integer', 1)
302         except Exception as err:
303             raiseCLIError(err)
304
305
306 @command(server_cmds)
307 class server_addr(_init_cyclades):
308     """List a server's nic address"""
309
310     def main(self, server_id):
311         super(self.__class__, self).main()
312         try:
313             reply = self.client.list_server_nics(int(server_id))
314         except ValueError as err:
315             raiseCLIError(err, 'Server id must be positive integer', 1)
316         except Exception as err:
317             raiseCLIError(err)
318         print_list(reply, with_enumeration=True)
319
320
321 @command(server_cmds)
322 class server_meta(_init_cyclades):
323     """Get a server's metadata"""
324
325     def main(self, server_id, key=''):
326         super(self.__class__, self).main()
327         try:
328             reply = self.client.get_server_metadata(int(server_id), key)
329         except ValueError as err:
330             raiseCLIError(err, 'Server id must be positive integer', 1)
331         except Exception as err:
332             raiseCLIError(err)
333         print_dict(reply)
334
335
336 @command(server_cmds)
337 class server_addmeta(_init_cyclades):
338     """Add server metadata"""
339
340     def main(self, server_id, key, val):
341         super(self.__class__, self).main()
342         try:
343             reply = self.client.create_server_metadata(\
344                 int(server_id), key, val)
345         except ValueError as err:
346             raiseCLIError(err, 'Server id must be positive integer', 1)
347         except Exception as err:
348             raiseCLIError(err)
349         print_dict(reply)
350
351
352 @command(server_cmds)
353 class server_setmeta(_init_cyclades):
354     """Update server's metadata"""
355
356     def main(self, server_id, key, val):
357         super(self.__class__, self).main()
358         metadata = {key: val}
359         try:
360             reply = self.client.update_server_metadata(int(server_id),
361                 **metadata)
362         except ValueError as err:
363             raiseCLIError(err, 'Server id must be positive integer', 1)
364         except Exception as err:
365             raiseCLIError(err)
366         print_dict(reply)
367
368
369 @command(server_cmds)
370 class server_delmeta(_init_cyclades):
371     """Delete server metadata"""
372
373     def main(self, server_id, key):
374         super(self.__class__, self).main()
375         try:
376             self.client.delete_server_metadata(int(server_id), key)
377         except ValueError as err:
378             raiseCLIError(err, 'Server id must be positive integer', 1)
379         except Exception as err:
380             raiseCLIError(err)
381
382
383 @command(server_cmds)
384 class server_stats(_init_cyclades):
385     """Get server statistics"""
386
387     def main(self, server_id):
388         super(self.__class__, self).main()
389         try:
390             reply = self.client.get_server_stats(int(server_id))
391         except ValueError as err:
392             raiseCLIError(err, 'Server id must be positive integer', 1)
393         except Exception as err:
394             raiseCLIError(err)
395         print_dict(reply, exclude=('serverRef',))
396
397
398 @command(server_cmds)
399 class server_wait(_init_cyclades):
400     """Wait for server to finish [BUILD, STOPPED, REBOOT, ACTIVE]"""
401
402     def __init__(self, arguments={}):
403         super(self.__class__, self).__init__(arguments)
404         self.arguments['progress_bar'] = ProgressBarArgument(\
405             'do not show progress bar', '--no-progress-bar', False)
406
407     def main(self, server_id, currect_status='BUILD'):
408         super(self.__class__, self).main()
409         try:
410             progress_bar = self.arguments['progress_bar']
411             wait_cb = progress_bar.get_generator(\
412                 'Server %s still in %s mode' % (server_id, currect_status))
413         except Exception:
414             wait_cb = None
415         try:
416             new_mode = self.client.wait_server(server_id,
417                 currect_status,
418                 wait_cb=wait_cb)
419             progress_bar.finish()
420         except KeyboardInterrupt:
421             print('\nCanceled')
422             progress_bar.finish()
423             return
424         except ClientError as err:
425             progress_bar.finish()
426             raiseCLIError(err)
427         if new_mode:
428             print('Server %s is now in %s mode' % (server_id, new_mode))
429         else:
430             raiseCLIError(None, 'Time out')
431
432
433 @command(flavor_cmds)
434 class flavor_list(_init_cyclades):
435     """List flavors"""
436
437     def __init__(self, arguments={}):
438         super(flavor_list, self).__init__(arguments)
439         self.arguments['detail'] = FlagArgument('show detailed output', '-l')
440
441     @classmethod
442     def _print(self, flist):
443         for i, flavor in enumerate(flist):
444             print(bold('%s. %s' % (i, flavor['name'])))
445             print_dict(flavor, exclude=('name'), ident=1)
446             print(' ')
447
448     def main(self):
449         super(self.__class__, self).main()
450         try:
451             flavors = self.client.list_flavors(self.get_argument('detail'))
452         except Exception as err:
453             raiseCLIError(err)
454         self._print(flavors)
455
456
457 @command(flavor_cmds)
458 class flavor_info(_init_cyclades):
459     """Get flavor details"""
460
461     def main(self, flavor_id):
462         super(self.__class__, self).main()
463         try:
464             flavor = self.client.get_flavor_details(int(flavor_id))
465         except ValueError as err:
466             raiseCLIError(err, 'Server id must be positive integer', 1)
467         except Exception as err:
468             raiseCLIError(err)
469         print_dict(flavor)
470
471
472 @command(network_cmds)
473 class network_list(_init_cyclades):
474     """List networks"""
475
476     def __init__(self, arguments={}):
477         super(network_list, self).__init__(arguments)
478         self.arguments['detail'] = FlagArgument('show detailed output', '-l')
479
480     def print_networks(self, nets):
481         for net in nets:
482             netname = bold(net.pop('name'))
483             netid = bold(unicode(net.pop('id')))
484             print('%s (%s)' % (netid, netname))
485             if self.get_argument('detail'):
486                 network_info.print_network(net)
487
488     def main(self):
489         super(self.__class__, self).main()
490         try:
491             networks = self.client.list_networks(self.get_argument('detail'))
492         except Exception as err:
493             raiseCLIError(err)
494         self.print_networks(networks)
495
496
497 @command(network_cmds)
498 class network_create(_init_cyclades):
499     """Create a network"""
500
501     def __init__(self, arguments={}):
502         super(network_create, self).__init__(arguments)
503         self.arguments['cidr'] =\
504             ValueArgument('specific cidr for new network', '--with-cidr')
505         self.arguments['gateway'] =\
506             ValueArgument('specific gateway for new network', '--with-gateway')
507         self.arguments['dhcp'] =\
508             ValueArgument('specific dhcp for new network', '--with-dhcp')
509         self.arguments['type'] =\
510             ValueArgument('specific type for new network', '--with-type')
511
512     def main(self, name):
513         super(self.__class__, self).main()
514         try:
515             reply = self.client.create_network(name,
516                 cidr=self.get_argument('cidr'),
517                 gateway=self.get_argument('gateway'),
518                 dhcp=self.get_argument('dhcp'),
519                 type=self.get_argument('type'))
520         except Exception as err:
521             raiseCLIError(err)
522         print_dict(reply)
523
524
525 @command(network_cmds)
526 class network_info(_init_cyclades):
527     """Get network details"""
528
529     @classmethod
530     def print_network(self, net):
531         if 'attachments' in net:
532             att = net['attachments']['values']
533             net['attachments'] = att if len(att) > 0 else None
534         print_dict(net, ident=1)
535
536     def main(self, network_id):
537         super(self.__class__, self).main()
538         try:
539             network = self.client.get_network_details(network_id)
540         except Exception as err:
541             raiseCLIError(err)
542         network_info.print_network(network)
543
544
545 @command(network_cmds)
546 class network_rename(_init_cyclades):
547     """Update network name"""
548
549     def main(self, network_id, new_name):
550         super(self.__class__, self).main()
551         try:
552             self.client.update_network_name(network_id, new_name)
553         except Exception as err:
554             raiseCLIError(err)
555
556
557 @command(network_cmds)
558 class network_delete(_init_cyclades):
559     """Delete a network"""
560
561     def main(self, network_id):
562         super(self.__class__, self).main()
563         try:
564             self.client.delete_network(network_id)
565         except Exception as err:
566             raiseCLIError(err)
567
568
569 @command(network_cmds)
570 class network_connect(_init_cyclades):
571     """Connect a server to a network"""
572
573     def main(self, server_id, network_id):
574         super(self.__class__, self).main()
575         try:
576             self.client.connect_server(server_id, network_id)
577         except Exception as err:
578             raiseCLIError(err)
579
580
581 @command(network_cmds)
582 class network_disconnect(_init_cyclades):
583     """Disconnect a nic that connects a server to a network"""
584
585     def main(self, nic_id):
586         super(self.__class__, self).main()
587         try:
588             server_id = nic_id.split('-')[1]
589             self.client.disconnect_server(server_id, nic_id)
590         except IndexError as err:
591             raiseCLIError(err, 'Incorrect nic format', importance=1,
592                 details='nid_id format: nic-<server_id>-<nic_index>')
593         except Exception as err:
594             raiseCLIError(err)