of the command. This behavior can be overriden with the 'group' and 'name'
decorator arguments:
- @command(api='nova')
+ @command(api='compute')
class server_list(object):
# This command will be named 'list' under group 'server'
...
- @command(api='nova', name='ls')
+ @command(api='compute', name='ls')
class server_list(object):
# This command will be named 'ls' under group 'server'
...
import inspect
import logging
import os
-import sys
from base64 import b64encode
from grp import getgrgid
from optparse import OptionParser
+from os.path import abspath, basename, exists
from pwd import getpwuid
+from sys import argv, exit
-from kamaki.client import ComputeClient, GlanceClient, ClientError
+from kamaki import clients
from kamaki.config import Config, ConfigError
from kamaki.utils import OrderedDict, print_addresses, print_dict, print_items
-# Path to the file that stores the configuration
-CONFIG_PATH = os.path.expanduser('~/.kamakirc')
-
-# Name of a shell variable to bypass the CONFIG_PATH value
-CONFIG_ENV = 'KAMAKI_CONFIG'
-
-# The defaults also determine the allowed keys
-CONFIG_DEFAULTS = {
- 'apis': 'nova synnefo glance',
- 'token': '',
- 'compute_url': 'https://okeanos.grnet.gr/api/v1',
- 'images_url': 'https://okeanos.grnet.gr/plankton',
-}
-
log = logging.getLogger('kamaki')
_commands = OrderedDict()
self.config.delete(key)
-@command(api='nova')
+@command(api='compute')
class server_list(object):
"""list servers"""
print_items(servers)
-@command(api='nova')
+@command(api='compute')
class server_info(object):
"""get server details"""
print_dict(server)
-@command(api='nova')
+@command(api='compute')
class server_create(object):
"""create server"""
if not path:
log.error("Invalid personality argument '%s'", p)
return 1
- if not os.path.exists(path):
+ if not exists(path):
log.error("File %s does not exist", path)
return 1
st = os.stat(path)
personalities.append({
- 'path': p[1] or os.path.abspath(path),
+ 'path': p[1] or abspath(path),
'owner': p[2] or getpwuid(st.st_uid).pw_name,
'group': p[3] or getgrgid(st.st_gid).gr_name,
'mode': int(p[4]) if p[4] else 0x7777 & st.st_mode,
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class server_rename(object):
"""update server name"""
self.client.update_server_name(int(server_id), new_name)
-@command(api='nova')
+@command(api='compute')
class server_delete(object):
"""delete server"""
self.client.delete_server(int(server_id))
-@command(api='nova')
+@command(api='compute')
class server_reboot(object):
"""reboot server"""
self.client.reboot_server(int(server_id), self.options.hard)
-@command(api='synnefo')
+@command(api='asterias')
class server_start(object):
"""start server"""
self.client.start_server(int(server_id))
-@command(api='synnefo')
+@command(api='asterias')
class server_shutdown(object):
"""shutdown server"""
self.client.shutdown_server(int(server_id))
-@command(api='synnefo')
+@command(api='asterias')
class server_console(object):
"""get a VNC console"""
print_dict(reply)
-@command(api='synnefo')
+@command(api='asterias')
class server_firewall(object):
"""set the firewall profile"""
self.client.set_firewall_profile(int(server_id), profile)
-@command(api='synnefo')
+@command(api='asterias')
class server_addr(object):
"""list server addresses"""
print_addresses(reply, margin)
-@command(api='nova')
+@command(api='compute')
class server_meta(object):
"""get server metadata"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class server_addmeta(object):
"""add server metadata"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class server_setmeta(object):
"""update server metadata"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class server_delmeta(object):
"""delete server metadata"""
self.client.delete_server_metadata(int(server_id), key)
-@command(api='synnefo')
+@command(api='asterias')
class server_stats(object):
"""get server statistics"""
print_dict(reply, exclude=('serverRef',))
-@command(api='nova')
+@command(api='compute')
class flavor_list(object):
"""list flavors"""
print_items(flavors)
-@command(api='nova')
+@command(api='compute')
class flavor_info(object):
"""get flavor details"""
print_dict(flavor)
-@command(api='nova')
+@command(api='compute')
class image_list(object):
"""list images"""
print_items(images)
-@command(api='nova')
+@command(api='compute')
class image_info(object):
"""get image details"""
print_dict(image)
-@command(api='nova')
+@command(api='compute')
class image_create(object):
"""create image"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class image_delete(object):
"""delete image"""
self.client.delete_image(image_id)
-@command(api='nova')
+@command(api='compute')
class image_meta(object):
"""get image metadata"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class image_addmeta(object):
"""add image metadata"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class image_setmeta(object):
"""update image metadata"""
print_dict(reply)
-@command(api='nova')
+@command(api='compute')
class image_delmeta(object):
"""delete image metadata"""
self.client.delete_image_metadata(image_id, key)
-@command(api='synnefo')
+@command(api='asterias')
class network_list(object):
"""list networks"""
print_items(networks)
-@command(api='synnefo')
+@command(api='asterias')
class network_create(object):
"""create a network"""
print_dict(reply)
-@command(api='synnefo')
+@command(api='asterias')
class network_info(object):
"""get network details"""
print_dict(network)
-@command(api='synnefo')
+@command(api='asterias')
class network_rename(object):
"""update network name"""
self.client.update_network_name(network_id, new_name)
-@command(api='synnefo')
+@command(api='asterias')
class network_delete(object):
"""delete a network"""
self.client.delete_network(network_id)
-@command(api='synnefo')
+@command(api='asterias')
class network_connect(object):
"""connect a server to a network"""
self.client.connect_server(server_id, network_id)
-@command(api='synnefo')
+@command(api='asterias')
class network_disconnect(object):
"""disconnect a server from a network"""
self.client.disconnect_server(server_id, network_id)
-@command(api='glance')
+@command(api='image')
class glance_list(object):
"""list images"""
print_items(images, title=('name',))
-@command(api='glance')
+@command(api='image')
class glance_meta(object):
"""get image metadata"""
print_dict(image)
-@command(api='glance')
+@command(api='image')
class glance_register(object):
"""register an image"""
self.client.register(name, location, params, properties)
-@command(api='glance')
+@command(api='image')
class glance_members(object):
"""get image members"""
print member['member_id']
-@command(api='glance')
+@command(api='image')
class glance_shared(object):
"""list shared images"""
print image['image_id']
-@command(api='glance')
+@command(api='image')
class glance_addmember(object):
"""add a member to an image"""
self.client.add_member(image_id, member)
-@command(api='glance')
+@command(api='image')
class glance_delmember(object):
"""remove a member from an image"""
self.client.remove_member(image_id, member)
-@command(api='glance')
+@command(api='image')
class glance_setmembers(object):
"""set the members of an image"""
self.client.set_members(image_id, member)
+@command(api='storage')
+class store_upload(object):
+ """upload a file"""
+
+ @classmethod
+ def update_parser(cls, parser):
+ parser.add_option('--account', dest='account', metavar='ACCOUNT',
+ help='use account ACCOUNT')
+ parser.add_option('--container', dest='container', metavar='CONTAINER',
+ help='use container CONTAINER')
+
+ def main(self, path, remote_path=None):
+ account = self.options.account or self.config.get('storage_account')
+ container = self.options.container or \
+ self.config.get('storage_container')
+ if remote_path is None:
+ remote_path = basename(path)
+ with open(path) as f:
+ self.client.create_object(account, container, remote_path, f)
+
+
def print_groups(groups):
print
print 'Groups:'
def main():
+ ch = logging.StreamHandler()
+ ch.setFormatter(logging.Formatter('%(message)s'))
+ log.addHandler(ch)
+
parser = OptionParser(add_help_option=False)
parser.usage = '%prog <group> <command> [options]'
parser.add_option('--help', dest='help', action='store_true',
default=False, help='show this help message and exit')
- parser.add_option('--api', dest='apis', action='append', metavar='API',
- help='API to use (can be used multiple times)')
- parser.add_option('--compute-url', dest='compute_url', metavar='URL',
- help='URL for the compute API')
- parser.add_option('--images-url', dest='images_url', metavar='URL',
- help='URL for the images API')
- parser.add_option('--token', dest='token', metavar='TOKEN',
- help='use token TOKEN')
parser.add_option('-v', dest='verbose', action='store_true', default=False,
help='use verbose output')
parser.add_option('-d', dest='debug', action='store_true', default=False,
# anyway if we don't reach the main parsing.
_error = parser.error
parser.error = lambda msg: None
- options, args = parser.parse_args(sys.argv)
+ options, args = parser.parse_args(argv)
parser.error = _error
if options.debug:
log.setLevel(logging.WARNING)
try:
- config = Config(CONFIG_PATH, CONFIG_ENV, CONFIG_DEFAULTS)
+ config = Config()
except ConfigError, e:
log.error('%s', e.args[0])
- return 1
-
- for key in CONFIG_DEFAULTS:
- config.override(key, getattr(options, key))
+ exit(1)
apis = config.get('apis').split()
if len(args) < 2:
parser.print_help()
print_groups(available_groups)
- return 0
+ exit(0)
group = args[1]
if group not in available_groups:
parser.print_help()
print_groups(available_groups)
- return 1
+ exit(1)
# Find available commands based on the given APIs
available_commands = []
if len(args) < 3:
parser.print_help()
print_commands(group, available_commands)
- return 0
+ exit(0)
name = args[2]
if name not in available_commands:
parser.print_help()
print_commands(group, available_commands)
- return 1
+ exit(1)
cls = _commands[group][name]
- cls.config = config
syntax = '%s [options]' % cls.syntax if cls.syntax else '[options]'
parser.usage = '%%prog %s %s %s' % (group, name, syntax)
if hasattr(cls, 'update_parser'):
cls.update_parser(parser)
- options, args = parser.parse_args(sys.argv)
+ options, args = parser.parse_args(argv)
if options.help:
parser.print_help()
- return 0
+ exit(0)
cmd = cls()
cmd.config = config
cmd.options = options
- if cmd.api in ('nova', 'synnefo'):
- url = config.get('compute_url')
+ if cmd.api in ('compute', 'image', 'storage', 'cyclades'):
token = config.get('token')
- cmd.client = ComputeClient(url, token)
- elif cmd.api == 'glance':
- url = config.get('images_url')
- token = config.get('token')
- cmd.client = GlanceClient(url, token)
+ if cmd.api in ('compute', 'image', 'storage'):
+ url = config.get(cmd.api + '_url')
+ elif cmd.api == 'cyclades':
+ url = config.get('compute_url')
+ cls_name = cmd.api.capitalize() + 'Client'
+ cmd.client = getattr(clients, cls_name)(url, token)
try:
- return cmd.main(*args[3:])
+ ret = cmd.main(*args[3:])
+ exit(ret)
except TypeError as e:
if e.args and e.args[0].startswith('main()'):
parser.print_help()
- return 1
+ exit(1)
else:
raise
- except ClientError, err:
+ except clients.ClientError, err:
log.error('%s', err.message)
log.info('%s', err.details)
- return 2
+ exit(2)
if __name__ == '__main__':
- ch = logging.StreamHandler()
- ch.setFormatter(logging.Formatter('%(message)s'))
- log.addHandler(ch)
- err = main() or 0
- sys.exit(err)
+ main()
+++ /dev/null
-# 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 urllib import quote
-from urlparse import urlparse
-
-
-log = logging.getLogger('kamaki.client')
-
-
-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
- return r
-
-
-class Client(object):
- def __init__(self, url, token=''):
- self.url = url
- self.token = token
-
- def raw_http_cmd(self, method, path, body=None, headers=None, success=200,
- json_reply=False):
- 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 = headers or {}
- headers['X-Auth-Token'] = self.token
- if body:
- headers['Content-Type'] = 'application/json'
- headers['Content-Length'] = len(body)
-
- log.debug('>' * 50)
- log.debug('%s %s', method, path)
- for key, val in headers.items():
- log.debug('%s: %s', key, val)
- if body:
- log.debug('')
- log.debug(body)
-
- conn.request(method, path, body, headers)
-
- resp = conn.getresponse()
- reply = resp.read()
-
- log.debug('<' * 50)
- log.info('%d %s', resp.status, resp.reason)
- for key, val in resp.getheaders():
- log.info('%s: %s', key.capitalize(), val)
- log.info('')
- log.debug(reply)
- log.debug('-' * 50)
-
- if json_reply:
- try:
- reply = json.loads(reply) if reply else {}
- except ValueError:
- raise ClientError('Did not receive valid JSON reply',
- resp.status, reply)
-
- if resp.status != success:
- if len(reply) == 1:
- if json_reply:
- key = reply.keys()[0]
- val = reply[key]
- message = '%s: %s' % (key, val.get('message', ''))
- details = val.get('details', '')
- else:
- message = reply
- details = ''
-
- raise ClientError(message, resp.status, details)
- else:
- raise ClientError('Invalid response from the server')
-
- return resp, reply
-
- def http_cmd(self, method, path, body=None, headers=None, success=200):
- resp, reply = self.raw_http_cmd(method, path, body, headers, success,
- json_reply=True)
- return reply
-
- def http_get(self, path, success=200):
- return self.http_cmd('GET', path, success=success)
-
- def http_post(self, path, body=None, headers=None, success=202):
- return self.http_cmd('POST', path, body, headers, success)
-
- def http_put(self, path, body=None, success=204):
- return self.http_cmd('PUT', path, body, success=success)
-
- def http_delete(self, path, success=204):
- return self.http_cmd('DELETE', path, success=success)
-
-
-class ComputeClient(Client):
- # Servers
-
- 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']
-
- 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']
-
- def create_server(self, name, flavor_id, image_id, personality=None):
- """Submit request to create a new server
-
- The flavor_id specifies the hardware configuration to use,
- the image_id specifies the OS Image to be deployed inside the new
- server.
-
- The personality argument is a list of (file path, file contents)
- tuples, describing files to be injected into the server upon creation.
-
- The call returns a dictionary describing the newly created server.
-
- """
-
- req = {'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']
-
- 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)
-
- 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)
-
- 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)
-
- 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)
-
- 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)
-
- 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']
-
- 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)
-
- def list_server_addresses(self, server_id, network=None):
- path = '/servers/%d/ips' % server_id
- if network:
- path += '/%s' % network
- reply = self.http_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.http_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.http_put(path, body, 201)
- return reply['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']
-
- def delete_server_metadata(self, server_id, key):
- path = '/servers/%d/meta/%s' % (server_id, key)
- reply = self.http_delete(path)
-
- def get_server_stats(self, server_id):
- path = '/servers/%d/stats' % server_id
- reply = self.http_get(path)
- return reply['stats']
-
-
- # Flavors
-
- def list_flavors(self, detail=False):
- path = '/flavors/detail' if detail else '/flavors'
- reply = self.http_get(path)
- return reply['flavors']['values']
-
- def get_flavor_details(self, flavor_id):
- path = '/flavors/%d' % flavor_id
- reply = self.http_get(path)
- return reply['flavor']
-
-
- # Images
-
- def list_images(self, detail=False):
- path = '/images/detail' if detail else '/images'
- reply = self.http_get(path)
- return reply['images']['values']
-
- def get_image_details(self, image_id):
- path = '/images/%d' % image_id
- reply = self.http_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.http_post('/images', body)
- return reply['image']
-
- def delete_image(self, image_id):
- path = '/images/%d' % image_id
- self.http_delete(path)
-
- def get_image_metadata(self, image_id, key=None):
- path = '/images/%d/meta' % image_id
- if key:
- path += '/%s' % key
- reply = self.http_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.http_put(path, body, 201)
- reply['meta']
-
- def update_image_metadata(self, image_id, **metadata):
- path = '/images/%d/meta' % image_id
- body = json.dumps({'metadata': metadata})
- reply = self.http_post(path, body, success=201)
- return reply['metadata']
-
- def delete_image_metadata(self, image_id, key):
- path = '/images/%d/meta/%s' % (image_id, key)
- reply = self.http_delete(path)
-
-
- # Networks
-
- def list_networks(self, detail=False):
- path = '/networks/detail' if detail else '/networks'
- reply = self.http_get(path)
- return reply['networks']['values']
-
- def create_network(self, name):
- body = json.dumps({'network': {'name': name}})
- reply = self.http_post('/networks', body)
- return reply['network']
-
- def get_network_details(self, network_id):
- path = '/networks/%s' % network_id
- reply = self.http_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.http_put(path, body)
-
- def delete_network(self, network_id):
- path = '/networks/%s' % network_id
- self.http_delete(path)
-
- 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)
-
- 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)
-
-
-class GlanceClient(Client):
- def list_public(self, detail=False, filters={}, order=''):
- path = '/images/detail' if detail else '/images/'
- params = {}
- params.update(filters)
-
- if order.startswith('-'):
- params['sort_dir'] = 'desc'
- order = order[1:]
- else:
- params['sort_dir'] = 'asc'
-
- if order:
- params['sort_key'] = order
-
- if params:
- path += '?' + '&'.join('%s=%s' % item for item in params.items())
- return self.http_get(path)
-
- def get_meta(self, image_id):
- path = '/images/%s' % image_id
- resp, buf = self.raw_http_cmd('HEAD', path)
- reply = {}
- prefix = 'x-image-meta-'
- for key, val in resp.getheaders():
- key = key.lower()
- if not key.startswith(prefix):
- continue
- key = key[len(prefix):]
- reply[key] = val
- return reply
-
- def register(self, name, location, params={}, properties={}):
- path = '/images/'
- headers = {}
- headers['x-image-meta-name'] = quote(name)
- headers['x-image-meta-location'] = location
- for key, val in params.items():
- if key in ('id', 'store', 'disk_format', 'container_format',
- 'size', 'checksum', 'is_public', 'owner'):
- key = 'x-image-meta-' + key.replace('_', '-')
- headers[key] = val
- for key, val in properties.items():
- headers['x-image-meta-property-' + quote(key)] = quote(val)
- return self.http_post(path, headers=headers, success=200)
-
- def list_members(self, image_id):
- path = '/images/%s/members' % image_id
- reply = self.http_get(path)
- return reply['members']
-
- def list_shared(self, member):
- path = '/shared-images/%s' % member
- reply = self.http_get(path)
- return reply['shared_images']
-
- def add_member(self, image_id, member):
- path = '/images/%s/members/%s' % (image_id, member)
- self.http_put(path)
-
- def remove_member(self, image_id, member):
- path = '/images/%s/members/%s' % (image_id, member)
- self.http_delete(path)
-
- def set_members(self, image_id, members):
- path = '/images/%s/members' % image_id
- req = {'memberships': [{'member_id': member} for member in members]}
- body = json.dumps(req)
- self.http_put(path, body)
--- /dev/null
+# 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.
+
+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
+ return r
+
+
+from .compute import ComputeClient
+from .image import ImageClient
+from .storage import StorageClient
+from .cyclades import CycladesClient
--- /dev/null
+# 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.
+
+"""
+ OpenStack Compute API 1.1 client
+"""
+
+import json
+
+from .http import HTTPClient
+
+
+class ComputeClient(HTTPClient):
+ 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']
+
+ 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']
+
+ def create_server(self, name, flavor_id, image_id, personality=None):
+ """Submit request to create a new server
+
+ The flavor_id specifies the hardware configuration to use,
+ the image_id specifies the OS Image to be deployed inside the new
+ server.
+
+ The personality argument is a list of (file path, file contents)
+ tuples, describing files to be injected into the server upon creation.
+
+ The call returns a dictionary describing the newly created server.
+ """
+ req = {'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']
+
+ 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)
+
+ 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)
+
+ 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)
+
+ def get_server_metadata(self, server_id, key=None):
+ path = '/servers/%d/meta' % server_id
+ if key:
+ path += '/%s' % key
+ reply = self.http_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.http_put(path, body, success=201)
+ return reply['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']
+
+ def delete_server_metadata(self, server_id, key):
+ path = '/servers/%d/meta/%s' % (server_id, key)
+ reply = self.http_delete(path)
+
+
+ def list_flavors(self, detail=False):
+ path = '/flavors/detail' if detail else '/flavors'
+ reply = self.http_get(path)
+ return reply['flavors']['values']
+
+ def get_flavor_details(self, flavor_id):
+ path = '/flavors/%d' % flavor_id
+ reply = self.http_get(path)
+ return reply['flavor']
+
+
+ def list_images(self, detail=False):
+ path = '/images/detail' if detail else '/images'
+ reply = self.http_get(path)
+ return reply['images']['values']
+
+ def get_image_details(self, image_id):
+ path = '/images/%d' % image_id
+ reply = self.http_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.http_post('/images', body)
+ return reply['image']
+
+ def delete_image(self, image_id):
+ path = '/images/%d' % image_id
+ self.http_delete(path)
+
+ def get_image_metadata(self, image_id, key=None):
+ path = '/images/%d/meta' % image_id
+ if key:
+ path += '/%s' % key
+ reply = self.http_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.http_put(path, body, success=201)
+ reply['meta']
+
+ def update_image_metadata(self, image_id, **metadata):
+ path = '/images/%d/meta' % image_id
+ body = json.dumps({'metadata': metadata})
+ reply = self.http_post(path, body, success=201)
+ return reply['metadata']
+
+ def delete_image_metadata(self, image_id, key):
+ path = '/images/%d/meta/%s' % (image_id, key)
+ reply = self.http_delete(path)
--- /dev/null
+# 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.
+
+"""
+ GRNet Cyclades API client
+"""
+
+import json
+
+from .http import HTTPClient
+
+
+class CycladesClient(HTTPClient):
+ 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)
+
+ 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)
+
+ 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']
+
+ 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)
+
+ def list_server_addresses(self, server_id, network=None):
+ path = '/servers/%d/ips' % server_id
+ if network:
+ path += '/%s' % network
+ reply = self.http_get(path)
+ return [reply['network']] if network else reply['addresses']['values']
+
+ def get_server_stats(self, server_id):
+ path = '/servers/%d/stats' % server_id
+ reply = self.http_get(path)
+ return reply['stats']
+
+
+ def list_networks(self, detail=False):
+ path = '/networks/detail' if detail else '/networks'
+ reply = self.http_get(path)
+ return reply['networks']['values']
+
+ def create_network(self, name):
+ body = json.dumps({'network': {'name': name}})
+ reply = self.http_post('/networks', body)
+ return reply['network']
+
+ def get_network_details(self, network_id):
+ path = '/networks/%s' % network_id
+ reply = self.http_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.http_put(path, body)
+
+ def delete_network(self, network_id):
+ path = '/networks/%s' % network_id
+ self.http_delete(path)
+
+ 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)
+
+ 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)
--- /dev/null
+# 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
+
+from . import ClientError
+
+
+log = logging.getLogger('kamaki.clients')
+
+
+class HTTPClient(object):
+ def __init__(self, url, token):
+ self.url = url
+ self.token = token
+
+ def raw_http_cmd(self, method, path, body=None, headers=None, success=200,
+ json_reply=False):
+ 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 = headers or {}
+ headers['X-Auth-Token'] = self.token
+ if body:
+ headers['Content-Type'] = 'application/json'
+ headers['Content-Length'] = len(body)
+
+ log.debug('>' * 50)
+ log.debug('%s %s', method, path)
+ for key, val in headers.items():
+ log.debug('%s: %s', key, val)
+ if body:
+ log.debug('')
+ log.debug(body)
+
+ conn.request(method, path, body, headers)
+
+ resp = conn.getresponse()
+ reply = resp.read()
+
+ log.debug('<' * 50)
+ log.info('%d %s', resp.status, resp.reason)
+ for key, val in resp.getheaders():
+ log.info('%s: %s', key.capitalize(), val)
+ log.info('')
+ log.debug(reply)
+ log.debug('-' * 50)
+
+ if json_reply:
+ try:
+ reply = json.loads(reply) if reply else {}
+ except ValueError:
+ raise ClientError('Did not receive valid JSON reply',
+ resp.status, reply)
+
+ if resp.status != success:
+ if len(reply) == 1:
+ if json_reply:
+ key = reply.keys()[0]
+ val = reply[key]
+ message = '%s: %s' % (key, val.get('message', ''))
+ details = val.get('details', '')
+ else:
+ message = reply
+ details = ''
+
+ raise ClientError(message, resp.status, details)
+ else:
+ raise ClientError('Invalid response from the server')
+
+ return resp, reply
+
+ def http_cmd(self, method, path, body=None, headers=None, success=200):
+ resp, reply = self.raw_http_cmd(method, path, body, headers, success,
+ json_reply=True)
+ return reply
+
+ def http_get(self, path, success=200):
+ return self.http_cmd('GET', path, success=success)
+
+ def http_post(self, path, body=None, headers=None, success=202):
+ return self.http_cmd('POST', path, body, headers, success)
+
+ def http_put(self, path, body=None, headers=None, success=204):
+ return self.http_cmd('PUT', path, body, headers, success)
+
+ def http_delete(self, path, success=204):
+ return self.http_cmd('DELETE', path, success=success)
--- /dev/null
+# 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.
+
+"""
+ OpenStack Image Service API 1.0 client
+"""
+
+from urllib import quote
+
+from .http import HTTPClient
+
+
+class ImageClient(HTTPClient):
+ def list_public(self, detail=False, filters={}, order=''):
+ path = '/images/detail' if detail else '/images/'
+ params = {}
+ params.update(filters)
+
+ if order.startswith('-'):
+ params['sort_dir'] = 'desc'
+ order = order[1:]
+ else:
+ params['sort_dir'] = 'asc'
+
+ if order:
+ params['sort_key'] = order
+
+ if params:
+ path += '?' + '&'.join('%s=%s' % item for item in params.items())
+ return self.http_get(path)
+
+ def get_meta(self, image_id):
+ path = '/images/%s' % image_id
+ resp, buf = self.raw_http_cmd('HEAD', path)
+ reply = {}
+ prefix = 'x-image-meta-'
+ for key, val in resp.getheaders():
+ key = key.lower()
+ if not key.startswith(prefix):
+ continue
+ key = key[len(prefix):]
+ reply[key] = val
+ return reply
+
+ def register(self, name, location, params={}, properties={}):
+ path = '/images/'
+ headers = {}
+ headers['x-image-meta-name'] = quote(name)
+ headers['x-image-meta-location'] = location
+ for key, val in params.items():
+ if key in ('id', 'store', 'disk_format', 'container_format',
+ 'size', 'checksum', 'is_public', 'owner'):
+ key = 'x-image-meta-' + key.replace('_', '-')
+ headers[key] = val
+ for key, val in properties.items():
+ headers['x-image-meta-property-' + quote(key)] = quote(val)
+ return self.http_post(path, headers=headers, success=200)
+
+ def list_members(self, image_id):
+ path = '/images/%s/members' % image_id
+ reply = self.http_get(path)
+ return reply['members']
+
+ def list_shared(self, member):
+ path = '/shared-images/%s' % member
+ reply = self.http_get(path)
+ return reply['shared_images']
+
+ def add_member(self, image_id, member):
+ path = '/images/%s/members/%s' % (image_id, member)
+ self.http_put(path)
+
+ def remove_member(self, image_id, member):
+ path = '/images/%s/members/%s' % (image_id, member)
+ self.http_delete(path)
+
+ def set_members(self, image_id, members):
+ path = '/images/%s/members' % image_id
+ req = {'memberships': [{'member_id': member} for member in members]}
+ body = json.dumps(req)
+ self.http_put(path, body)
--- /dev/null
+# 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.
+
+"""
+ OpenStack Object Storage API 1.0 client
+"""
+
+from .http import HTTPClient
+
+
+class StorageClient(HTTPClient):
+ def __init__(self, url, token, account, container):
+ self.url = url
+ self.token = token
+ self.account = account
+ self.container = container
+
+ def create_object(self, object, f):
+ path = '/%s/%s/%s' % (self.account, self.container, object)
+ data = f.read()
+ self.http_put(path, data, success=201)
+
+ def delete_object(self, object):
+ path = '/%s/%s/%s' % (self.account, self.container, object)
+ self.http_delete(path)
import logging
import os
+from os.path import exists, expanduser
+
+
+# Path to the file that stores the configuration
+CONFIG_PATH = expanduser('~/.kamakirc')
+
+# Name of a shell variable to bypass the CONFIG_PATH value
+CONFIG_ENV = 'KAMAKI_CONFIG'
+
+# The defaults also determine the allowed keys
+CONFIG_DEFAULTS = {
+ 'apis': 'compute image storage cyclades',
+ 'token': '',
+ 'compute_url': 'https://okeanos.grnet.gr/api/v1',
+ 'image_url': 'https://okeanos.grnet.gr/plankton',
+ 'storage_url': 'https://plus.pithos.grnet.gr/v1',
+ 'storage_account': '',
+ 'storage_container': '',
+ 'test_token': ''
+}
+
log = logging.getLogger('kamaki.config')
class Config(object):
- def __init__(self, path, env, defaults):
- self.path = os.environ.get(env, path)
- self.defaults = defaults
+ def __init__(self):
+ self.path = os.environ.get(CONFIG_ENV, CONFIG_PATH)
+ self.defaults = CONFIG_DEFAULTS
d = self.read()
for key, val in d.items():
- if key not in defaults:
+ if key not in self.defaults:
log.warning('Ignoring unknown config key "%s".', key)
self.d = d
self.overrides = {}
def read(self):
- if not os.path.exists(self.path):
+ if not exists(self.path):
return {}
with open(self.path) as f:
return self.overrides[key]
if key in self.d:
return self.d[key]
- return self.defaults.get(key)
+ return self.defaults.get(key, '')
def set(self, key, val):
if key not in self.defaults: