Support extended personality attributes
[kamaki] / kamaki / kamaki.py
1 #!/usr/bin/env python
2
3 # Copyright 2011 GRNET S.A. All rights reserved.
4 #
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
7 # conditions are met:
8 #
9 #   1. Redistributions of source code must retain the above
10 #      copyright notice, this list of conditions and the following
11 #      disclaimer.
12 #
13 #   2. Redistributions in binary form must reproduce the above
14 #      copyright notice, this list of conditions and the following
15 #      disclaimer in the documentation and/or other materials
16 #      provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
19 # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
25 # USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
26 # AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
28 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 # POSSIBILITY OF SUCH DAMAGE.
30 #
31 # The views and conclusions contained in the software and
32 # documentation are those of the authors and should not be
33 # interpreted as representing official policies, either expressed
34 # or implied, of GRNET S.A.
35
36 import inspect
37 import logging
38 import os
39 import sys
40
41 from base64 import b64encode
42 from collections import defaultdict
43 from grp import getgrgid
44 from optparse import OptionParser
45 from pwd import getpwuid
46
47 from client import Client, ClientError
48
49
50 API_ENV = 'KAMAKI_API'
51 URL_ENV = 'KAMAKI_URL'
52 TOKEN_ENV = 'KAMAKI_TOKEN'
53 RCFILE = '.kamakirc'
54
55
56 log = logging.getLogger('kamaki')
57
58
59 def print_addresses(addresses, margin):
60     for address in addresses:
61         if address['id'] == 'public':
62             net = 'public'
63         else:
64             net = '%s/%s' % (address['id'], address['name'])
65         print '%s:' % net.rjust(margin + 4)
66         
67         ether = address.get('mac', None)
68         if ether:
69             print '%s: %s' % ('ether'.rjust(margin + 8), ether)
70         
71         firewall = address.get('firewallProfile', None)
72         if firewall:
73             print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
74         
75         for ip in address.get('values', []):
76             key = 'inet' if ip['version'] == 4 else 'inet6'
77             print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
78
79
80 def print_metadata(metadata, margin):
81     print '%s:' % 'metadata'.rjust(margin)
82     for key, val in metadata.get('values', {}).items():
83         print '%s: %s' % (key.rjust(margin + 4), val)
84
85
86 def print_dict(d, exclude=()):
87     if not d:
88         return
89     margin = max(len(key) for key in d) + 1
90     
91     for key, val in sorted(d.items()):
92         if key in exclude:
93             continue
94         
95         if key == 'addresses':
96             print '%s:' % 'addresses'.rjust(margin)
97             print_addresses(val.get('values', []), margin)
98             continue
99         elif key == 'metadata':
100             print_metadata(val, margin)
101             continue
102         elif key == 'servers':
103             val = ', '.join(str(x) for x in val['values'])
104         
105         print '%s: %s' % (key.rjust(margin), val)
106
107
108 def print_items(items, detail=False):
109     for item in items:
110         print '%s %s' % (item['id'], item.get('name', ''))
111         if detail:
112             print_dict(item, exclude=('id', 'name'))
113             print
114
115
116 class Command(object):
117     """Abstract class.
118     
119     All commands should subclass this class.
120     """
121     
122     api = 'openstack'
123     group = '<group>'
124     name = '<command>'
125     syntax = ''
126     description = ''
127     
128     def __init__(self, argv):
129         self._init_parser(argv)
130         self._init_logging()
131         self._init_conf()
132         if self.name != '<command>':
133             self.client = Client(self.url, self.token)
134     
135     def _init_parser(self, argv):
136         parser = OptionParser()
137         parser.usage = '%%prog %s %s %s [options]' % (self.group, self.name,
138                                                         self.syntax)
139         parser.add_option('--api', dest='api', metavar='API',
140                             help='API can be either openstack or synnefo')
141         parser.add_option('--url', dest='url', metavar='URL',
142                             help='API URL')
143         parser.add_option('--token', dest='token', metavar='TOKEN',
144                             help='use token TOKEN')
145         parser.add_option('-v', action='store_true', dest='verbose',
146                             default=False, help='use verbose output')
147         parser.add_option('-d', action='store_true', dest='debug',
148                             default=False, help='use debug output')
149         
150         self.add_options(parser)
151         
152         options, args = parser.parse_args(argv)
153         
154         # Add options to self
155         for opt in parser.option_list:
156             key = opt.dest
157             if key:
158                 val = getattr(options, key)
159                 setattr(self, key, val)
160         
161         self.args = args
162         self.parser = parser
163     
164     def _init_logging(self):
165         if self.debug:
166             log.setLevel(logging.DEBUG)
167         elif self.verbose:
168             log.setLevel(logging.INFO)
169         else:
170             log.setLevel(logging.WARNING)
171         
172     def _init_conf(self):
173         if not self.api:
174             self.api = os.environ.get(API_ENV, None)
175         if not self.url:
176             self.url = os.environ.get(URL_ENV, None)
177         if not self.token:
178             self.token = os.environ.get(TOKEN_ENV, None)
179         
180         path = os.path.join(os.path.expanduser('~'), RCFILE)
181         if not os.path.exists(path):
182             return
183
184         for line in open(path):
185             key, sep, val = line.partition('=')
186             if not sep:
187                 continue
188             key = key.strip().lower()
189             val = val.strip()
190             if key == 'api' and not self.api:
191                 self.api = val
192             elif key == 'url' and not self.url:
193                 self.url = val
194             elif key == 'token' and not self.token:
195                 self.token = val
196     
197     def add_options(self, parser):
198         pass
199     
200     def main(self, *args):
201         pass
202     
203     def execute(self):
204         try:
205             self.main(*self.args)
206         except TypeError:
207             self.parser.print_help()
208
209
210 # Server Group
211
212 class ListServers(Command):
213     group = 'server'
214     name = 'list'
215     description = 'list servers'
216     
217     def add_options(self, parser):
218         parser.add_option('-l', action='store_true', dest='detail',
219                             default=False, help='show detailed output')
220     
221     def main(self):
222         servers = self.client.list_servers(self.detail)
223         print_items(servers, self.detail)
224
225
226 class GetServerDetails(Command):
227     group = 'server'
228     name = 'info'
229     syntax = '<server id>'
230     description = 'get server details'
231     
232     def main(self, server_id):
233         server = self.client.get_server_details(int(server_id))
234         print_dict(server)
235
236
237 class CreateServer(Command):
238     group = 'server'
239     name = 'create'
240     syntax = '<server name>'
241     description = 'create server'
242
243     def add_options(self, parser):
244         parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
245                         help='use flavor FLAVOR_ID')
246         parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
247                         help='use image IMAGE_ID')
248         parser.add_option('--personality',
249                         dest='personalities',
250                         action='append',
251                         default=[],
252                         metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
253                         help='add a personality file')
254     
255     def main(self, name):
256         flavor_id = int(self.flavor)
257         image_id = int(self.image)
258         personalities = []
259         for personality in self.personalities:
260             p = personality.split(',')
261             p.extend([None] * (5 - len(p)))     # Fill missing fields with None
262             
263             path = p[0]
264             
265             if not path:
266                 log.error("Invalid personality argument '%s'", p)
267                 return
268             if not os.path.exists(path):
269                 log.error("File %s does not exist", path)
270                 return
271             
272             with open(path) as f:
273                 contents = b64encode(f.read())
274             
275             st = os.stat(path)
276             personalities.append({
277                 'path': p[1] or os.path.abspath(path),
278                 'owner': p[2] or getpwuid(st.st_uid).pw_name,
279                 'group': p[3] or getgrgid(st.st_gid).gr_name,
280                 'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode,
281                 'contents': contents})
282         
283         reply = self.client.create_server(name, flavor_id, image_id,
284                                             personalities)
285         print_dict(reply)
286
287
288 class UpdateServerName(Command):
289     group = 'server'
290     name = 'rename'
291     syntax = '<server id> <new name>'
292     description = 'update server name'
293     
294     def main(self, server_id, new_name):
295         self.client.update_server_name(int(server_id), new_name)
296
297
298 class DeleteServer(Command):
299     group = 'server'
300     name = 'delete'
301     syntax = '<server id>'
302     description = 'delete server'
303     
304     def main(self, server_id):
305         self.client.delete_server(int(server_id))
306
307
308 class RebootServer(Command):
309     group = 'server'
310     name = 'reboot'
311     syntax = '<server id>'
312     description = 'reboot server'
313     
314     def add_options(self, parser):
315         parser.add_option('-f', action='store_true', dest='hard',
316                             default=False, help='perform a hard reboot')
317     
318     def main(self, server_id):
319         self.client.reboot_server(int(server_id), self.hard)
320
321
322 class StartServer(Command):
323     api = 'synnefo'
324     group = 'server'
325     name = 'start'
326     syntax = '<server id>'
327     description = 'start server'
328     
329     def main(self, server_id):
330         self.client.start_server(int(server_id))
331
332
333 class StartServer(Command):
334     api = 'synnefo'
335     group = 'server'
336     name = 'shutdown'
337     syntax = '<server id>'
338     description = 'shutdown server'
339     
340     def main(self, server_id):
341         self.client.shutdown_server(int(server_id))
342
343
344 class ServerConsole(Command):
345     api = 'synnefo'
346     group = 'server'
347     name = 'console'
348     syntax = '<server id>'
349     description = 'get VNC console'
350
351     def main(self, server_id):
352         reply = self.client.get_server_console(int(server_id))
353         print_dict(reply)
354
355
356 class SetFirewallProfile(Command):
357     api = 'synnefo'
358     group = 'server'
359     name = 'firewall'
360     syntax = '<server id> <profile>'
361     description = 'set the firewall profile'
362     
363     def main(self, server_id, profile):
364         self.client.set_firewall_profile(int(server_id), profile)
365
366
367 class ListAddresses(Command):
368     group = 'server'
369     name = 'addr'
370     syntax = '<server id> [network]'
371     description = 'list server addresses'
372     
373     def main(self, server_id, network=None):
374         reply = self.client.list_server_addresses(int(server_id), network)
375         margin = max(len(x['name']) for x in reply)
376         print_addresses(reply, margin)
377
378
379 class GetServerMeta(Command):
380     group = 'server'
381     name = 'meta'
382     syntax = '<server id> [key]'
383     description = 'get server metadata'
384     
385     def main(self, server_id, key=None):
386         reply = self.client.get_server_metadata(int(server_id), key)
387         print_dict(reply)
388
389
390 class CreateServerMetadata(Command):
391     group = 'server'
392     name = 'addmeta'
393     syntax = '<server id> <key> <val>'
394     description = 'add server metadata'
395     
396     def main(self, server_id, key, val):
397         reply = self.client.create_server_metadata(int(server_id), key, val)
398         print_dict(reply)
399
400
401 class UpdateServerMetadata(Command):
402     group = 'server'
403     name = 'setmeta'
404     syntax = '<server id> <key> <val>'
405     description = 'update server metadata'
406     
407     def main(self, server_id, key, val):
408         metadata = {key: val}
409         reply = self.client.update_server_metadata(int(server_id), **metadata)
410         print_dict(reply)
411
412
413 class DeleteServerMetadata(Command):
414     group = 'server'
415     name = 'delmeta'
416     syntax = '<server id> <key>'
417     description = 'delete server metadata'
418     
419     def main(self, server_id, key):
420         self.client.delete_server_metadata(int(server_id), key)
421
422
423 class GetServerStats(Command):
424     api = 'synnefo'
425     group = 'server'
426     name = 'stats'
427     syntax = '<server id>'
428     description = 'get server statistics'
429     
430     def main(self, server_id):
431         reply = self.client.get_server_stats(int(server_id))
432         print_dict(reply, exclude=('serverRef',))
433
434
435 # Flavor Group
436
437 class ListFlavors(Command):
438     group = 'flavor'
439     name = 'list'
440     description = 'list flavors'
441     
442     def add_options(self, parser):
443         parser.add_option('-l', action='store_true', dest='detail',
444                             default=False, help='show detailed output')
445
446     def main(self):
447         flavors = self.client.list_flavors(self.detail)
448         print_items(flavors, self.detail)
449
450
451 class GetFlavorDetails(Command):
452     group = 'flavor'
453     name = 'info'
454     syntax = '<flavor id>'
455     description = 'get flavor details'
456     
457     def main(self, flavor_id):
458         flavor = self.client.get_flavor_details(int(flavor_id))
459         print_dict(flavor)
460
461
462 class ListImages(Command):
463     group = 'image'
464     name = 'list'
465     description = 'list images'
466
467     def add_options(self, parser):
468         parser.add_option('-l', action='store_true', dest='detail',
469                             default=False, help='show detailed output')
470
471     def main(self):
472         images = self.client.list_images(self.detail)
473         print_items(images, self.detail)
474
475
476 class GetImageDetails(Command):
477     group = 'image'
478     name = 'info'
479     syntax = '<image id>'
480     description = 'get image details'
481     
482     def main(self, image_id):
483         image = self.client.get_image_details(int(image_id))
484         print_dict(image)
485
486
487 class CreateImage(Command):
488     group = 'image'
489     name = 'create'
490     syntax = '<server id> <image name>'
491     description = 'create image'
492     
493     def main(self, server_id, name):
494         reply = self.client.create_image(int(server_id), name)
495         print_dict(reply)
496
497
498 class DeleteImage(Command):
499     group = 'image'
500     name = 'delete'
501     syntax = '<image id>'
502     description = 'delete image'
503     
504     def main(self, image_id):
505         self.client.delete_image(int(image_id))
506
507
508 class GetImageMetadata(Command):
509     group = 'image'
510     name = 'meta'
511     syntax = '<image id> [key]'
512     description = 'get image metadata'
513     
514     def main(self, image_id, key=None):
515         reply = self.client.get_image_metadata(int(image_id), key)
516         print_dict(reply)
517
518
519 class CreateImageMetadata(Command):
520     group = 'image'
521     name = 'addmeta'
522     syntax = '<image id> <key> <val>'
523     description = 'add image metadata'
524     
525     def main(self, image_id, key, val):
526         reply = self.client.create_image_metadata(int(image_id), key, val)
527         print_dict(reply)
528
529
530 class UpdateImageMetadata(Command):
531     group = 'image'
532     name = 'setmeta'
533     syntax = '<image id> <key> <val>'
534     description = 'update image metadata'
535     
536     def main(self, image_id, key, val):
537         metadata = {key: val}
538         reply = self.client.update_image_metadata(int(image_id), **metadata)
539         print_dict(reply)
540
541
542 class DeleteImageMetadata(Command):
543     group = 'image'
544     name = 'delmeta'
545     syntax = '<image id> <key>'
546     description = 'delete image metadata'
547     
548     def main(self, image_id, key):
549         self.client.delete_image_metadata(int(image_id), key)
550
551
552 class ListNetworks(Command):
553     api = 'synnefo'
554     group = 'network'
555     name = 'list'
556     description = 'list networks'
557     
558     def add_options(self, parser):
559         parser.add_option('-l', action='store_true', dest='detail',
560                             default=False, help='show detailed output')
561     
562     def main(self):
563         networks = self.client.list_networks(self.detail)
564         print_items(networks, self.detail)
565
566
567 class CreateNetwork(Command):
568     api = 'synnefo'
569     group = 'network'
570     name = 'create'
571     syntax = '<network name>'
572     description = 'create a network'
573     
574     def main(self, name):
575         reply = self.client.create_network(name)
576         print_dict(reply)
577
578
579 class GetNetworkDetails(Command):
580     api = 'synnefo'
581     group = 'network'
582     name = 'info'
583     syntax = '<network id>'
584     description = 'get network details'
585
586     def main(self, network_id):
587         network = self.client.get_network_details(network_id)
588         print_dict(network)
589
590
591 class RenameNetwork(Command):
592     api = 'synnefo'
593     group = 'network'
594     name = 'rename'
595     syntax = '<network id> <new name>'
596     description = 'update network name'
597     
598     def main(self, network_id, name):
599         self.client.update_network_name(network_id, name)
600
601
602 class DeleteNetwork(Command):
603     api = 'synnefo'
604     group = 'network'
605     name = 'delete'
606     syntax = '<network id>'
607     description = 'delete a network'
608     
609     def main(self, network_id):
610         self.client.delete_network(network_id)
611
612 class ConnectServer(Command):
613     api = 'synnefo'
614     group = 'network'
615     name = 'connect'
616     syntax = '<server id> <network id>'
617     description = 'connect a server to a network'
618     
619     def main(self, server_id, network_id):
620         self.client.connect_server(server_id, network_id)
621
622
623 class DisconnectServer(Command):
624     api = 'synnefo'
625     group = 'network'
626     name = 'disconnect'
627     syntax = '<server id> <network id>'
628     description = 'disconnect a server from a network'
629
630     def main(self, server_id, network_id):
631         self.client.disconnect_server(server_id, network_id)
632
633
634
635 def print_usage(exe, groups, group=None):
636     nop = Command([])
637     nop.parser.print_help()
638     
639     print
640     print 'Commands:'
641     
642     if group:
643         items = [(group, groups[group])]
644     else:
645         items = sorted(groups.items())
646     
647     for group, commands in items:
648         for command, cls in sorted(commands.items()):
649             name = '  %s %s' % (group, command)
650             print '%s %s' % (name.ljust(22), cls.description)
651         print
652
653
654 def main():
655     nop = Command([])
656     groups = defaultdict(dict)
657     module = sys.modules[__name__]
658     for name, cls in inspect.getmembers(module, inspect.isclass):
659         if issubclass(cls, Command) and cls != Command:
660             if nop.api == 'openstack' and nop.api != cls.api:
661                 continue    # Ignore synnefo commands
662             groups[cls.group][cls.name] = cls
663     
664     argv = list(sys.argv)
665     exe = os.path.basename(argv.pop(0))
666     group = argv.pop(0) if argv else None
667     command = argv.pop(0) if argv else None
668     
669     if group not in groups:
670         group = None
671     
672     if not group or command not in groups[group]:
673         print_usage(exe, groups, group)
674         sys.exit(1)
675     
676     cls = groups[group][command]
677     
678     try:
679         cmd = cls(argv)
680         cmd.execute()
681     except ClientError, err:
682         log.error('%s', err.message)
683         log.info('%s', err.details)
684
685
686 if __name__ == '__main__':
687     ch = logging.StreamHandler()
688     ch.setFormatter(logging.Formatter('%(message)s'))
689     log.addHandler(ch)
690     
691     main()