3 # Copyright 2011 GRNET S.A. All rights reserved.
5 # Redistribution and use in source and binary forms, with or
6 # without modification, are permitted provided that the following
9 # 1. Redistributions of source code must retain the above
10 # copyright notice, this list of conditions and the following
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.
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.
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.
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
47 from client import Client, ClientError
50 API_ENV = 'KAMAKI_API'
51 URL_ENV = 'KAMAKI_URL'
52 TOKEN_ENV = 'KAMAKI_TOKEN'
56 log = logging.getLogger('kamaki')
59 def print_addresses(addresses, margin):
60 for address in addresses:
61 if address['id'] == 'public':
64 net = '%s/%s' % (address['id'], address['name'])
65 print '%s:' % net.rjust(margin + 4)
67 ether = address.get('mac', None)
69 print '%s: %s' % ('ether'.rjust(margin + 8), ether)
71 firewall = address.get('firewallProfile', None)
73 print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
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'])
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)
86 def print_dict(d, exclude=()):
89 margin = max(len(key) for key in d) + 1
91 for key, val in sorted(d.items()):
95 if key == 'addresses':
96 print '%s:' % 'addresses'.rjust(margin)
97 print_addresses(val.get('values', []), margin)
99 elif key == 'metadata':
100 print_metadata(val, margin)
102 elif key == 'servers':
103 val = ', '.join(str(x) for x in val['values'])
105 print '%s: %s' % (key.rjust(margin), val)
108 def print_items(items, detail=False):
110 print '%s %s' % (item['id'], item.get('name', ''))
112 print_dict(item, exclude=('id', 'name'))
116 class Command(object):
119 All commands should subclass this class.
128 def __init__(self, argv):
129 self._init_parser(argv)
132 if self.name != '<command>':
133 self.client = Client(self.url, self.token)
135 def _init_parser(self, argv):
136 parser = OptionParser()
137 parser.usage = '%%prog %s %s %s [options]' % (self.group, self.name,
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',
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')
150 self.add_options(parser)
152 options, args = parser.parse_args(argv)
154 # Add options to self
155 for opt in parser.option_list:
158 val = getattr(options, key)
159 setattr(self, key, val)
164 def _init_logging(self):
166 log.setLevel(logging.DEBUG)
168 log.setLevel(logging.INFO)
170 log.setLevel(logging.WARNING)
172 def _init_conf(self):
174 self.api = os.environ.get(API_ENV, None)
176 self.url = os.environ.get(URL_ENV, None)
178 self.token = os.environ.get(TOKEN_ENV, None)
180 path = os.path.join(os.path.expanduser('~'), RCFILE)
181 if not os.path.exists(path):
184 for line in open(path):
185 key, sep, val = line.partition('=')
188 key = key.strip().lower()
190 if key == 'api' and not self.api:
192 elif key == 'url' and not self.url:
194 elif key == 'token' and not self.token:
197 def add_options(self, parser):
200 def main(self, *args):
205 self.main(*self.args)
207 self.parser.print_help()
212 class ListServers(Command):
215 description = 'list servers'
217 def add_options(self, parser):
218 parser.add_option('-l', action='store_true', dest='detail',
219 default=False, help='show detailed output')
222 servers = self.client.list_servers(self.detail)
223 print_items(servers, self.detail)
226 class GetServerDetails(Command):
229 syntax = '<server id>'
230 description = 'get server details'
232 def main(self, server_id):
233 server = self.client.get_server_details(int(server_id))
237 class CreateServer(Command):
240 syntax = '<server name>'
241 description = 'create server'
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',
252 metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
253 help='add a personality file')
255 def main(self, name):
256 flavor_id = int(self.flavor)
257 image_id = int(self.image)
259 for personality in self.personalities:
260 p = personality.split(',')
261 p.extend([None] * (5 - len(p))) # Fill missing fields with None
266 log.error("Invalid personality argument '%s'", p)
268 if not os.path.exists(path):
269 log.error("File %s does not exist", path)
272 with open(path) as f:
273 contents = b64encode(f.read())
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})
283 reply = self.client.create_server(name, flavor_id, image_id,
288 class UpdateServerName(Command):
291 syntax = '<server id> <new name>'
292 description = 'update server name'
294 def main(self, server_id, new_name):
295 self.client.update_server_name(int(server_id), new_name)
298 class DeleteServer(Command):
301 syntax = '<server id>'
302 description = 'delete server'
304 def main(self, server_id):
305 self.client.delete_server(int(server_id))
308 class RebootServer(Command):
311 syntax = '<server id>'
312 description = 'reboot server'
314 def add_options(self, parser):
315 parser.add_option('-f', action='store_true', dest='hard',
316 default=False, help='perform a hard reboot')
318 def main(self, server_id):
319 self.client.reboot_server(int(server_id), self.hard)
322 class StartServer(Command):
326 syntax = '<server id>'
327 description = 'start server'
329 def main(self, server_id):
330 self.client.start_server(int(server_id))
333 class StartServer(Command):
337 syntax = '<server id>'
338 description = 'shutdown server'
340 def main(self, server_id):
341 self.client.shutdown_server(int(server_id))
344 class ServerConsole(Command):
348 syntax = '<server id>'
349 description = 'get VNC console'
351 def main(self, server_id):
352 reply = self.client.get_server_console(int(server_id))
356 class SetFirewallProfile(Command):
360 syntax = '<server id> <profile>'
361 description = 'set the firewall profile'
363 def main(self, server_id, profile):
364 self.client.set_firewall_profile(int(server_id), profile)
367 class ListAddresses(Command):
370 syntax = '<server id> [network]'
371 description = 'list server addresses'
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)
379 class GetServerMeta(Command):
382 syntax = '<server id> [key]'
383 description = 'get server metadata'
385 def main(self, server_id, key=None):
386 reply = self.client.get_server_metadata(int(server_id), key)
390 class CreateServerMetadata(Command):
393 syntax = '<server id> <key> <val>'
394 description = 'add server metadata'
396 def main(self, server_id, key, val):
397 reply = self.client.create_server_metadata(int(server_id), key, val)
401 class UpdateServerMetadata(Command):
404 syntax = '<server id> <key> <val>'
405 description = 'update server metadata'
407 def main(self, server_id, key, val):
408 metadata = {key: val}
409 reply = self.client.update_server_metadata(int(server_id), **metadata)
413 class DeleteServerMetadata(Command):
416 syntax = '<server id> <key>'
417 description = 'delete server metadata'
419 def main(self, server_id, key):
420 self.client.delete_server_metadata(int(server_id), key)
423 class GetServerStats(Command):
427 syntax = '<server id>'
428 description = 'get server statistics'
430 def main(self, server_id):
431 reply = self.client.get_server_stats(int(server_id))
432 print_dict(reply, exclude=('serverRef',))
437 class ListFlavors(Command):
440 description = 'list flavors'
442 def add_options(self, parser):
443 parser.add_option('-l', action='store_true', dest='detail',
444 default=False, help='show detailed output')
447 flavors = self.client.list_flavors(self.detail)
448 print_items(flavors, self.detail)
451 class GetFlavorDetails(Command):
454 syntax = '<flavor id>'
455 description = 'get flavor details'
457 def main(self, flavor_id):
458 flavor = self.client.get_flavor_details(int(flavor_id))
462 class ListImages(Command):
465 description = 'list images'
467 def add_options(self, parser):
468 parser.add_option('-l', action='store_true', dest='detail',
469 default=False, help='show detailed output')
472 images = self.client.list_images(self.detail)
473 print_items(images, self.detail)
476 class GetImageDetails(Command):
479 syntax = '<image id>'
480 description = 'get image details'
482 def main(self, image_id):
483 image = self.client.get_image_details(int(image_id))
487 class CreateImage(Command):
490 syntax = '<server id> <image name>'
491 description = 'create image'
493 def main(self, server_id, name):
494 reply = self.client.create_image(int(server_id), name)
498 class DeleteImage(Command):
501 syntax = '<image id>'
502 description = 'delete image'
504 def main(self, image_id):
505 self.client.delete_image(int(image_id))
508 class GetImageMetadata(Command):
511 syntax = '<image id> [key]'
512 description = 'get image metadata'
514 def main(self, image_id, key=None):
515 reply = self.client.get_image_metadata(int(image_id), key)
519 class CreateImageMetadata(Command):
522 syntax = '<image id> <key> <val>'
523 description = 'add image metadata'
525 def main(self, image_id, key, val):
526 reply = self.client.create_image_metadata(int(image_id), key, val)
530 class UpdateImageMetadata(Command):
533 syntax = '<image id> <key> <val>'
534 description = 'update image metadata'
536 def main(self, image_id, key, val):
537 metadata = {key: val}
538 reply = self.client.update_image_metadata(int(image_id), **metadata)
542 class DeleteImageMetadata(Command):
545 syntax = '<image id> <key>'
546 description = 'delete image metadata'
548 def main(self, image_id, key):
549 self.client.delete_image_metadata(int(image_id), key)
552 class ListNetworks(Command):
556 description = 'list networks'
558 def add_options(self, parser):
559 parser.add_option('-l', action='store_true', dest='detail',
560 default=False, help='show detailed output')
563 networks = self.client.list_networks(self.detail)
564 print_items(networks, self.detail)
567 class CreateNetwork(Command):
571 syntax = '<network name>'
572 description = 'create a network'
574 def main(self, name):
575 reply = self.client.create_network(name)
579 class GetNetworkDetails(Command):
583 syntax = '<network id>'
584 description = 'get network details'
586 def main(self, network_id):
587 network = self.client.get_network_details(network_id)
591 class RenameNetwork(Command):
595 syntax = '<network id> <new name>'
596 description = 'update network name'
598 def main(self, network_id, name):
599 self.client.update_network_name(network_id, name)
602 class DeleteNetwork(Command):
606 syntax = '<network id>'
607 description = 'delete a network'
609 def main(self, network_id):
610 self.client.delete_network(network_id)
612 class ConnectServer(Command):
616 syntax = '<server id> <network id>'
617 description = 'connect a server to a network'
619 def main(self, server_id, network_id):
620 self.client.connect_server(server_id, network_id)
623 class DisconnectServer(Command):
627 syntax = '<server id> <network id>'
628 description = 'disconnect a server from a network'
630 def main(self, server_id, network_id):
631 self.client.disconnect_server(server_id, network_id)
635 def print_usage(exe, groups, group=None):
637 nop.parser.print_help()
643 items = [(group, groups[group])]
645 items = sorted(groups.items())
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)
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
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
669 if group not in groups:
672 if not group or command not in groups[group]:
673 print_usage(exe, groups, group)
676 cls = groups[group][command]
681 except ClientError, err:
682 log.error('%s', err.message)
683 log.info('%s', err.details)
686 if __name__ == '__main__':
687 ch = logging.StreamHandler()
688 ch.setFormatter(logging.Formatter('%(message)s'))