Adds requests dependency.
from pwd import getpwuid
from sys import argv, exit, stdout
-from clint.textui import puts, puts_err, indent
+from clint import args
+from clint.textui import puts, puts_err, indent, progress
+from clint.textui.colored import magenta, red, yellow
from clint.textui.cols import columns
+from requests.exceptions import ConnectionError
+
from kamaki import clients
from kamaki.config import Config
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
cls.api = api
cls.group = group or grp
cls.name = name or cmd
- cls.description = description or cls.__doc__
- cls.syntax = syntax
short_description, sep, long_description = cls.__doc__.partition('\n')
cls.description = short_description
@command(api='compute')
class server_list(object):
- """list servers"""
+ """List servers"""
def update_parser(cls, parser):
parser.add_option('-l', dest='detail', action='store_true',
@command(api='compute')
class server_info(object):
- """get server details"""
+ """Get server details"""
def main(self, server_id):
server = self.client.get_server_details(int(server_id))
@command(api='compute')
class server_create(object):
- """create server"""
+ """Create a server"""
def update_parser(cls, parser):
parser.add_option('--personality', dest='personalities',
- action='append', default=[],
- metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
- help='add a personality file')
+ action='append', default=[],
+ metavar='PATH[,SERVER PATH[,OWNER[,GROUP,[MODE]]]]',
+ help='add a personality file')
parser.epilog = "If missing, optional personality values will be " \
- "filled based on the file at PATH if missing."
+ "filled based on the file at PATH."
def main(self, name, flavor_id, image_id):
personalities = []
@command(api='compute')
class server_rename(object):
- """update server name"""
+ """Update a server's name"""
def main(self, server_id, new_name):
self.client.update_server_name(int(server_id), new_name)
@command(api='compute')
class server_delete(object):
- """delete server"""
+ """Delete a server"""
def main(self, server_id):
self.client.delete_server(int(server_id))
@command(api='compute')
class server_reboot(object):
- """reboot server"""
+ """Reboot a server"""
def update_parser(cls, parser):
parser.add_option('-f', dest='hard', action='store_true',
@command(api='cyclades')
class server_start(object):
- """start server"""
+ """Start a server"""
def main(self, server_id):
self.client.start_server(int(server_id))
@command(api='cyclades')
class server_shutdown(object):
- """shutdown server"""
+ """Shutdown a server"""
def main(self, server_id):
self.client.shutdown_server(int(server_id))
@command(api='cyclades')
class server_console(object):
- """get a VNC console"""
+ """Get a VNC console"""
def main(self, server_id):
reply = self.client.get_server_console(int(server_id))
@command(api='cyclades')
class server_firewall(object):
- """set the firewall profile"""
+ """Set the server's firewall profile"""
def main(self, server_id, profile):
self.client.set_firewall_profile(int(server_id), profile)
@command(api='cyclades')
class server_addr(object):
- """list server addresses"""
+ """List a server's addresses"""
def main(self, server_id, network=None):
reply = self.client.list_server_addresses(int(server_id), network)
@command(api='compute')
class server_meta(object):
- """get server metadata"""
+ """Get a server's metadata"""
def main(self, server_id, key=None):
reply = self.client.get_server_metadata(int(server_id), key)
@command(api='compute')
class server_addmeta(object):
- """add server metadata"""
+ """Add server metadata"""
def main(self, server_id, key, val):
reply = self.client.create_server_metadata(int(server_id), key, val)
@command(api='compute')
class server_setmeta(object):
- """update server metadata"""
+ """Update server's metadata"""
def main(self, server_id, key, val):
metadata = {key: val}
@command(api='compute')
class server_delmeta(object):
- """delete server metadata"""
+ """Delete server metadata"""
def main(self, server_id, key):
self.client.delete_server_metadata(int(server_id), key)
@command(api='cyclades')
class server_stats(object):
- """get server statistics"""
+ """Get server statistics"""
def main(self, server_id):
reply = self.client.get_server_stats(int(server_id))
@command(api='compute')
class flavor_list(object):
- """list flavors"""
+ """List flavors"""
def update_parser(cls, parser):
parser.add_option('-l', dest='detail', action='store_true',
@command(api='compute')
class flavor_info(object):
- """get flavor details"""
+ """Get flavor details"""
def main(self, flavor_id):
flavor = self.client.get_flavor_details(int(flavor_id))
@command(api='compute')
class image_list(object):
- """list images"""
+ """List images"""
def update_parser(cls, parser):
parser.add_option('-l', dest='detail', action='store_true',
@command(api='compute')
class image_info(object):
- """get image details"""
+ """Get image details"""
def main(self, image_id):
image = self.client.get_image_details(image_id)
@command(api='compute')
class image_delete(object):
- """delete image"""
+ """Delete image"""
def main(self, image_id):
self.client.delete_image(image_id)
@command(api='compute')
class image_meta(object):
- """get image metadata"""
+ """Get image metadata"""
def main(self, image_id, key=None):
reply = self.client.get_image_metadata(image_id, key)
@command(api='compute')
class image_addmeta(object):
- """add image metadata"""
+ """Add image metadata"""
def main(self, image_id, key, val):
reply = self.client.create_image_metadata(image_id, key, val)
@command(api='compute')
class image_setmeta(object):
- """update image metadata"""
+ """Update image metadata"""
def main(self, image_id, key, val):
metadata = {key: val}
@command(api='compute')
class image_delmeta(object):
- """delete image metadata"""
+ """Delete image metadata"""
def main(self, image_id, key):
self.client.delete_image_metadata(image_id, key)
@command(api='cyclades')
class network_list(object):
- """list networks"""
+ """List networks"""
def update_parser(cls, parser):
parser.add_option('-l', dest='detail', action='store_true',
@command(api='cyclades')
class network_create(object):
- """create a network"""
+ """Create a network"""
def main(self, name):
reply = self.client.create_network(name)
@command(api='cyclades')
class network_info(object):
- """get network details"""
+ """Get network details"""
def main(self, network_id):
network = self.client.get_network_details(network_id)
@command(api='cyclades')
class network_rename(object):
- """update network name"""
+ """Update network name"""
def main(self, network_id, new_name):
self.client.update_network_name(network_id, new_name)
@command(api='cyclades')
class network_delete(object):
- """delete a network"""
+ """Delete a network"""
def main(self, network_id):
self.client.delete_network(network_id)
@command(api='cyclades')
class network_connect(object):
- """connect a server to a network"""
+ """Connect a server to a network"""
def main(self, server_id, network_id):
self.client.connect_server(server_id, network_id)
@command(api='cyclades')
class network_disconnect(object):
- """disconnect a server from a network"""
+ """Disconnect a server from a network"""
def main(self, server_id, network_id):
self.client.disconnect_server(server_id, network_id)
@command(api='image')
class glance_list(object):
- """list images"""
+ """List images"""
def update_parser(cls, parser):
parser.add_option('-l', dest='detail', action='store_true',
@command(api='image')
class glance_meta(object):
- """get image metadata"""
+ """Get image metadata"""
def main(self, image_id):
image = self.client.get_meta(image_id)
@command(api='image')
class glance_register(object):
- """register an image"""
+ """Register an image"""
def update_parser(cls, parser):
parser.add_option('--checksum', dest='checksum', metavar='CHECKSUM',
def main(self, name, location):
params = {}
for key in ('checksum', 'container_format', 'disk_format', 'id',
- 'owner', 'is_public', 'size'):
+ 'owner', 'size'):
val = getattr(self.options, key)
if val is not None:
params[key] = val
+ if self.options.is_public:
+ params['is_public'] = 'true'
+
properties = {}
for property in self.options.properties or []:
key, sep, val = property.partition('=')
@command(api='image')
class glance_members(object):
- """get image members"""
+ """Get image members"""
def main(self, image_id):
members = self.client.list_members(image_id)
@command(api='image')
class glance_shared(object):
- """list shared images"""
+ """List shared images"""
def main(self, member):
images = self.client.list_shared(member)
@command(api='image')
class glance_addmember(object):
- """add a member to an image"""
+ """Add a member to an image"""
def main(self, image_id, member):
self.client.add_member(image_id, member)
@command(api='image')
class glance_delmember(object):
- """remove a member from an image"""
+ """Remove a member from an image"""
def main(self, image_id, member):
self.client.remove_member(image_id, member)
@command(api='image')
class glance_setmembers(object):
- """set the members of an image"""
+ """Set the members of an image"""
def main(self, image_id, *member):
self.client.set_members(image_id, member)
def update_parser(cls, parser):
parser.add_option('--account', dest='account', metavar='NAME',
- help='use account NAME')
+ help="Specify an account to use")
parser.add_option('--container', dest='container', metavar='NAME',
- help='use container NAME')
+ help="Specify a container to use")
- def main(self):
- self.config.override('storage_account', self.options.account)
- self.config.override('storage_container', self.options.container)
+ def progress(self, message):
+ """Return a generator function to be used for progress tracking"""
+
+ MESSAGE_LENGTH = 25
+ MAX_PROGRESS_LENGTH = 32
+
+ def progress_gen(n):
+ msg = message.ljust(MESSAGE_LENGTH)
+ width = min(n, MAX_PROGRESS_LENGTH)
+ hide = self.config.get('global', 'silent') or (n < 2)
+ for i in progress.bar(range(n), msg, width, hide):
+ yield
+ yield
- # Use the more efficient Pithos client if available
- if 'pithos' in self.config.get('apis').split():
- self.client = clients.PithosClient(self.config)
+ return progress_gen
+
+ def main(self):
+ if self.options.account is not None:
+ self.client.account = self.options.account
+ if self.options.container is not None:
+ self.client.container = self.options.container
@command(api='storage')
class store_create(object):
- """create a container"""
+ """Create a container"""
def update_parser(cls, parser):
- parser.add_option('--account', dest='account', metavar='ACCOUNT',
- help='use account ACCOUNT')
+ parser.add_option('--account', dest='account', metavar='NAME',
+ help="Specify an account to use")
def main(self, container):
- self.config.override('storage_account', self.options.account)
+ if self.options.account:
+ self.client.account = self.options.account
self.client.create_container(container)
@command(api='storage')
-class store_container(store_command):
- """get container info"""
+class store_container(object):
+ """Get container info"""
- def main(self):
- store_command.main(self)
- reply = self.client.get_container_meta()
+ def update_parser(cls, parser):
+ parser.add_option('--account', dest='account', metavar='NAME',
+ help="Specify an account to use")
+
+ def main(self, container):
+ if self.options.account:
+ self.client.account = self.options.account
+ reply = self.client.get_container_meta(container)
print_dict(reply)
@command(api='storage')
class store_upload(store_command):
- """upload a file"""
+ """Upload a file"""
def main(self, path, remote_path=None):
- store_command.main(self)
+ super(store_upload, self).main()
+
if remote_path is None:
remote_path = basename(path)
with open(path) as f:
- self.client.create_object(remote_path, f)
+ hash_cb = self.progress('Calculating block hashes')
+ upload_cb = self.progress('Uploading blocks')
+ self.client.create_object(remote_path, f, hash_cb=hash_cb,
+ upload_cb=upload_cb)
@command(api='storage')
class store_download(store_command):
- """download a file"""
-
- def main(self, remote_path, local_path):
- store_command.main(self)
- f = self.client.get_object(remote_path)
+ """Download a file"""
+
+ def main(self, remote_path, local_path='-'):
+ super(store_download, self).main()
+
+ f, size = self.client.get_object(remote_path)
out = open(local_path, 'w') if local_path != '-' else stdout
- block = 4096
- data = f.read(block)
+
+ blocksize = 4 * 1024**2
+ nblocks = 1 + (size - 1) // blocksize
+
+ cb = self.progress('Downloading blocks') if local_path != '-' else None
+ if cb:
+ gen = cb(nblocks)
+ gen.next()
+
+ data = f.read(blocksize)
while data:
out.write(data)
- data = f.read(block)
+ data = f.read(blocksize)
+ if cb:
+ gen.next()
@command(api='storage')
class store_delete(store_command):
- """delete a file"""
+ """Delete a file"""
def main(self, path):
store_command.main(self)
puts(columns([name, 12], [cls.description, 60]))
+def add_handler(name, level, prefix=''):
+ h = logging.StreamHandler()
+ fmt = logging.Formatter(prefix + '%(message)s')
+ h.setFormatter(fmt)
+ logger = logging.getLogger(name)
+ logger.addHandler(h)
+ logger.setLevel(level)
+
+
def main():
parser = OptionParser(add_help_option=False)
parser.usage = '%prog <group> <command> [options]'
help="Show this help message and exit")
parser.add_option('--config', dest='config', metavar='PATH',
help="Specify the path to the configuration file")
+ parser.add_option('-d', '--debug', dest='debug', action='store_true',
+ default=False,
+ help="Include debug output")
parser.add_option('-i', '--include', dest='include', action='store_true',
default=False,
help="Include protocol headers in the output")
print "kamaki %s" % kamaki.__version__
exit(0)
- if args.contains(['-s', '--silent']):
- level = logging.CRITICAL
- elif args.contains(['-v', '--verbose']):
- level = logging.INFO
- else:
- level = logging.WARNING
-
- logging.basicConfig(level=level, format='%(message)s')
-
if '--config' in args:
config_path = args.grouped['--config'].get(0)
else:
if hasattr(cmd, 'update_parser'):
cmd.update_parser(parser)
- if args.contains(['-h', '--help']):
+ options, arguments = parser.parse_args(argv)
+
+ if options.help:
parser.print_help()
exit(0)
- cmd.options, cmd.args = parser.parse_args(argv)
+ if options.silent:
+ add_handler('', logging.CRITICAL)
+ elif options.debug:
+ add_handler('requests', logging.INFO, prefix='* ')
+ add_handler('clients.send', logging.DEBUG, prefix='> ')
+ add_handler('clients.recv', logging.DEBUG, prefix='< ')
+ elif options.verbose:
+ add_handler('requests', logging.INFO, prefix='* ')
+ add_handler('clients.send', logging.INFO, prefix='> ')
+ add_handler('clients.recv', logging.INFO, prefix='< ')
+ elif options.include:
+ add_handler('clients.recv', logging.INFO)
+ else:
+ add_handler('', logging.WARNING)
api = cmd.api
- if api == 'config':
- cmd.config = config
- elif api in ('compute', 'image', 'storage'):
- token = config.get(api, 'token') or config.get('gobal', 'token')
- url = config.get(api, 'url')
- client_cls = getattr(clients, api)
- kwargs = dict(base_url=url, token=token)
-
- # Special cases
- if api == 'compute' and config.getboolean(api, 'cyclades_extensions'):
- client_cls = clients.cyclades
- elif api == 'storage':
- kwargs['account'] = config.get(api, 'account')
- kwargs['container'] = config.get(api, 'container')
- if config.getboolean(api, 'pithos_extensions'):
- client_cls = clients.pithos
-
- cmd.client = client_cls(**kwargs)
-
+ if api in ('compute', 'cyclades'):
+ url = config.get('compute', 'url')
+ token = config.get('compute', 'token') or config.get('global', 'token')
+ if config.getboolean('compute', 'cyclades_extensions'):
+ cmd.client = clients.cyclades(url, token)
+ else:
+ cmd.client = clients.compute(url, token)
+ elif api in ('storage', 'pithos'):
+ url = config.get('storage', 'url')
+ token = config.get('storage', 'token') or config.get('global', 'token')
+ account = config.get('storage', 'account')
+ container = config.get('storage', 'container')
+ if config.getboolean('storage', 'pithos_extensions'):
+ cmd.client = clients.pithos(url, token, account, container)
+ else:
+ cmd.client = clients.storage(url, token, account, container)
+ elif api == 'image':
+ url = config.get('image', 'url')
+ token = config.get('image', 'token') or config.get('global', 'token')
+ cmd.client = clients.image(url, token)
+
+ cmd.options = options
+ cmd.config = config
+
try:
- ret = cmd.main(*args.grouped['_'][2:])
+ ret = cmd.main(*arguments[3:])
exit(ret)
except TypeError as e:
if e.args and e.args[0].startswith('main()'):
exit(1)
else:
raise
- except clients.ClientError, err:
- log.error('%s', err.message)
- log.info('%s', err.details)
+ except clients.ClientError as err:
+ if err.status == 404:
+ color = yellow
+ elif 500 <= err.status < 600:
+ color = magenta
+ else:
+ color = red
+
+ puts_err(color(err.message))
+ if err.details and (options.verbose or options.debug):
+ puts_err(err.details)
exit(2)
+ except ConnectionError as err:
+ puts_err(red("Connection error"))
+ exit(1)
if __name__ == '__main__':
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
+import json
+import logging
+
+import requests
+
+from requests.auth import AuthBase
+
+
+sendlog = logging.getLogger('clients.send')
+recvlog = logging.getLogger('clients.recv')
+
+
+# Add a convenience json property to the responses
+def _json(self):
+ try:
+ return json.loads(self.content)
+ except ValueError:
+ raise ClientError("Invalid JSON reply", self.status_code)
+requests.Response.json = property(_json)
+
+# Add a convenience status property to the responses
+def _status(self):
+ return requests.status_codes._codes[self.status_code][0].upper()
+requests.Response.status = property(_status)
+
+
+class XAuthTokenAuth(AuthBase):
+ def __init__(self, token):
+ self.token = token
+
+ def __call__(self, r):
+ r.headers['X-Auth-Token'] = self.token
+ return r
+
+
class ClientError(Exception):
def __init__(self, message, status=0, details=''):
self.message = message
self.status = status
self.details = details
- def __int__(self):
- return int(self.status)
- def __str__(self):
- r = self.message
- if self.status:
- r += "\nHTTP Status: %d" % self.status
- if self.details:
- r += "\nDetails: \n%s" % self.details
+class Client(object):
+ def __init__(self, base_url, token, include=False, verbose=False):
+ self.base_url = base_url
+ self.auth = XAuthTokenAuth(token)
+ self.include = include
+ self.verbose = verbose
+
+ def raise_for_status(self, r):
+ if 400 <= r.status_code < 500:
+ message, sep, details = r.text.partition('\n')
+ elif 500 <= r.status_code < 600:
+ message = '%d Server Error' % (r.status_code,)
+ details = r.text
+ else:
+ message = '%d Unknown Error' % (r.status_code,)
+ details = r.text
+
+ message = message or "HTTP Error %d" % (r.status_code,)
+ raise ClientError(message, r.status_code, details)
+
+ def request(self, method, path, **kwargs):
+ raw = kwargs.pop('raw', False)
+ success = kwargs.pop('success', 200)
+ if 'json' in kwargs:
+ data = json.dumps(kwargs.pop('json'))
+ kwargs['data'] = data
+ headers = kwargs.setdefault('headers', {})
+ headers['content-type'] = 'application/json'
+
+ url = self.base_url + path
+ kwargs.setdefault('auth', self.auth)
+ r = requests.request(method, url, **kwargs)
+
+ req = r.request
+ sendlog.info('%s %s', req.method, req.url)
+ for key, val in req.headers.items():
+ sendlog.info('%s: %s', key, val)
+ sendlog.info('')
+ if req.data:
+ sendlog.info('%s', req.data)
+
+ recvlog.info('%d %s', r.status_code, r.status)
+ for key, val in r.headers.items():
+ recvlog.info('%s: %s', key, val)
+ recvlog.info('')
+ if not raw and r.text:
+ recvlog.debug(r.text)
+
+ if success is not None:
+ # Success can either be an in or a collection
+ success = (success,) if isinstance(success, int) else success
+ if r.status_code not in success:
+ self.raise_for_status(r)
+
return r
+ def delete(self, path, **kwargs):
+ return self.request('delete', path, **kwargs)
+
+ def get(self, path, **kwargs):
+ return self.request('get', path, **kwargs)
+
+ def head(self, path, **kwargs):
+ return self.request('head', path, **kwargs)
+
+ def post(self, path, **kwargs):
+ return self.request('post', path, **kwargs)
+
+ def put(self, path, **kwargs):
+ return self.request('put', path, **kwargs)
+
-from .compute import ComputeClient
-from .image import ImageClient
-from .storage import StorageClient
-from .cyclades import CycladesClient
-from .pithos import PithosClient
+from .compute import ComputeClient as compute
+from .image import ImageClient as image
+from .storage import StorageClient as storage
+from .cyclades import CycladesClient as cyclades
+from .pithos import PithosClient as pithos
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
-import json
+from . import Client, ClientError
-from . import ClientError
-from .http import HTTPClient
-
-class ComputeClient(HTTPClient):
+class ComputeClient(Client):
"""OpenStack Compute API 1.1 client"""
- @property
- def url(self):
- url = self.config.get('compute_url') or self.config.get('url')
- if not url:
- raise ClientError('No URL was given')
- return url
-
- @property
- def token(self):
- token = self.config.get('compute_token') or self.config.get('token')
- if not token:
- raise ClientError('No token was given')
- return token
+ def raise_for_status(self, r):
+ d = r.json
+ key = d.keys()[0]
+ val = d[key]
+ message = '%s: %s' % (key, val.get('message', ''))
+ details = val.get('details', '')
+ raise ClientError(message, r.status_code, details)
def list_servers(self, detail=False):
"""List servers, returned detailed output if detailed is True"""
+
path = '/servers/detail' if detail else '/servers'
- reply = self.http_get(path)
- return reply['servers']['values']
+ r = self.get(path, success=200)
+ return r.json['servers']['values']
def get_server_details(self, server_id):
"""Return detailed output on a server specified by its id"""
- path = '/servers/%d' % server_id
- reply = self.http_get(path)
- return reply['server']
+
+ path = '/servers/%s' % (server_id,)
+ r = self.get(path, success=200)
+ return r.json['server']
def create_server(self, name, flavor_id, image_id, personality=None):
"""Submit request to create a new server
The call returns a dictionary describing the newly created server.
"""
- req = {'name': name, 'flavorRef': flavor_id, 'imageRef': image_id}
+ req = {'server': {'name': name,
+ 'flavorRef': flavor_id,
+ 'imageRef': image_id}}
if personality:
req['personality'] = personality
- body = json.dumps({'server': req})
- reply = self.http_post('/servers', body)
- return reply['server']
+ r = self.post('/servers', json=req, success=202)
+ return r.json['server']
def update_server_name(self, server_id, new_name):
"""Update the name of the server as reported by the API.
This call does not modify the hostname actually used by the server
internally.
"""
- path = '/servers/%d' % server_id
- body = json.dumps({'server': {'name': new_name}})
- self.http_put(path, body)
+ path = '/servers/%s' % (server_id,)
+ req = {'server': {'name': new_name}}
+ self.put(path, json=req, success=204)
def delete_server(self, server_id):
"""Submit a deletion request for a server specified by id"""
- path = '/servers/%d' % server_id
- self.http_delete(path)
+
+ path = '/servers/%s' % (server_id,)
+ self.delete(path, success=204)
def reboot_server(self, server_id, hard=False):
"""Submit a reboot request for a server specified by id"""
- path = '/servers/%d/action' % server_id
- type = 'HARD' if hard else 'SOFT'
- body = json.dumps({'reboot': {'type': type}})
- self.http_post(path, body)
+ path = '/servers/%s/action' % (server_id,)
+ type = 'HARD' if hard else 'SOFT'
+ req = {'reboot': {'type': type}}
+ self.post(path, json=req, success=202)
+
def get_server_metadata(self, server_id, key=None):
- path = '/servers/%d/meta' % server_id
+ path = '/servers/%s/meta' % (server_id,)
if key:
path += '/%s' % key
- reply = self.http_get(path)
- return reply['meta'] if key else reply['metadata']['values']
+ r = self.get(path, success=200)
+ return r.json['meta'] if key else r.json['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.http_put(path, body, success=201)
- return reply['meta']
+ req = {'meta': {key: val}}
+ r = self.put(path, json=req, success=201)
+ return r.json['meta']
def update_server_metadata(self, server_id, **metadata):
- path = '/servers/%d/meta' % server_id
- body = json.dumps({'metadata': metadata})
- reply = self.http_post(path, body, success=201)
- return reply['metadata']
+ path = '/servers/%d/meta' % (server_id,)
+ req = {'metadata': metadata}
+ r = self.post(path, json=req, success=201)
+ return r.json['metadata']
def delete_server_metadata(self, server_id, key):
path = '/servers/%d/meta/%s' % (server_id, key)
- reply = self.http_delete(path)
-
-
+ self.delete(path, success=204)
+
+
def list_flavors(self, detail=False):
path = '/flavors/detail' if detail else '/flavors'
- reply = self.http_get(path)
- return reply['flavors']['values']
+ r = self.get(path, success=200)
+ return r.json['flavors']['values']
def get_flavor_details(self, flavor_id):
path = '/flavors/%d' % flavor_id
- reply = self.http_get(path)
- return reply['flavor']
+ r = self.get(path, success=200)
+ return r.json['flavor']
def list_images(self, detail=False):
path = '/images/detail' if detail else '/images'
- reply = self.http_get(path)
- return reply['images']['values']
+ r = self.get(path, success=200)
+ return r.json['images']['values']
def get_image_details(self, image_id):
- path = '/images/%s' % image_id
- reply = self.http_get(path)
- return reply['image']
+ path = '/images/%s' % (image_id,)
+ r = self.get(path, success=200)
+ return r.json['image']
def delete_image(self, image_id):
- path = '/images/%s' % image_id
- self.http_delete(path)
+ path = '/images/%s' % (image_id,)
+ self.delete(path, success=204)
def get_image_metadata(self, image_id, key=None):
- path = '/images/%s/meta' % image_id
+ path = '/images/%s/meta' % (image_id,)
if key:
path += '/%s' % key
- reply = self.http_get(path)
- return reply['meta'] if key else reply['metadata']['values']
+ r = self.get(path, success=200)
+ return r.json['meta'] if key else r.json['metadata']['values']
def create_image_metadata(self, image_id, key, val):
path = '/images/%s/meta/%s' % (image_id, key)
- body = json.dumps({'meta': {key: val}})
- reply = self.http_put(path, body, success=201)
- return reply['meta']
+ req = {'meta': {key: val}}
+ r = self.put(path, json=req, success=201)
+ return r.json['meta']
def update_image_metadata(self, image_id, **metadata):
- path = '/images/%s/meta' % image_id
- body = json.dumps({'metadata': metadata})
- reply = self.http_post(path, body, success=201)
- return reply['metadata']
+ path = '/images/%s/meta' % (image_id,)
+ req = {'metadata': metadata}
+ r = self.post(path, json=req, success=201)
+ return r.json['metadata']
def delete_image_metadata(self, image_id, key):
path = '/images/%s/meta/%s' % (image_id, key)
- self.http_delete(path)
+ self.delete(path, success=204)
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
-import json
-
from .compute import ComputeClient
def start_server(self, server_id):
"""Submit a startup request for a server specified by id"""
- path = '/servers/%d/action' % server_id
- body = json.dumps({'start': {}})
- self.http_post(path, body)
+
+ path = '/servers/%s/action' % (server_id,)
+ req = {'start': {}}
+ self.post(path, json=req, success=202)
def shutdown_server(self, server_id):
"""Submit a shutdown request for a server specified by id"""
- path = '/servers/%d/action' % server_id
- body = json.dumps({'shutdown': {}})
- self.http_post(path, body)
+
+ path = '/servers/%s/action' % (server_id,)
+ req = {'shutdown': {}}
+ self.post(path, json=req, success=202)
def get_server_console(self, server_id):
"""Get a VNC connection to the console of a server specified by id"""
- path = '/servers/%d/action' % server_id
- body = json.dumps({'console': {'type': 'vnc'}})
- reply = self.http_post(path, body, success=200)
- return reply['console']
+
+ path = '/servers/%s/action' % (server_id,)
+ req = {'console': {'type': 'vnc'}}
+ r = self.post(path, json=req, success=200)
+ return r.json['console']
def set_firewall_profile(self, server_id, profile):
"""Set the firewall profile for the public interface of a server
The server is specified by id, the profile argument
is one of (ENABLED, DISABLED, PROTECTED).
"""
- path = '/servers/%d/action' % server_id
- body = json.dumps({'firewallProfile': {'profile': profile}})
- self.http_post(path, body)
+ path = '/servers/%s/action' % (server_id,)
+ req = {'firewallProfile': {'profile': profile}}
+ self.post(path, json=req, success=202)
def list_server_addresses(self, server_id, network=None):
- path = '/servers/%d/ips' % server_id
+ path = '/servers/%s/ips' % (server_id,)
if network:
path += '/%s' % network
- reply = self.http_get(path)
- return [reply['network']] if network else reply['addresses']['values']
+ r = self.get(path, success=200)
+ if network:
+ return [r.json['network']]
+ else:
+ return r.json['addresses']['values']
def get_server_stats(self, server_id):
- path = '/servers/%d/stats' % server_id
- reply = self.http_get(path)
- return reply['stats']
+ path = '/servers/%s/stats' % (server_id,)
+ r = self.get(path, success=200)
+ return r.json['stats']
def list_networks(self, detail=False):
path = '/networks/detail' if detail else '/networks'
- reply = self.http_get(path)
- return reply['networks']['values']
+ r = self.get(path, success=200)
+ return r.json['networks']['values']
def create_network(self, name):
- body = json.dumps({'network': {'name': name}})
- reply = self.http_post('/networks', body)
- return reply['network']
+ req = {'network': {'name': name}}
+ r = self.post('/networks', json=req, success=202)
+ return r.json['network']
def get_network_details(self, network_id):
- path = '/networks/%s' % network_id
- reply = self.http_get(path)
- return reply['network']
+ path = '/networks/%s' % (network_id,)
+ r = self.get(path, success=200)
+ return r.json['network']
def update_network_name(self, network_id, new_name):
- path = '/networks/%s' % network_id
- body = json.dumps({'network': {'name': new_name}})
- self.http_put(path, body)
+ path = '/networks/%s' % (network_id,)
+ req = {'network': {'name': new_name}}
+ self.put(path, json=req, success=204)
def delete_network(self, network_id):
- path = '/networks/%s' % network_id
- self.http_delete(path)
+ path = '/networks/%s' % (network_id,)
+ self.delete(path, success=204)
def connect_server(self, server_id, network_id):
- path = '/networks/%s/action' % network_id
- body = json.dumps({'add': {'serverRef': server_id}})
- self.http_post(path, body)
+ path = '/networks/%s/action' % (network_id,)
+ req = {'add': {'serverRef': server_id}}
+ self.post(path, json=req, success=202)
def disconnect_server(self, server_id, network_id):
- path = '/networks/%s/action' % network_id
- body = json.dumps({'remove': {'serverRef': server_id}})
- self.http_post(path, body)
+ path = '/networks/%s/action' % (network_id,)
+ req = {'remove': {'serverRef': server_id}}
+ self.post(path, json=req, success=202)
# or implied, of GRNET S.A.
import hashlib
-import json
+import os
-from . import ClientError
-from .storage import StorageClient
from ..utils import OrderedDict
+from .storage import StorageClient
+
+
+def pithos_hash(block, blockhash):
+ h = hashlib.new(blockhash)
+ h.update(block.rstrip('\x00'))
+ return h.hexdigest()
+
class PithosClient(StorageClient):
"""GRNet Pithos API client"""
def put_block(self, data, hash):
- path = '/%s/%s?update' % (self.account, self.container)
+ path = '/%s/%s' % (self.account, self.container)
+ params = {'update': ''}
headers = {'Content-Type': 'application/octet-stream',
- 'Content-Length': len(data)}
- resp, reply = self.raw_http_cmd('POST', path, data, headers,
- success=202)
- assert reply.strip() == hash, 'Local hash does not match server'
+ 'Content-Length': str(len(data))}
+ r = self.post(path, params=params, data=data, headers=headers,
+ success=202)
+ assert r.text.strip() == hash, 'Local hash does not match server'
- def create_object(self, object, f):
- meta = self.get_container_meta()
+ def create_object(self, object, f, hash_cb=None, upload_cb=None):
+ """Create an object by uploading only the missing blocks
+
+ hash_cb is a generator function taking the total number of blocks to
+ be hashed as an argument. Its next() will be called every time a block
+ is hashed.
+
+ upload_cb is a generator function with the same properties that is
+ called every time a block is uploaded.
+ """
+ self.assert_container()
+
+ meta = self.get_container_meta(self.container)
blocksize = int(meta['block-size'])
blockhash = meta['block-hash']
- size = 0
+ file_size = os.fstat(f.fileno()).st_size
+ nblocks = 1 + (file_size - 1) // blocksize
hashes = OrderedDict()
- data = f.read(blocksize)
- while data:
- bytes = len(data)
- h = hashlib.new(blockhash)
- h.update(data.rstrip('\x00'))
- hash = h.hexdigest()
+
+ size = 0
+
+ if hash_cb:
+ hash_gen = hash_cb(nblocks)
+ hash_gen.next()
+ for i in range(nblocks):
+ block = f.read(blocksize)
+ bytes = len(block)
+ hash = pithos_hash(block, blockhash)
hashes[hash] = (size, bytes)
size += bytes
- data = f.read(blocksize)
+ if hash_cb:
+ hash_gen.next()
+
+ assert size == file_size
- path = '/%s/%s/%s?hashmap&format=json' % (self.account, self.container,
- object)
+ path = '/%s/%s/%s' % (self.account, self.container, object)
+ params = {'hashmap': '', 'format': 'json'}
hashmap = dict(bytes=size, hashes=hashes.keys())
- req = json.dumps(hashmap)
- resp, reply = self.raw_http_cmd('PUT', path, req, success=None)
-
- if resp.status not in (201, 409):
- raise ClientError('Invalid response from the server')
+ r = self.put(path, params=params, json=hashmap, success=(201, 409))
- if resp.status == 201:
+ if r.status_code == 201:
return
- missing = json.loads(reply)
+ missing = r.json
+ if upload_cb:
+ upload_gen = upload_cb(len(missing))
+ upload_gen.next()
for hash in missing:
offset, bytes = hashes[hash]
f.seek(offset)
data = f.read(bytes)
self.put_block(data, hash)
+ if upload_cb:
+ upload_gen.next()
- self.http_put(path, req, success=201)
+ self.put(path, params=params, json=hashmap, success=201)
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
-from . import ClientError
-from .http import HTTPClient
+from . import Client, ClientError
-class StorageClient(HTTPClient):
+class StorageClient(Client):
"""OpenStack Object Storage API 1.0 client"""
- @property
- def url(self):
- url = self.config.get('storage_url') or self.config.get('url')
- if not url:
- raise ClientError('No URL was given')
- return url
-
- @property
- def token(self):
- token = self.config.get('storage_token') or self.config.get('token')
- if not token:
- raise ClientError('No token was given')
- return token
+ def __init__(self, base_url, token, account=None, container=None):
+ super(StorageClient, self).__init__(base_url, token)
+ self.account = account
+ self.container = container
- @property
- def account(self):
- account = self.config.get('storage_account')
- if not account:
- raise ClientError('No account was given')
- return account
+ def assert_account(self):
+ if not self.account:
+ raise ClientError("Please provide an account")
- @property
- def container(self):
- container = self.config.get('storage_container')
- if not container:
- raise ClientError('No container was given')
- return container
+ def assert_container(self):
+ self.assert_account()
+ if not self.container:
+ raise ClientError("Please provide a container")
def create_container(self, container):
+ self.assert_account()
path = '/%s/%s' % (self.account, container)
- self.http_put(path, success=201)
+ r = self.put(path, success=(201, 202))
+ if r.status_code == 202:
+ raise ClientError("Container already exists")
- def get_container_meta(self):
- path = '/%s/%s' % (self.account, self.container)
- resp, reply = self.raw_http_cmd('HEAD', path, success=204)
+ def get_container_meta(self, container):
+ self.assert_account()
+ path = '/%s/%s' % (self.account, container)
+ r = self.head(path, success=(204, 404))
+ if r.status_code == 404:
+ raise ClientError("Container does not exist", r.status_code)
+
reply = {}
prefix = 'x-container-'
- for key, val in resp.getheaders():
+ for key, val in r.headers.items():
key = key.lower()
if key.startswith(prefix):
reply[key[len(prefix):]] = val
+
return reply
- def create_object(self, object, f):
+ def create_object(self, object, f, hash_cb=None, upload_cb=None):
+ # This is a naive implementation, it loads the whole file in memory
+ self.assert_container()
path = '/%s/%s/%s' % (self.account, self.container, object)
data = f.read()
- self.http_put(path, data, success=201)
-
+ self.put(path, data=data, success=201)
+
def get_object(self, object):
+ self.assert_container()
path = '/%s/%s/%s' % (self.account, self.container, object)
- resp, reply = self.raw_http_cmd('GET', path, success=200,
- skip_read=True)
- return resp.fp
-
+ r = self.get(path, raw=True)
+ size = int(r.headers['content-length'])
+ return r.raw, size
+
def delete_object(self, object):
+ self.assert_container()
path = '/%s/%s/%s' % (self.account, self.container, object)
- self.http_delete(path)
+ self.delete(path, success=204)
'console_scripts': ['kamaki = kamaki.cli:main']
},
install_requires=[
+ 'requests>=0.10.2',
'clint>=0.3'
]
)