Commit 5d1d131b authored by Giorgos Verigakis's avatar Giorgos Verigakis
Browse files

Initial import of kamaki

* No authentication method yet, you need to provide a valid token.
* Not fully compatible with Cactus yet.
* Split into client and CLI.
* build script will create a standalone binary with no dependencies.
parent e059edb1
*.pyc
.DS_Store
bin/
#!/usr/bin/env python
import os
import stat
SRCDIR = 'kamaki'
SRC = 'kamaki.py'
CLIENT = 'client.py'
DSTDIR = 'bin'
DST = 'kamaki'
def main():
if not os.path.exists(DSTDIR):
os.makedirs(DSTDIR)
dstpath = os.path.join(DSTDIR, DST)
dst = open(dstpath, 'w')
srcpath = os.path.join(SRCDIR, SRC)
clientpath = os.path.join(SRCDIR, CLIENT)
for line in open(srcpath):
if line.startswith('from client import'):
for l in open(clientpath):
if l.startswith('#'):
continue # Skip comments
dst.write(l)
else:
dst.write(line)
dst.close()
# Make file executable
mode = stat.S_IMODE(os.stat(dstpath).st_mode)
mode |= 0111
os.chmod(dstpath, mode)
if __name__ == '__main__':
main()
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import json
import logging
from httplib import HTTPConnection, HTTPSConnection
from urlparse import urlparse
class ClientError(Exception):
def __init__(self, message, details=''):
self.message = message
self.details = details
class Client(object):
def __init__(self, url, token=''):
self.url = url
self.token = token
def _cmd(self, method, path, body=None, success=200):
p = urlparse(self.url)
path = p.path + path
if p.scheme == 'http':
conn = HTTPConnection(p.netloc)
elif p.scheme == 'https':
conn = HTTPSConnection(p.netloc)
else:
raise ClientError("Unknown URL scheme")
headers = {'X-Auth-Token': self.token}
if body:
headers['Content-Type'] = 'application/json'
headers['Content-Length'] = len(body)
logging.debug('%s', '>' * 40)
logging.debug('%s %s', method, path)
for key, val in headers.items():
logging.debug('%s: %s', key, val)
logging.debug('')
if body:
logging.debug(body)
logging.debug('')
conn.request(method, path, body, headers)
resp = conn.getresponse()
logging.debug('%s', '<' * 40)
logging.info('%d %s', resp.status, resp.reason)
for key, val in resp.getheaders():
logging.info('%s: %s', key.capitalize(), val)
logging.info('')
buf = resp.read()
try:
reply = json.loads(buf) if buf else {}
except ValueError:
raise ClientError('Invalid response from the server', buf)
if resp.status != success:
if len(reply) == 1:
key = reply.keys()[0]
val = reply[key]
message = '%s: %s' % (key, val.get('message', ''))
details = val.get('details', '')
raise ClientError(message, details)
else:
raise ClientError('Invalid response from the server')
return reply
def _get(self, path, success=200):
return self._cmd('GET', path, None, success)
def _post(self, path, body, success=202):
return self._cmd('POST', path, body, success)
def _put(self, path, body, success=204):
return self._cmd('PUT', path, body, success)
def _delete(self, path, success=204):
return self._cmd('DELETE', path, None, success)
# Servers
def list_servers(self, detail=False):
path = '/servers/detail' if detail else '/servers'
reply = self._get(path)
return reply['servers']['values']
def get_server_details(self, server_id):
path = '/servers/%d' % server_id
reply = self._get(path)
return reply['server']
def create_server(self, name, flavor, image):
req = {'name': name, 'flavorRef': flavor, 'imageRef': image}
body = json.dumps({'server': req})
reply = self._post('/servers', body)
return reply['server']
def update_server_name(self, server_id, new_name):
path = '/servers/%d' % server_id
body = json.dumps({'server': {'name': new_name}})
self._put(path, body)
def delete_server(self, server_id):
path = '/servers/%d' % server_id
self._delete(path)
def reboot_server(self, server_id, hard=False):
path = '/servers/%d/action' % server_id
type = 'HARD' if hard else 'SOFT'
body = json.dumps({'reboot': {'type': type}})
self._post(path, body)
def start_server(self, server_id):
path = '/servers/%d/action' % server_id
body = json.dumps({'start': {}})
self._post(path, body)
def shutdown_server(self, server_id):
path = '/servers/%d/action' % server_id
body = json.dumps({'shutdown': {}})
self._post(path, body)
def get_server_console(self, server_id):
path = '/servers/%d/action' % server_id
body = json.dumps({'console': {'type': 'vnc'}})
reply = self._cmd('POST', path, body, 200)
return reply['console']
def set_firewall_profile(self, server_id, profile):
path = '/servers/%d/action' % server_id
body = json.dumps({'firewallProfile': {'profile': profile}})
self._cmd('POST', path, body, 202)
def list_server_addresses(self, server_id, network=None):
path = '/servers/%d/ips' % server_id
if network:
path += '/%s' % network
reply = self._get(path)
return [reply['network']] if network else reply['addresses']['values']
def get_server_metadata(self, server_id, key=None):
path = '/servers/%d/meta' % server_id
if key:
path += '/%s' % key
reply = self._get(path)
return reply['meta'] if key else reply['metadata']['values']
def create_server_metadata(self, server_id, key, val):
path = '/servers/%d/meta/%s' % (server_id, key)
body = json.dumps({'meta': {key: val}})
reply = self._put(path, body, 201)
return reply['meta']
def update_server_metadata(self, server_id, key, val):
path = '/servers/%d/meta' % server_id
body = json.dumps({'metadata': {key: val}})
reply = self._post(path, body, 201)
return reply['metadata']
def delete_server_metadata(self, server_id, key):
path = '/servers/%d/meta/%s' % (server_id, key)
reply = self._delete(path)
def get_server_stats(self, server_id):
path = '/servers/%d/stats' % server_id
reply = self._get(path)
return reply['stats']
# Flavors
def list_flavors(self, detail=False):
path = '/flavors/detail' if detail else '/flavors'
reply = self._get(path)
return reply['flavors']['values']
def get_flavor_details(self, flavor_id):
path = '/flavors/%d' % flavor_id
reply = self._get(path)
return reply['flavor']
# Images
def list_images(self, detail=False):
path = '/images/detail' if detail else '/images'
reply = self._get(path)
return reply['images']['values']
def get_image_details(self, image_id):
path = '/images/%d' % image_id
reply = self._get(path)
return reply['image']
def create_image(self, server_id, name):
req = {'name': name, 'serverRef': server_id}
body = json.dumps({'image': req})
reply = self._post('/images', body)
return reply['image']
def delete_image(self, image_id):
path = '/images/%d' % image_id
self._delete(path)
def get_image_metadata(self, image_id, key=None):
path = '/images/%d/meta' % image_id
if key:
path += '/%s' % key
reply = self._get(path)
return reply['meta'] if key else reply['metadata']['values']
def create_image_metadata(self, image_id, key, val):
path = '/images/%d/meta/%s' % (image_id, key)
body = json.dumps({'meta': {key: val}})
reply = self._put(path, body, 201)
reply['meta']
def update_image_metadata(self, image_id, key, val):
path = '/images/%d/meta' % image_id
body = json.dumps({'metadata': {key: val}})
reply = self._post(path, body, 201)
return reply['metadata']
def delete_image_metadata(self, image_id, key):
path = '/images/%d/meta/%s' % (image_id, key)
reply = self._delete(path)
# Networks
def list_networks(self, detail=False):
path = '/networks/detail' if detail else '/networks'
reply = self._get(path)
return reply['networks']['values']
def create_network(self, name):
body = json.dumps({'network': {'name': name}})
reply = self._post('/networks', body)
return reply['network']
def get_network_details(self, network_id):
path = '/networks/%s' % network_id
reply = self._get(path)
return reply['network']
def update_network_name(self, network_id, new_name):
path = '/networks/%s' % network_id
body = json.dumps({'network': {'name': new_name}})
self._put(path, body)
def delete_network(self, network_id):
path = '/networks/%s' % network_id
self._delete(path)
def connect_server(self, server_id, network_id):
path = '/networks/%s/action' % network_id
body = json.dumps({'add': {'serverRef': server_id}})
self._post(path, body)
def disconnect_server(self, server_id, network_id):
path = '/networks/%s/action' % network_id
body = json.dumps({'remove': {'serverRef': server_id}})
self._post(path, body)
#!/usr/bin/env python
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
import inspect
import logging
import os
import sys
from collections import defaultdict
from optparse import OptionParser
from client import Client, ClientError
API_ENV = 'KAMAKI_API'
URL_ENV = 'KAMAKI_URL'
TOKEN_ENV = 'KAMAKI_TOKEN'
RCFILE = '.kamakirc'
def print_addresses(addresses, margin):
for address in addresses:
if address['id'] == 'public':
net = 'public'
else:
net = '%s/%s' % (address['id'], address['name'])
print '%s:' % net.rjust(margin + 4)
ether = address.get('mac', None)
if ether:
print '%s: %s' % ('ether'.rjust(margin + 8), ether)
firewall = address.get('firewallProfile', None)
if firewall:
print '%s: %s' % ('firewall'.rjust(margin + 8), firewall)
for ip in address.get('values', []):
key = 'inet' if ip['version'] == 4 else 'inet6'
print '%s: %s' % (key.rjust(margin + 8), ip['addr'])
def print_metadata(metadata, margin):
print '%s:' % 'metadata'.rjust(margin)
for key, val in metadata.get('values', {}).items():
print '%s: %s' % (key.rjust(margin + 4), val)
def print_dict(d, exclude=()):
if not d:
return
margin = max(len(key) for key in d) + 1
for key, val in sorted(d.items()):
if key in exclude:
continue
if key == 'addresses':
print '%s:' % 'addresses'.rjust(margin)
print_addresses(val.get('values', []), margin)
continue
elif key == 'metadata':
print_metadata(val, margin)
continue
elif key == 'servers':
val = ', '.join(str(x) for x in val['values'])
print '%s: %s' % (key.rjust(margin), val)
def print_items(items, detail=False):
for item in items:
print '%s %s' % (item['id'], item.get('name', ''))
if detail:
print_dict(item, exclude=('id', 'name'))
print
class Command(object):
"""Abstract class.
All commands should subclass this class.
"""
api = 'openstack'
group = '<group>'
name = '<command>'
syntax = ''
description = ''
def __init__(self, argv):
self._init_parser(argv)
self._init_logging()
self._init_conf()
if self.name != '<command>':
self.client = Client(self.url, self.token)
def _init_parser(self, argv):
parser = OptionParser()
parser.usage = '%%prog %s %s %s [options]' % (self.group, self.name,
self.syntax)
parser.add_option('--api', dest='api', metavar='API',
help='API can be either openstack or synnefo')
parser.add_option('--url', dest='url', metavar='URL',
help='API URL')
parser.add_option('--token', dest='token', metavar='TOKEN',
help='use token TOKEN')
parser.add_option('-v', action='store_true', dest='verbose',
default=False, help='use verbose output')
parser.add_option('-d', action='store_true', dest='debug',
default=False, help='use debug output')
self.add_options(parser)
options, args = parser.parse_args(argv)
# Add options to self
for opt in parser.option_list:
key = opt.dest
if key:
val = getattr(options, key)
setattr(self, key, val)
self.args = args
self.parser = parser
def _init_logging(self):
ch = logging.StreamHandler()
ch.setFormatter(logging.Formatter('%(message)s'))
logger = logging.getLogger()
logger.addHandler(ch)
if self.debug:
level = logging.DEBUG
elif self.verbose:
level = logging.INFO
else:
level = logging.WARNING
logger.setLevel(level)
def _init_conf(self):
if not self.api:
self.api = os.environ.get(API_ENV, None)
if not self.url:
self.url = os.environ.get(URL_ENV, None)
if not self.token:
self.token = os.environ.get(TOKEN_ENV, None)
path = os.path.join(os.path.expanduser('~'), RCFILE)
if not os.path.exists(path):
return
for line in open(path):
key, sep, val = line.partition('=')
if not sep:
continue
key = key.strip().lower()
val = val.strip()
if key == 'api' and not self.api:
self.api = val
elif key == 'url' and not self.url:
self.url = val
elif key == 'token' and not self.token:
self.token = val
def add_options(self, parser):
pass
def main(self, *args):
pass
def execute(self):
try:
self.main(*self.args)
except TypeError:
self.parser.print_help()
# Server Group
class ListServers(Command):
group = 'server'
name = 'list'
description = 'list servers'
def add_options(self, parser):
parser.add_option('-l', action='store_true', dest='detail',
default=False, help='show detailed output')
def main(self):
servers = self.client.list_servers(self.detail)
print_items(servers, self.detail)
class GetServerDetails(Command):
group = 'server'
name = 'info'
syntax = '<server id>'
description = 'get server details'
def main(self, server_id):
server = self.client.get_server_details(int(server_id))
print_dict(server)
class CreateServer(Command):
group = 'server'
name = 'create'
syntax = '<server name>'
description = 'create server'
def add_options(self, parser):
parser.add_option('-f', dest='flavor', metavar='FLAVOR_ID', default=1,
help='use flavor FLAVOR_ID')
parser.add_option('-i', dest='image', metavar='IMAGE_ID', default=1,
help='use image IMAGE_ID')
def main(self, name):
flavor_id = int(self.flavor)
image_id = int(self.image)
reply = self.client.create_server(name, flavor_id, image_id)
print_dict(reply)
class UpdateServerName(Command):
group = 'server'
name = 'rename'
syntax = '<server id> <new name>'
description = 'update server name'
def main(self, server_id, new_name):
self.client.update_server_name(int(server_id), new_name)
class DeleteServer(Command):
group = 'server'
name = 'delete'
syntax = '<server id>'
description = 'delete server'