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