summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/direct.py54
-rw-r--r--nova/api/ec2/__init__.py57
-rw-r--r--nova/api/ec2/admin.py127
-rw-r--r--nova/api/ec2/apirequest.py27
-rw-r--r--nova/api/ec2/cloud.py271
-rw-r--r--nova/api/ec2/ec2utils.py32
-rw-r--r--nova/api/ec2/metadatarequesthandler.py2
-rw-r--r--nova/api/openstack/__init__.py108
-rw-r--r--nova/api/openstack/accounts.py86
-rw-r--r--nova/api/openstack/auth.py44
-rw-r--r--nova/api/openstack/backup_schedules.py7
-rw-r--r--nova/api/openstack/common.py94
-rw-r--r--nova/api/openstack/consoles.py2
-rw-r--r--nova/api/openstack/contrib/__init__.py22
-rw-r--r--nova/api/openstack/contrib/volumes.py336
-rw-r--r--nova/api/openstack/extensions.py450
-rw-r--r--nova/api/openstack/faults.py47
-rw-r--r--nova/api/openstack/flavors.py61
-rw-r--r--nova/api/openstack/image_metadata.py93
-rw-r--r--nova/api/openstack/images.py223
-rw-r--r--nova/api/openstack/limits.py358
-rw-r--r--nova/api/openstack/ratelimiting/__init__.py4
-rw-r--r--nova/api/openstack/server_metadata.py78
-rw-r--r--nova/api/openstack/servers.py601
-rw-r--r--nova/api/openstack/shared_ip_groups.py8
-rw-r--r--nova/api/openstack/users.py104
-rw-r--r--nova/api/openstack/versions.py60
-rw-r--r--nova/api/openstack/views/__init__.py0
-rw-r--r--nova/api/openstack/views/addresses.py42
-rw-r--r--nova/api/openstack/views/flavors.py96
-rw-r--r--nova/api/openstack/views/images.py114
-rw-r--r--nova/api/openstack/views/servers.py168
-rw-r--r--nova/api/openstack/views/versions.py59
-rw-r--r--nova/api/openstack/zones.py101
34 files changed, 3484 insertions, 452 deletions
diff --git a/nova/api/direct.py b/nova/api/direct.py
index 208b6d086..e5f33cee4 100644
--- a/nova/api/direct.py
+++ b/nova/api/direct.py
@@ -38,6 +38,7 @@ import routes
import webob
from nova import context
+from nova import exception
from nova import flags
from nova import utils
from nova import wsgi
@@ -187,7 +188,7 @@ class ServiceWrapper(wsgi.Controller):
def __init__(self, service_handle):
self.service_handle = service_handle
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
arg_dict = req.environ['wsgiorg.routing_args'][1]
action = arg_dict['action']
@@ -205,10 +206,53 @@ class ServiceWrapper(wsgi.Controller):
# NOTE(vish): make sure we have no unicode keys for py2.6.
params = dict([(str(k), v) for (k, v) in params.iteritems()])
result = method(context, **params)
- if type(result) is dict or type(result) is list:
- return self._serialize(result, req)
- else:
+ if result is None or type(result) is str or type(result) is unicode:
return result
+ try:
+ return self._serialize(result, req.best_match_content_type())
+ except:
+ raise exception.Error("returned non-serializable type: %s"
+ % result)
+
+
+class Limited(object):
+ __notdoc = """Limit the available methods on a given object.
+
+ (Not a docstring so that the docstring can be conditionally overriden.)
+
+ Useful when defining a public API that only exposes a subset of an
+ internal API.
+
+ Expected usage of this class is to define a subclass that lists the allowed
+ methods in the 'allowed' variable.
+
+ Additionally where appropriate methods can be added or overwritten, for
+ example to provide backwards compatibility.
+
+ The wrapping approach has been chosen so that the wrapped API can maintain
+ its own internal consistency, for example if it calls "self.create" it
+ should get its own create method rather than anything we do here.
+
+ """
+
+ _allowed = None
+
+ def __init__(self, proxy):
+ self._proxy = proxy
+ if not self.__doc__:
+ self.__doc__ = proxy.__doc__
+ if not self._allowed:
+ self._allowed = []
+
+ def __getattr__(self, key):
+ """Only return methods that are named in self._allowed."""
+ if key not in self._allowed:
+ raise AttributeError()
+ return getattr(self._proxy, key)
+
+ def __dir__(self):
+ """Only return methods that are named in self._allowed."""
+ return [x for x in dir(self._proxy) if x in self._allowed]
class Proxy(object):
@@ -218,7 +262,7 @@ class Proxy(object):
self.prefix = prefix
def __do_request(self, path, context, **kwargs):
- req = webob.Request.blank(path)
+ req = wsgi.Request.blank(path)
req.method = 'POST'
req.body = urllib.urlencode({'json': utils.dumps(kwargs)})
req.environ['openstack.context'] = context
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index 1a06b3f01..a3c3b25a1 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -20,7 +20,6 @@ Starting point for routing EC2 requests.
"""
-import datetime
import webob
import webob.dec
import webob.exc
@@ -32,7 +31,7 @@ from nova import log as logging
from nova import utils
from nova import wsgi
from nova.api.ec2 import apirequest
-from nova.api.ec2 import cloud
+from nova.api.ec2 import ec2utils
from nova.auth import manager
@@ -54,32 +53,32 @@ flags.DEFINE_list('lockout_memcached_servers', None,
class RequestLogging(wsgi.Middleware):
"""Access-Log akin logging for all EC2 API requests."""
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
+ start = utils.utcnow()
rv = req.get_response(self.application)
- self.log_request_completion(rv, req)
+ self.log_request_completion(rv, req, start)
return rv
- def log_request_completion(self, response, request):
- controller = request.environ.get('ec2.controller', None)
- if controller:
- controller = controller.__class__.__name__
- action = request.environ.get('ec2.action', None)
+ def log_request_completion(self, response, request, start):
+ apireq = request.environ.get('ec2.request', None)
+ if apireq:
+ controller = apireq.controller
+ action = apireq.action
+ else:
+ controller = None
+ action = None
ctxt = request.environ.get('ec2.context', None)
- seconds = 'X'
- microseconds = 'X'
- if ctxt:
- delta = datetime.datetime.utcnow() - \
- ctxt.timestamp
- seconds = delta.seconds
- microseconds = delta.microseconds
+ delta = utils.utcnow() - start
+ seconds = delta.seconds
+ microseconds = delta.microseconds
LOG.info(
"%s.%ss %s %s %s %s:%s %s [%s] %s %s",
seconds,
microseconds,
request.remote_addr,
request.method,
- request.path_info,
+ "%s%s" % (request.script_name, request.path_info),
controller,
action,
response.status_int,
@@ -116,7 +115,7 @@ class Lockout(wsgi.Middleware):
debug=0)
super(Lockout, self).__init__(application)
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
access_key = str(req.params['AWSAccessKeyId'])
failures_key = "authfailures-%s" % access_key
@@ -145,7 +144,7 @@ class Authenticate(wsgi.Middleware):
"""Authenticate an EC2 request and add 'ec2.context' to WSGI environ."""
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
# Read request signature and access id.
try:
@@ -194,7 +193,7 @@ class Requestify(wsgi.Middleware):
super(Requestify, self).__init__(app)
self.controller = utils.import_class(controller)()
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
non_args = ['Action', 'Signature', 'AWSAccessKeyId', 'SignatureMethod',
'SignatureVersion', 'Version', 'Timestamp']
@@ -202,6 +201,12 @@ class Requestify(wsgi.Middleware):
try:
# Raise KeyError if omitted
action = req.params['Action']
+ # Fix bug lp:720157 for older (version 1) clients
+ version = req.params['SignatureVersion']
+ if int(version) == 1:
+ non_args.remove('SignatureMethod')
+ if 'SignatureMethod' in args:
+ args.pop('SignatureMethod')
for non_arg in non_args:
# Remove, but raise KeyError if omitted
args.pop(non_arg)
@@ -273,7 +278,7 @@ class Authorizer(wsgi.Middleware):
},
}
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
context = req.environ['ec2.context']
controller = req.environ['ec2.request'].controller.__class__.__name__
@@ -294,7 +299,7 @@ class Authorizer(wsgi.Middleware):
return True
if 'none' in roles:
return False
- return any(context.project.has_role(context.user.id, role)
+ return any(context.project.has_role(context.user_id, role)
for role in roles)
@@ -307,7 +312,7 @@ class Executor(wsgi.Application):
response, or a 400 upon failure.
"""
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
context = req.environ['ec2.context']
api_request = req.environ['ec2.request']
@@ -317,13 +322,13 @@ class Executor(wsgi.Application):
except exception.InstanceNotFound as ex:
LOG.info(_('InstanceNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = cloud.id_to_ec2_id(ex.instance_id)
+ ec2_id = ec2utils.id_to_ec2_id(ex.instance_id)
message = _('Instance %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.VolumeNotFound as ex:
LOG.info(_('VolumeNotFound raised: %s'), unicode(ex),
context=context)
- ec2_id = cloud.id_to_ec2_id(ex.volume_id, 'vol-%08x')
+ ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x')
message = _('Volume %s not found') % ec2_id
return self._error(req, context, type(ex).__name__, message)
except exception.NotFound as ex:
@@ -369,7 +374,7 @@ class Executor(wsgi.Application):
class Versions(wsgi.Application):
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Respond to a request for all EC2 versions."""
# available api versions
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index d131e5257..0b27854ef 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -21,17 +21,21 @@ Admin API controller, exposed through http via the api worker.
"""
import base64
+import datetime
import IPy
import urllib
from nova import compute
from nova import db
from nova import exception
+from nova import flags
from nova import log as logging
+from nova import utils
+from nova.api.ec2 import ec2utils
from nova.auth import manager
-from nova.compute import instance_types
+FLAGS = flags.FLAGS
LOG = logging.getLogger('nova.api.ec2.admin')
@@ -58,22 +62,57 @@ def project_dict(project):
return {}
-def host_dict(host):
+def host_dict(host, compute_service, instances, volume_service, volumes, now):
"""Convert a host model object to a result dict"""
- if host:
- return host.state
- else:
- return {}
+ rv = {'hostname': host, 'instance_count': len(instances),
+ 'volume_count': len(volumes)}
+ if compute_service:
+ latest = compute_service['updated_at'] or compute_service['created_at']
+ delta = now - latest
+ if delta.seconds <= FLAGS.service_down_time:
+ rv['compute'] = 'up'
+ else:
+ rv['compute'] = 'down'
+ if volume_service:
+ latest = volume_service['updated_at'] or volume_service['created_at']
+ delta = now - latest
+ if delta.seconds <= FLAGS.service_down_time:
+ rv['volume'] = 'up'
+ else:
+ rv['volume'] = 'down'
+ return rv
-def instance_dict(name, inst):
- return {'name': name,
+def instance_dict(inst):
+ return {'name': inst['name'],
'memory_mb': inst['memory_mb'],
'vcpus': inst['vcpus'],
'disk_gb': inst['local_gb'],
'flavor_id': inst['flavorid']}
+def vpn_dict(project, vpn_instance):
+ rv = {'project_id': project.id,
+ 'public_ip': project.vpn_ip,
+ 'public_port': project.vpn_port}
+ if vpn_instance:
+ rv['instance_id'] = ec2utils.id_to_ec2_id(vpn_instance['id'])
+ rv['created_at'] = utils.isotime(vpn_instance['created_at'])
+ address = vpn_instance.get('fixed_ip', None)
+ if address:
+ rv['internal_ip'] = address['address']
+ if project.vpn_ip and project.vpn_port:
+ if utils.vpn_ping(project.vpn_ip, project.vpn_port):
+ rv['state'] = 'running'
+ else:
+ rv['state'] = 'down'
+ else:
+ rv['state'] = 'down - invalid project vpn config'
+ else:
+ rv['state'] = 'pending'
+ return rv
+
+
class AdminController(object):
"""
API Controller for users, hosts, nodes, and workers.
@@ -85,9 +124,10 @@ class AdminController(object):
def __init__(self):
self.compute_api = compute.API()
- def describe_instance_types(self, _context, **_kwargs):
- return {'instanceTypeSet': [instance_dict(n, v) for n, v in
- instance_types.INSTANCE_TYPES.iteritems()]}
+ def describe_instance_types(self, context, **_kwargs):
+ """Returns all active instance types data (vcpus, memory, etc.)"""
+ return {'instanceTypeSet': [instance_dict(v) for v in
+ db.instance_type_get_all(context).values()]}
def describe_user(self, _context, name, **_kwargs):
"""Returns user data, including access and secret keys."""
@@ -229,19 +269,68 @@ class AdminController(object):
raise exception.ApiError(_('operation must be add or remove'))
return True
+ def _vpn_for(self, context, project_id):
+ """Get the VPN instance for a project ID."""
+ for instance in db.instance_get_all_by_project(context, project_id):
+ if (instance['image_id'] == FLAGS.vpn_image_id
+ and not instance['state_description'] in
+ ['shutting_down', 'shutdown']):
+ return instance
+
+ def start_vpn(self, context, project):
+ instance = self._vpn_for(context, project)
+ if not instance:
+ # NOTE(vish) import delayed because of __init__.py
+ from nova.cloudpipe import pipelib
+ pipe = pipelib.CloudPipe()
+ try:
+ pipe.launch_vpn_instance(project)
+ except db.NoMoreNetworks:
+ raise exception.ApiError("Unable to claim IP for VPN instance"
+ ", ensure it isn't running, and try "
+ "again in a few minutes")
+ instance = self._vpn_for(context, project)
+ return {'instance_id': ec2utils.id_to_ec2_id(instance['id'])}
+
+ def describe_vpns(self, context):
+ vpns = []
+ for project in manager.AuthManager().get_projects():
+ instance = self._vpn_for(context, project.id)
+ vpns.append(vpn_dict(project, instance))
+ return {'items': vpns}
+
# FIXME(vish): these host commands don't work yet, perhaps some of the
# required data can be retrieved from service objects?
- def describe_hosts(self, _context, **_kwargs):
+ def describe_hosts(self, context, **_kwargs):
"""Returns status info for all nodes. Includes:
- * Disk Space
- * Instance List
- * RAM used
- * CPU used
- * DHCP servers running
- * Iptables / bridges
+ * Hostname
+ * Compute (up, down, None)
+ * Instance count
+ * Volume (up, down, None)
+ * Volume Count
"""
- return {'hostSet': [host_dict(h) for h in db.host_get_all()]}
+ services = db.service_get_all(context, False)
+ now = datetime.datetime.utcnow()
+ hosts = []
+ rv = []
+ for host in [service['host'] for service in services]:
+ if not host in hosts:
+ hosts.append(host)
+ for host in hosts:
+ compute = [s for s in services if s['host'] == host \
+ and s['binary'] == 'nova-compute']
+ if compute:
+ compute = compute[0]
+ instances = db.instance_get_all_by_host(context, host)
+ volume = [s for s in services if s['host'] == host \
+ and s['binary'] == 'nova-volume']
+ if volume:
+ volume = volume[0]
+ volumes = db.volume_get_all_by_host(context, host)
+ rv.append(host_dict(host, compute, instances, volume, volumes,
+ now))
+ return {'hosts': rv}
def describe_host(self, _context, name, **_kwargs):
"""Returns status info for single node."""
diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py
index 7e72d67fb..d7ad08d2f 100644
--- a/nova/api/ec2/apirequest.py
+++ b/nova/api/ec2/apirequest.py
@@ -20,6 +20,7 @@
APIRequest class
"""
+import datetime
import re
# TODO(termie): replace minidom with etree
from xml.dom import minidom
@@ -45,8 +46,29 @@ def _underscore_to_xmlcase(str):
return res[:1].lower() + res[1:]
+def _database_to_isoformat(datetimeobj):
+ """Return a xs:dateTime parsable string from datatime"""
+ return datetimeobj.strftime("%Y-%m-%dT%H:%M:%SZ")
+
+
def _try_convert(value):
- """Return a non-string if possible"""
+ """Return a non-string from a string or unicode, if possible.
+
+ ============= =====================================================
+ When value is returns
+ ============= =====================================================
+ zero-length ''
+ 'None' None
+ 'True' True
+ 'False' False
+ '0', '-0' 0
+ 0xN, -0xN int from hex (postitive) (N is any number)
+ 0bN, -0bN int from binary (positive) (N is any number)
+ * try conversion to int, float, complex, fallback value
+
+ """
+ if len(value) == 0:
+ return ''
if value == 'None':
return None
if value == 'True':
@@ -171,6 +193,9 @@ class APIRequest(object):
self._render_dict(xml, data_el, data.__dict__)
elif isinstance(data, bool):
data_el.appendChild(xml.createTextNode(str(data).lower()))
+ elif isinstance(data, datetime.datetime):
+ data_el.appendChild(
+ xml.createTextNode(_database_to_isoformat(data)))
elif data != None:
data_el.appendChild(xml.createTextNode(str(data)))
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 6919cd8d2..425784e8a 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -39,7 +39,9 @@ from nova import log as logging
from nova import network
from nova import utils
from nova import volume
+from nova.api.ec2 import ec2utils
from nova.compute import instance_types
+from nova.image import s3
FLAGS = flags.FLAGS
@@ -73,30 +75,19 @@ def _gen_key(context, user_id, key_name):
return {'private_key': private_key, 'fingerprint': fingerprint}
-def ec2_id_to_id(ec2_id):
- """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)"""
- return int(ec2_id.split('-')[-1], 16)
-
-
-def id_to_ec2_id(instance_id, template='i-%08x'):
- """Convert an instance ID (int) to an ec2 ID (i-[base 16 number])"""
- return template % instance_id
-
-
class CloudController(object):
""" CloudController provides the critical dispatch between
inbound API calls through the endpoint and messages
sent to the other nodes.
"""
def __init__(self):
- self.image_service = utils.import_object(FLAGS.image_service)
+ self.image_service = s3.S3ImageService()
self.network_api = network.API()
self.volume_api = volume.API()
self.compute_api = compute.API(
network_api=self.network_api,
- image_service=self.image_service,
volume_api=self.volume_api,
- hostname_factory=id_to_ec2_id)
+ hostname_factory=ec2utils.id_to_ec2_id)
self.setup()
def __str__(self):
@@ -115,7 +106,7 @@ class CloudController(object):
start = os.getcwd()
os.chdir(FLAGS.ca_path)
# TODO(vish): Do this with M2Crypto instead
- utils.runthis(_("Generating root CA: %s"), "sh genrootca.sh")
+ utils.runthis(_("Generating root CA: %s"), "sh", "genrootca.sh")
os.chdir(start)
def _get_mpi_data(self, context, project_id):
@@ -154,11 +145,12 @@ class CloudController(object):
availability_zone = self._get_availability_zone_by_host(ctxt, host)
floating_ip = db.instance_get_floating_address(ctxt,
instance_ref['id'])
- ec2_id = id_to_ec2_id(instance_ref['id'])
+ ec2_id = ec2utils.id_to_ec2_id(instance_ref['id'])
+ image_ec2_id = self._image_ec2_id(instance_ref['image_id'], 'machine')
data = {
'user-data': base64.b64decode(instance_ref['user_data']),
'meta-data': {
- 'ami-id': instance_ref['image_id'],
+ 'ami-id': image_ec2_id,
'ami-launch-index': instance_ref['launch_index'],
'ami-manifest-path': 'FIXME',
'block-device-mapping': {
@@ -173,15 +165,20 @@ class CloudController(object):
'instance-type': instance_ref['instance_type'],
'local-hostname': hostname,
'local-ipv4': address,
- 'kernel-id': instance_ref['kernel_id'],
'placement': {'availability-zone': availability_zone},
'public-hostname': hostname,
'public-ipv4': floating_ip or '',
'public-keys': keys,
- 'ramdisk-id': instance_ref['ramdisk_id'],
'reservation-id': instance_ref['reservation_id'],
'security-groups': '',
'mpi': mpi}}
+
+ for image_type in ['kernel', 'ramdisk']:
+ if '%s_id' % image_type in instance_ref:
+ ec2_id = self._image_ec2_id(instance_ref['%s_id' % image_type],
+ image_type)
+ data['meta-data']['%s-id' % image_type] = ec2_id
+
if False: # TODO(vish): store ancestor ids
data['ancestor-ami-ids'] = []
if False: # TODO(vish): store product codes
@@ -198,8 +195,9 @@ class CloudController(object):
return self._describe_availability_zones(context, **kwargs)
def _describe_availability_zones(self, context, **kwargs):
- enabled_services = db.service_get_all(context)
- disabled_services = db.service_get_all(context, True)
+ ctxt = context.elevated()
+ enabled_services = db.service_get_all(ctxt, False)
+ disabled_services = db.service_get_all(ctxt, True)
available_zones = []
for zone in [service.availability_zone for service
in enabled_services]:
@@ -223,7 +221,7 @@ class CloudController(object):
rv = {'availabilityZoneInfo': [{'zoneName': 'nova',
'zoneState': 'available'}]}
- services = db.service_get_all(context)
+ services = db.service_get_all(context, False)
now = datetime.datetime.utcnow()
hosts = []
for host in [service['host'] for service in services]:
@@ -282,7 +280,7 @@ class CloudController(object):
'description': 'fixme'}]}
def describe_key_pairs(self, context, key_name=None, **kwargs):
- key_pairs = db.key_pair_get_all_by_user(context, context.user.id)
+ key_pairs = db.key_pair_get_all_by_user(context, context.user_id)
if not key_name is None:
key_pairs = [x for x in key_pairs if x['name'] in key_name]
@@ -290,18 +288,18 @@ class CloudController(object):
for key_pair in key_pairs:
# filter out the vpn keys
suffix = FLAGS.vpn_key_suffix
- if context.user.is_admin() or \
+ if context.is_admin or \
not key_pair['name'].endswith(suffix):
result.append({
'keyName': key_pair['name'],
'keyFingerprint': key_pair['fingerprint'],
})
- return {'keypairsSet': result}
+ return {'keySet': result}
def create_key_pair(self, context, key_name, **kwargs):
LOG.audit(_("Create key pair %s"), key_name, context=context)
- data = _gen_key(context, context.user.id, key_name)
+ data = _gen_key(context, context.user_id, key_name)
return {'keyName': key_name,
'keyFingerprint': data['fingerprint'],
'keyMaterial': data['private_key']}
@@ -310,7 +308,7 @@ class CloudController(object):
def delete_key_pair(self, context, key_name, **kwargs):
LOG.audit(_("Delete key pair %s"), key_name, context=context)
try:
- db.key_pair_destroy(context, context.user.id, key_name)
+ db.key_pair_destroy(context, context.user_id, key_name)
except exception.NotFound:
# aws returns true even if the key doesn't exist
pass
@@ -318,14 +316,19 @@ class CloudController(object):
def describe_security_groups(self, context, group_name=None, **kwargs):
self.compute_api.ensure_default_security_group(context)
- if context.user.is_admin():
+ if group_name:
+ groups = []
+ for name in group_name:
+ group = db.security_group_get_by_name(context,
+ context.project_id,
+ name)
+ groups.append(group)
+ elif context.is_admin:
groups = db.security_group_get_all(context)
else:
groups = db.security_group_get_by_project(context,
context.project_id)
groups = [self._format_security_group(context, g) for g in groups]
- if not group_name is None:
- groups = [g for g in groups if g.name in group_name]
return {'securityGroupInfo':
list(sorted(groups,
@@ -494,7 +497,7 @@ class CloudController(object):
if db.security_group_exists(context, context.project_id, group_name):
raise exception.ApiError(_('group %s already exists') % group_name)
- group = {'user_id': context.user.id,
+ group = {'user_id': context.user_id,
'project_id': context.project_id,
'name': group_name,
'description': group_description}
@@ -519,7 +522,7 @@ class CloudController(object):
ec2_id = instance_id[0]
else:
ec2_id = instance_id
- instance_id = ec2_id_to_id(ec2_id)
+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
output = self.compute_api.get_console_output(
context, instance_id=instance_id)
now = datetime.datetime.utcnow()
@@ -529,15 +532,23 @@ class CloudController(object):
def get_ajax_console(self, context, instance_id, **kwargs):
ec2_id = instance_id[0]
- internal_id = ec2_id_to_id(ec2_id)
- return self.compute_api.get_ajax_console(context, internal_id)
+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
+ return self.compute_api.get_ajax_console(context,
+ instance_id=instance_id)
+
+ def get_vnc_console(self, context, instance_id, **kwargs):
+ """Returns vnc browser url. Used by OS dashboard."""
+ ec2_id = instance_id
+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
+ return self.compute_api.get_vnc_console(context,
+ instance_id=instance_id)
def describe_volumes(self, context, volume_id=None, **kwargs):
if volume_id:
volumes = []
for ec2_id in volume_id:
- internal_id = ec2_id_to_id(ec2_id)
- volume = self.volume_api.get(context, internal_id)
+ internal_id = ec2utils.ec2_id_to_id(ec2_id)
+ volume = self.volume_api.get(context, volume_id=internal_id)
volumes.append(volume)
else:
volumes = self.volume_api.get_all(context)
@@ -549,11 +560,11 @@ class CloudController(object):
instance_data = None
if volume.get('instance', None):
instance_id = volume['instance']['id']
- instance_ec2_id = id_to_ec2_id(instance_id)
+ instance_ec2_id = ec2utils.id_to_ec2_id(instance_id)
instance_data = '%s[%s]' % (instance_ec2_id,
volume['instance']['host'])
v = {}
- v['volumeId'] = id_to_ec2_id(volume['id'], 'vol-%08x')
+ v['volumeId'] = ec2utils.id_to_ec2_id(volume['id'], 'vol-%08x')
v['status'] = volume['status']
v['size'] = volume['size']
v['availabilityZone'] = volume['availability_zone']
@@ -561,7 +572,7 @@ class CloudController(object):
if context.is_admin:
v['status'] = '%s (%s, %s, %s, %s)' % (
volume['status'],
- volume['user_id'],
+ volume['project_id'],
volume['host'],
instance_data,
volume['mountpoint'])
@@ -571,8 +582,7 @@ class CloudController(object):
'device': volume['mountpoint'],
'instanceId': instance_ec2_id,
'status': 'attached',
- 'volumeId': id_to_ec2_id(volume['id'],
- 'vol-%08x')}]
+ 'volumeId': v['volumeId']}]
else:
v['attachmentSet'] = [{}]
@@ -582,33 +592,37 @@ class CloudController(object):
def create_volume(self, context, size, **kwargs):
LOG.audit(_("Create volume of %s GB"), size, context=context)
- volume = self.volume_api.create(context, size,
- kwargs.get('display_name'),
- kwargs.get('display_description'))
+ volume = self.volume_api.create(
+ context,
+ size=size,
+ name=kwargs.get('display_name'),
+ description=kwargs.get('display_description'))
# TODO(vish): Instance should be None at db layer instead of
# trying to lazy load, but for now we turn it into
# a dict to avoid an error.
return {'volumeSet': [self._format_volume(context, dict(volume))]}
def delete_volume(self, context, volume_id, **kwargs):
- volume_id = ec2_id_to_id(volume_id)
+ volume_id = ec2utils.ec2_id_to_id(volume_id)
self.volume_api.delete(context, volume_id=volume_id)
return True
def update_volume(self, context, volume_id, **kwargs):
- volume_id = ec2_id_to_id(volume_id)
+ volume_id = ec2utils.ec2_id_to_id(volume_id)
updatable_fields = ['display_name', 'display_description']
changes = {}
for field in updatable_fields:
if field in kwargs:
changes[field] = kwargs[field]
if changes:
- self.volume_api.update(context, volume_id, kwargs)
+ self.volume_api.update(context,
+ volume_id=volume_id,
+ fields=changes)
return True
def attach_volume(self, context, volume_id, instance_id, device, **kwargs):
- volume_id = ec2_id_to_id(volume_id)
- instance_id = ec2_id_to_id(instance_id)
+ volume_id = ec2utils.ec2_id_to_id(volume_id)
+ instance_id = ec2utils.ec2_id_to_id(instance_id)
msg = _("Attach volume %(volume_id)s to instance %(instance_id)s"
" at %(device)s") % locals()
LOG.audit(msg, context=context)
@@ -616,25 +630,25 @@ class CloudController(object):
instance_id=instance_id,
volume_id=volume_id,
device=device)
- volume = self.volume_api.get(context, volume_id)
+ volume = self.volume_api.get(context, volume_id=volume_id)
return {'attachTime': volume['attach_time'],
'device': volume['mountpoint'],
- 'instanceId': id_to_ec2_id(instance_id),
+ 'instanceId': ec2utils.id_to_ec2_id(instance_id),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
def detach_volume(self, context, volume_id, **kwargs):
- volume_id = ec2_id_to_id(volume_id)
+ volume_id = ec2utils.ec2_id_to_id(volume_id)
LOG.audit(_("Detach volume %s"), volume_id, context=context)
- volume = self.volume_api.get(context, volume_id)
+ volume = self.volume_api.get(context, volume_id=volume_id)
instance = self.compute_api.detach_volume(context, volume_id=volume_id)
return {'attachTime': volume['attach_time'],
'device': volume['mountpoint'],
- 'instanceId': id_to_ec2_id(instance['id']),
+ 'instanceId': ec2utils.id_to_ec2_id(instance['id']),
'requestId': context.request_id,
'status': volume['attach_status'],
- 'volumeId': id_to_ec2_id(volume_id, 'vol-%08x')}
+ 'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
def _convert_to_set(self, lst, label):
if lst == None or lst == []:
@@ -668,20 +682,21 @@ class CloudController(object):
if instance_id:
instances = []
for ec2_id in instance_id:
- internal_id = ec2_id_to_id(ec2_id)
- instance = self.compute_api.get(context, internal_id)
+ internal_id = ec2utils.ec2_id_to_id(ec2_id)
+ instance = self.compute_api.get(context,
+ instance_id=internal_id)
instances.append(instance)
else:
instances = self.compute_api.get_all(context, **kwargs)
for instance in instances:
- if not context.user.is_admin():
+ if not context.is_admin:
if instance['image_id'] == FLAGS.vpn_image_id:
continue
i = {}
instance_id = instance['id']
- ec2_id = id_to_ec2_id(instance_id)
+ ec2_id = ec2utils.id_to_ec2_id(instance_id)
i['instanceId'] = ec2_id
- i['imageId'] = instance['image_id']
+ i['imageId'] = self._image_ec2_id(instance['image_id'])
i['instanceState'] = {
'code': instance['state'],
'name': instance['state_description']}
@@ -702,7 +717,7 @@ class CloudController(object):
i['dnsName'] = i['publicDnsName'] or i['privateDnsName']
i['keyName'] = instance['key_name']
- if context.user.is_admin():
+ if context.is_admin:
i['keyName'] = '%s (%s, %s)' % (i['keyName'],
instance['project_id'],
instance['host'])
@@ -736,21 +751,23 @@ class CloudController(object):
def format_addresses(self, context):
addresses = []
- if context.user.is_admin():
+ if context.is_admin:
iterator = db.floating_ip_get_all(context)
else:
iterator = db.floating_ip_get_all_by_project(context,
context.project_id)
for floating_ip_ref in iterator:
+ if floating_ip_ref['project_id'] is None:
+ continue
address = floating_ip_ref['address']
ec2_id = None
if (floating_ip_ref['fixed_ip']
and floating_ip_ref['fixed_ip']['instance']):
instance_id = floating_ip_ref['fixed_ip']['instance']['id']
- ec2_id = id_to_ec2_id(instance_id)
+ ec2_id = ec2utils.id_to_ec2_id(instance_id)
address_rv = {'public_ip': address,
'instance_id': ec2_id}
- if context.user.is_admin():
+ if context.is_admin:
details = "%s (%s)" % (address_rv['instance_id'],
floating_ip_ref['project_id'])
address_rv['instance_id'] = details
@@ -764,13 +781,13 @@ class CloudController(object):
def release_address(self, context, public_ip, **kwargs):
LOG.audit(_("Release address %s"), public_ip, context=context)
- self.network_api.release_floating_ip(context, public_ip)
+ self.network_api.release_floating_ip(context, address=public_ip)
return {'releaseResponse': ["Address released."]}
def associate_address(self, context, instance_id, public_ip, **kwargs):
LOG.audit(_("Associate address %(public_ip)s to"
" instance %(instance_id)s") % locals(), context=context)
- instance_id = ec2_id_to_id(instance_id)
+ instance_id = ec2utils.ec2_id_to_id(instance_id)
self.compute_api.associate_floating_ip(context,
instance_id=instance_id,
address=public_ip)
@@ -778,18 +795,24 @@ class CloudController(object):
def disassociate_address(self, context, public_ip, **kwargs):
LOG.audit(_("Disassociate address %s"), public_ip, context=context)
- self.network_api.disassociate_floating_ip(context, public_ip)
+ self.network_api.disassociate_floating_ip(context, address=public_ip)
return {'disassociateResponse': ["Address disassociated."]}
def run_instances(self, context, **kwargs):
max_count = int(kwargs.get('max_count', 1))
+ if kwargs.get('kernel_id'):
+ kernel = self._get_image(context, kwargs['kernel_id'])
+ kwargs['kernel_id'] = kernel['id']
+ if kwargs.get('ramdisk_id'):
+ ramdisk = self._get_image(context, kwargs['ramdisk_id'])
+ kwargs['ramdisk_id'] = ramdisk['id']
instances = self.compute_api.create(context,
instance_type=instance_types.get_by_type(
kwargs.get('instance_type', None)),
- image_id=kwargs['image_id'],
+ image_id=self._get_image(context, kwargs['image_id'])['id'],
min_count=int(kwargs.get('min_count', max_count)),
max_count=max_count,
- kernel_id=kwargs.get('kernel_id', None),
+ kernel_id=kwargs.get('kernel_id'),
ramdisk_id=kwargs.get('ramdisk_id'),
display_name=kwargs.get('display_name'),
display_description=kwargs.get('display_description'),
@@ -806,7 +829,7 @@ class CloudController(object):
instance_id is a kwarg so its name cannot be modified."""
LOG.debug(_("Going to start terminating instances"))
for ec2_id in instance_id:
- instance_id = ec2_id_to_id(ec2_id)
+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
self.compute_api.delete(context, instance_id=instance_id)
return True
@@ -814,64 +837,105 @@ class CloudController(object):
"""instance_id is a list of instance ids"""
LOG.audit(_("Reboot instance %r"), instance_id, context=context)
for ec2_id in instance_id:
- instance_id = ec2_id_to_id(ec2_id)
+ instance_id = ec2utils.ec2_id_to_id(ec2_id)
self.compute_api.reboot(context, instance_id=instance_id)
return True
def rescue_instance(self, context, instance_id, **kwargs):
"""This is an extension to the normal ec2_api"""
- instance_id = ec2_id_to_id(instance_id)
+ instance_id = ec2utils.ec2_id_to_id(instance_id)
self.compute_api.rescue(context, instance_id=instance_id)
return True
def unrescue_instance(self, context, instance_id, **kwargs):
"""This is an extension to the normal ec2_api"""
- instance_id = ec2_id_to_id(instance_id)
+ instance_id = ec2utils.ec2_id_to_id(instance_id)
self.compute_api.unrescue(context, instance_id=instance_id)
return True
- def update_instance(self, context, ec2_id, **kwargs):
+ def update_instance(self, context, instance_id, **kwargs):
updatable_fields = ['display_name', 'display_description']
changes = {}
for field in updatable_fields:
if field in kwargs:
changes[field] = kwargs[field]
if changes:
- instance_id = ec2_id_to_id(ec2_id)
+ instance_id = ec2utils.ec2_id_to_id(instance_id)
self.compute_api.update(context, instance_id=instance_id, **kwargs)
return True
- def _format_image(self, context, image):
+ _type_prefix_map = {'machine': 'ami',
+ 'kernel': 'aki',
+ 'ramdisk': 'ari'}
+
+ def _image_ec2_id(self, image_id, image_type='machine'):
+ prefix = self._type_prefix_map[image_type]
+ template = prefix + '-%08x'
+ return ec2utils.id_to_ec2_id(int(image_id), template=template)
+
+ def _get_image(self, context, ec2_id):
+ try:
+ internal_id = ec2utils.ec2_id_to_id(ec2_id)
+ return self.image_service.show(context, internal_id)
+ except exception.NotFound:
+ return self.image_service.show_by_name(context, ec2_id)
+
+ def _format_image(self, image):
"""Convert from format defined by BaseImageService to S3 format."""
i = {}
- i['imageId'] = image.get('id')
- i['kernelId'] = image.get('kernel_id')
- i['ramdiskId'] = image.get('ramdisk_id')
- i['imageOwnerId'] = image.get('owner_id')
- i['imageLocation'] = image.get('location')
- i['imageState'] = image.get('status')
- i['type'] = image.get('type')
- i['isPublic'] = image.get('is_public')
- i['architecture'] = image.get('architecture')
+ image_type = image['properties'].get('type')
+ ec2_id = self._image_ec2_id(image.get('id'), image_type)
+ name = image.get('name')
+ if name:
+ i['imageId'] = "%s (%s)" % (ec2_id, name)
+ else:
+ i['imageId'] = ec2_id
+ kernel_id = image['properties'].get('kernel_id')
+ if kernel_id:
+ i['kernelId'] = self._image_ec2_id(kernel_id, 'kernel')
+ ramdisk_id = image['properties'].get('ramdisk_id')
+ if ramdisk_id:
+ i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ramdisk')
+ i['imageOwnerId'] = image['properties'].get('owner_id')
+ i['imageLocation'] = image['properties'].get('image_location')
+ i['imageState'] = image['properties'].get('image_state')
+ i['displayName'] = image.get('name')
+ i['description'] = image.get('description')
+ i['type'] = image_type
+ i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True'
+ i['architecture'] = image['properties'].get('architecture')
return i
def describe_images(self, context, image_id=None, **kwargs):
# NOTE: image_id is a list!
- images = self.image_service.index(context)
if image_id:
- images = filter(lambda x: x['id'] in image_id, images)
- images = [self._format_image(context, i) for i in images]
+ images = []
+ for ec2_id in image_id:
+ try:
+ image = self._get_image(context, ec2_id)
+ except exception.NotFound:
+ raise exception.NotFound(_('Image %s not found') %
+ ec2_id)
+ images.append(image)
+ else:
+ images = self.image_service.detail(context)
+ images = [self._format_image(i) for i in images]
return {'imagesSet': images}
def deregister_image(self, context, image_id, **kwargs):
LOG.audit(_("De-registering image %s"), image_id, context=context)
- self.image_service.deregister(context, image_id)
+ image = self._get_image(context, image_id)
+ internal_id = image['id']
+ self.image_service.delete(context, internal_id)
return {'imageId': image_id}
def register_image(self, context, image_location=None, **kwargs):
if image_location is None and 'name' in kwargs:
image_location = kwargs['name']
- image_id = self.image_service.register(context, image_location)
+ metadata = {'properties': {'image_location': image_location}}
+ image = self.image_service.create(context, metadata)
+ image_id = self._image_ec2_id(image['id'],
+ image['properties']['type'])
msg = _("Registered image %(image_location)s with"
" id %(image_id)s") % locals()
LOG.audit(msg, context=context)
@@ -882,14 +946,11 @@ class CloudController(object):
raise exception.ApiError(_('attribute not supported: %s')
% attribute)
try:
- image = self.image_service.show(context, image_id)
- image = self._format_image(context,
- self.image_service.show(context,
- image_id))
- except IndexError:
- raise exception.ApiError(_('invalid id: %s') % image_id)
- result = {'image_id': image_id, 'launchPermission': []}
- if image['isPublic']:
+ image = self._get_image(context, image_id)
+ except exception.NotFound:
+ raise exception.NotFound(_('Image %s not found') % image_id)
+ result = {'imageId': image_id, 'launchPermission': []}
+ if image['properties']['is_public']:
result['launchPermission'].append({'group': 'all'})
return result
@@ -906,8 +967,18 @@ class CloudController(object):
if not operation_type in ['add', 'remove']:
raise exception.ApiError(_('operation_type must be add or remove'))
LOG.audit(_("Updating image %s publicity"), image_id, context=context)
- return self.image_service.modify(context, image_id, operation_type)
+
+ try:
+ image = self._get_image(context, image_id)
+ except exception.NotFound:
+ raise exception.NotFound(_('Image %s not found') % image_id)
+ internal_id = image['id']
+ del(image['id'])
+
+ image['properties']['is_public'] = (operation_type == 'add')
+ return self.image_service.update(context, internal_id, image)
def update_image(self, context, image_id, **kwargs):
- result = self.image_service.update(context, image_id, dict(kwargs))
+ internal_id = ec2utils.ec2_id_to_id(image_id)
+ result = self.image_service.update(context, internal_id, dict(kwargs))
return result
diff --git a/nova/api/ec2/ec2utils.py b/nova/api/ec2/ec2utils.py
new file mode 100644
index 000000000..3b34f6ea5
--- /dev/null
+++ b/nova/api/ec2/ec2utils.py
@@ -0,0 +1,32 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 United States Government as represented by the
+# Administrator of the National Aeronautics and Space Administration.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova import exception
+
+
+def ec2_id_to_id(ec2_id):
+ """Convert an ec2 ID (i-[base 16 number]) to an instance id (int)"""
+ try:
+ return int(ec2_id.split('-')[-1], 16)
+ except ValueError:
+ raise exception.NotFound(_("Id %s Not Found") % ec2_id)
+
+
+def id_to_ec2_id(instance_id, template='i-%08x'):
+ """Convert an instance ID (int) to an ec2 ID (i-[base 16 number])"""
+ return template % instance_id
diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py
index 6fb441656..28f99b0ef 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -65,7 +65,7 @@ class MetadataRequestHandler(wsgi.Application):
data = data[item]
return data
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
cc = cloud.CloudController()
remote_address = req.remote_addr
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 056c7dd27..7545eb0c9 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -27,13 +27,19 @@ import webob.exc
from nova import flags
from nova import log as logging
from nova import wsgi
+from nova.api.openstack import accounts
from nova.api.openstack import faults
from nova.api.openstack import backup_schedules
from nova.api.openstack import consoles
from nova.api.openstack import flavors
from nova.api.openstack import images
+from nova.api.openstack import image_metadata
+from nova.api.openstack import limits
from nova.api.openstack import servers
+from nova.api.openstack import server_metadata
from nova.api.openstack import shared_ip_groups
+from nova.api.openstack import users
+from nova.api.openstack import zones
LOG = logging.getLogger('nova.api.openstack')
@@ -46,7 +52,7 @@ flags.DEFINE_bool('allow_admin_api',
class FaultWrapper(wsgi.Middleware):
"""Calls down the middleware stack, making exceptions into faults."""
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
try:
return req.get_response(self.application)
@@ -67,52 +73,102 @@ class APIRouter(wsgi.Router):
"""Simple paste factory, :class:`nova.wsgi.Router` doesn't have one"""
return cls()
- def __init__(self):
+ def __init__(self, ext_mgr=None):
+ self.server_members = {}
mapper = routes.Mapper()
+ self._setup_routes(mapper)
+ super(APIRouter, self).__init__(mapper)
- server_members = {'action': 'POST'}
+ def _setup_routes(self, mapper):
+ server_members = self.server_members
+ server_members['action'] = 'POST'
if FLAGS.allow_admin_api:
LOG.debug(_("Including admin operations in API."))
+
server_members['pause'] = 'POST'
server_members['unpause'] = 'POST'
- server_members["diagnostics"] = "GET"
- server_members["actions"] = "GET"
+ server_members['diagnostics'] = 'GET'
+ server_members['actions'] = 'GET'
server_members['suspend'] = 'POST'
server_members['resume'] = 'POST'
+ server_members['rescue'] = 'POST'
+ server_members['unrescue'] = 'POST'
+ server_members['reset_network'] = 'POST'
+ server_members['inject_network_info'] = 'POST'
- mapper.resource("server", "servers", controller=servers.Controller(),
- collection={'detail': 'GET'},
- member=server_members)
+ mapper.resource("zone", "zones", controller=zones.Controller(),
+ collection={'detail': 'GET', 'info': 'GET'}),
- mapper.resource("backup_schedule", "backup_schedule",
- controller=backup_schedules.Controller(),
- parent_resource=dict(member_name='server',
- collection_name='servers'))
+ mapper.resource("user", "users", controller=users.Controller(),
+ collection={'detail': 'GET'})
+
+ mapper.resource("account", "accounts",
+ controller=accounts.Controller(),
+ collection={'detail': 'GET'})
mapper.resource("console", "consoles",
controller=consoles.Controller(),
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("image", "images", controller=images.Controller(),
+ _limits = limits.LimitsController()
+ mapper.resource("limit", "limits", controller=_limits)
+
+ super(APIRouter, self).__init__(mapper)
+
+
+class APIRouterV10(APIRouter):
+ """Define routes specific to OpenStack API V1.0."""
+
+ def _setup_routes(self, mapper):
+ super(APIRouterV10, self)._setup_routes(mapper)
+ mapper.resource("server", "servers",
+ controller=servers.ControllerV10(),
+ collection={'detail': 'GET'},
+ member=self.server_members)
+
+ mapper.resource("image", "images",
+ controller=images.ControllerV10(),
collection={'detail': 'GET'})
- mapper.resource("flavor", "flavors", controller=flavors.Controller(),
+
+ mapper.resource("flavor", "flavors",
+ controller=flavors.ControllerV10(),
collection={'detail': 'GET'})
+
mapper.resource("shared_ip_group", "shared_ip_groups",
collection={'detail': 'GET'},
controller=shared_ip_groups.Controller())
- super(APIRouter, self).__init__(mapper)
+ mapper.resource("backup_schedule", "backup_schedule",
+ controller=backup_schedules.Controller(),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
-class Versions(wsgi.Application):
- @webob.dec.wsgify
- def __call__(self, req):
- """Respond to a request for all OpenStack API versions."""
- response = {
- "versions": [
- dict(status="CURRENT", id="v1.0")]}
- metadata = {
- "application/xml": {
- "attributes": dict(version=["status", "id"])}}
- return wsgi.Serializer(req.environ, metadata).to_content_type(response)
+class APIRouterV11(APIRouter):
+ """Define routes specific to OpenStack API V1.1."""
+
+ def _setup_routes(self, mapper):
+ super(APIRouterV11, self)._setup_routes(mapper)
+ mapper.resource("server", "servers",
+ controller=servers.ControllerV11(),
+ collection={'detail': 'GET'},
+ member=self.server_members)
+
+ mapper.resource("image", "images",
+ controller=images.ControllerV11(),
+ collection={'detail': 'GET'})
+
+ mapper.resource("image_meta", "meta",
+ controller=image_metadata.Controller(),
+ parent_resource=dict(member_name='image',
+ collection_name='images'))
+
+ mapper.resource("server_meta", "meta",
+ controller=server_metadata.Controller(),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
+ mapper.resource("flavor", "flavors",
+ controller=flavors.ControllerV11(),
+ collection={'detail': 'GET'})
diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py
new file mode 100644
index 000000000..86066fa20
--- /dev/null
+++ b/nova/api/openstack/accounts.py
@@ -0,0 +1,86 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import common
+import webob.exc
+
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import wsgi
+
+from nova.auth import manager
+from nova.api.openstack import faults
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.api.openstack')
+
+
+def _translate_keys(account):
+ return dict(id=account.id,
+ name=account.name,
+ description=account.description,
+ manager=account.project_manager_id)
+
+
+class Controller(wsgi.Controller):
+
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "account": ["id", "name", "description", "manager"]}}}
+
+ def __init__(self):
+ self.manager = manager.AuthManager()
+
+ def _check_admin(self, context):
+ """We cannot depend on the db layer to check for admin access
+ for the auth manager, so we do it here"""
+ if not context.is_admin:
+ raise exception.NotAuthorized(_("Not admin user."))
+
+ def index(self, req):
+ raise faults.Fault(webob.exc.HTTPNotImplemented())
+
+ def detail(self, req):
+ raise faults.Fault(webob.exc.HTTPNotImplemented())
+
+ def show(self, req, id):
+ """Return data about the given account id"""
+ account = self.manager.get_project(id)
+ return dict(account=_translate_keys(account))
+
+ def delete(self, req, id):
+ self._check_admin(req.environ['nova.context'])
+ self.manager.delete_project(id)
+ return {}
+
+ def create(self, req):
+ """We use update with create-or-update semantics
+ because the id comes from an external source"""
+ raise faults.Fault(webob.exc.HTTPNotImplemented())
+
+ def update(self, req, id):
+ """This is really create or update."""
+ self._check_admin(req.environ['nova.context'])
+ env = self._deserialize(req.body, req.get_content_type())
+ description = env['account'].get('description')
+ manager = env['account'].get('manager')
+ try:
+ account = self.manager.get_project(id)
+ self.manager.modify_project(id, manager, description)
+ except exception.NotFound:
+ account = self.manager.create_project(id, manager, description)
+ return dict(account=_translate_keys(account))
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index 1dfdd5318..f3a9bdeca 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -26,12 +26,15 @@ import webob.dec
from nova import auth
from nova import context
from nova import db
+from nova import exception
from nova import flags
+from nova import log as logging
from nova import manager
from nova import utils
from nova import wsgi
from nova.api.openstack import faults
+LOG = logging.getLogger('nova.api.openstack')
FLAGS = flags.FLAGS
@@ -45,18 +48,27 @@ class AuthMiddleware(wsgi.Middleware):
self.auth = auth.manager.AuthManager()
super(AuthMiddleware, self).__init__(application)
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
if not self.has_authentication(req):
return self.authenticate(req)
-
user = self.get_user_by_authentication(req)
-
+ accounts = self.auth.get_projects(user=user)
if not user:
return faults.Fault(webob.exc.HTTPUnauthorized())
- project = self.auth.get_project(FLAGS.default_project)
- req.environ['nova.context'] = context.RequestContext(user, project)
+ if accounts:
+ #we are punting on this til auth is settled,
+ #and possibly til api v1.1 (mdragon)
+ account = accounts[0]
+ else:
+ return faults.Fault(webob.exc.HTTPUnauthorized())
+
+ if not self.auth.is_admin(user) and \
+ not self.auth.is_project_member(user, account):
+ return faults.Fault(webob.exc.HTTPUnauthorized())
+
+ req.environ['nova.context'] = context.RequestContext(user, account)
return self.application
def has_authentication(self, req):
@@ -103,11 +115,14 @@ class AuthMiddleware(wsgi.Middleware):
2 days ago.
"""
ctxt = context.get_admin_context()
- token = self.db.auth_get_token(ctxt, token_hash)
+ try:
+ token = self.db.auth_token_get(ctxt, token_hash)
+ except exception.NotFound:
+ return None
if token:
delta = datetime.datetime.now() - token.created_at
if delta.days >= 2:
- self.db.auth_destroy_token(ctxt, token)
+ self.db.auth_token_destroy(ctxt, token.token_hash)
else:
return self.auth.get_user(token.user_id)
return None
@@ -117,20 +132,25 @@ class AuthMiddleware(wsgi.Middleware):
username - string
key - string API key
- req - webob.Request object
+ req - wsgi.Request object
"""
ctxt = context.get_admin_context()
- user = self.auth.get_user_from_access_key(key)
+
+ try:
+ user = self.auth.get_user_from_access_key(key)
+ except exception.NotFound:
+ user = None
+
if user and user.name == username:
token_hash = hashlib.sha1('%s%s%f' % (username, key,
time.time())).hexdigest()
token_dict = {}
token_dict['token_hash'] = token_hash
token_dict['cdn_management_url'] = ''
- # Same as auth url, e.g. http://foo.org:8774/baz/v1.0
- token_dict['server_management_url'] = req.url
+ os_url = req.url
+ token_dict['server_management_url'] = os_url
token_dict['storage_url'] = ''
token_dict['user_id'] = user.id
- token = self.db.auth_create_token(ctxt, token_dict)
+ token = self.db.auth_token_create(ctxt, token_dict)
return token, user
return None, None
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 197125d86..f2d2d86e8 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -15,7 +15,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import logging
import time
from webob import exc
@@ -43,7 +42,11 @@ class Controller(wsgi.Controller):
def index(self, req, server_id):
""" Returns the list of backup schedules for a given instance """
- return _translate_keys({})
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def show(self, req, server_id, id):
+ """ Returns a single backup schedule for a given instance """
+ return faults.Fault(exc.HTTPNotImplemented())
def create(self, req, server_id):
""" No actual update method required, since the existing API allows
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 6d2fa16e8..75aeb0a5f 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -15,29 +15,85 @@
# License for the specific language governing permissions and limitations
# under the License.
+from urlparse import urlparse
+
+import webob
+
from nova import exception
+from nova import flags
+from nova import log as logging
+
+
+LOG = logging.getLogger('common')
-def limited(items, req):
- """Return a slice of items according to requested offset and limit.
+FLAGS = flags.FLAGS
- items - a sliceable
- req - wobob.Request possibly containing offset and limit GET variables.
- offset is where to start in the list, and limit is the maximum number
- of items to return.
- If limit is not specified, 0, or > 1000, defaults to 1000.
+def limited(items, request, max_limit=FLAGS.osapi_max_limit):
"""
+ Return a slice of items according to requested offset and limit.
+
+ @param items: A sliceable entity
+ @param request: `wsgi.Request` possibly containing 'offset' and 'limit'
+ GET variables. 'offset' is where to start in the list,
+ and 'limit' is the maximum number of items to return. If
+ 'limit' is not specified, 0, or > max_limit, we default
+ to max_limit. Negative values for either offset or limit
+ will cause exc.HTTPBadRequest() exceptions to be raised.
+ @kwarg max_limit: The maximum number of items to return from 'items'
+ """
+ try:
+ offset = int(request.GET.get('offset', 0))
+ except ValueError:
+ raise webob.exc.HTTPBadRequest(_('offset param must be an integer'))
- offset = int(req.GET.get('offset', 0))
- limit = int(req.GET.get('limit', 0))
- if not limit:
- limit = 1000
- limit = min(1000, limit)
+ try:
+ limit = int(request.GET.get('limit', max_limit))
+ except ValueError:
+ raise webob.exc.HTTPBadRequest(_('limit param must be an integer'))
+
+ if limit < 0:
+ raise webob.exc.HTTPBadRequest(_('limit param must be positive'))
+
+ if offset < 0:
+ raise webob.exc.HTTPBadRequest(_('offset param must be positive'))
+
+ limit = min(max_limit, limit or max_limit)
range_end = offset + limit
return items[offset:range_end]
+def limited_by_marker(items, request, max_limit=FLAGS.osapi_max_limit):
+ """Return a slice of items according to the requested marker and limit."""
+
+ try:
+ marker = int(request.GET.get('marker', 0))
+ except ValueError:
+ raise webob.exc.HTTPBadRequest(_('marker param must be an integer'))
+
+ try:
+ limit = int(request.GET.get('limit', max_limit))
+ except ValueError:
+ raise webob.exc.HTTPBadRequest(_('limit param must be an integer'))
+
+ if limit < 0:
+ raise webob.exc.HTTPBadRequest(_('limit param must be positive'))
+
+ limit = min(max_limit, limit)
+ start_index = 0
+ if marker:
+ start_index = -1
+ for i, item in enumerate(items):
+ if item['id'] == marker:
+ start_index = i + 1
+ break
+ if start_index < 0:
+ raise webob.exc.HTTPBadRequest(_('marker [%s] not found' % marker))
+ range_end = start_index + limit
+ return items[start_index:range_end]
+
+
def get_image_id_from_image_hash(image_service, context, image_hash):
"""Given an Image ID Hash, return an objectstore Image ID.
@@ -58,3 +114,17 @@ def get_image_id_from_image_hash(image_service, context, image_hash):
if abs(hash(image_id)) == int(image_hash):
return image_id
raise exception.NotFound(image_hash)
+
+
+def get_id_from_href(href):
+ """Return the id portion of a url as an int.
+
+ Given: http://www.foo.com/bar/123?q=4
+ Returns: 123
+
+ """
+ try:
+ return int(urlparse(href).path.split('/')[-1])
+ except:
+ LOG.debug(_("Error extracting id from href: %s") % href)
+ raise webob.exc.HTTPBadRequest(_('could not parse id from href'))
diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py
index 9ebdbe710..8c291c2eb 100644
--- a/nova/api/openstack/consoles.py
+++ b/nova/api/openstack/consoles.py
@@ -65,7 +65,7 @@ class Controller(wsgi.Controller):
def create(self, req, server_id):
"""Creates a new console"""
- #info = self._deserialize(req.body, req)
+ #info = self._deserialize(req.body, req.get_content_type())
self.console_api.create_console(
req.environ['nova.context'],
int(server_id))
diff --git a/nova/api/openstack/contrib/__init__.py b/nova/api/openstack/contrib/__init__.py
new file mode 100644
index 000000000..b42a1d89d
--- /dev/null
+++ b/nova/api/openstack/contrib/__init__.py
@@ -0,0 +1,22 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 Justin Santa Barbara
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.import datetime
+
+"""Contrib contains extensions that are shipped with nova.
+
+It can't be called 'extensions' because that causes namespacing problems.
+
+"""
diff --git a/nova/api/openstack/contrib/volumes.py b/nova/api/openstack/contrib/volumes.py
new file mode 100644
index 000000000..6efacce52
--- /dev/null
+++ b/nova/api/openstack/contrib/volumes.py
@@ -0,0 +1,336 @@
+# Copyright 2011 Justin Santa Barbara
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""The volumes extension."""
+
+from webob import exc
+
+from nova import compute
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import volume
+from nova import wsgi
+from nova.api.openstack import common
+from nova.api.openstack import extensions
+from nova.api.openstack import faults
+
+
+LOG = logging.getLogger("nova.api.volumes")
+
+
+FLAGS = flags.FLAGS
+
+
+def _translate_volume_detail_view(context, vol):
+ """Maps keys for volumes details view."""
+
+ d = _translate_volume_summary_view(context, vol)
+
+ # No additional data / lookups at the moment
+
+ return d
+
+
+def _translate_volume_summary_view(context, vol):
+ """Maps keys for volumes summary view."""
+ d = {}
+
+ d['id'] = vol['id']
+ d['status'] = vol['status']
+ d['size'] = vol['size']
+ d['availabilityZone'] = vol['availability_zone']
+ d['createdAt'] = vol['created_at']
+
+ if vol['attach_status'] == 'attached':
+ d['attachments'] = [_translate_attachment_detail_view(context, vol)]
+ else:
+ d['attachments'] = [{}]
+
+ d['displayName'] = vol['display_name']
+ d['displayDescription'] = vol['display_description']
+ return d
+
+
+class VolumeController(wsgi.Controller):
+ """The Volumes API controller for the OpenStack API."""
+
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "volume": [
+ "id",
+ "status",
+ "size",
+ "availabilityZone",
+ "createdAt",
+ "displayName",
+ "displayDescription",
+ ]}}}
+
+ def __init__(self):
+ self.volume_api = volume.API()
+ super(VolumeController, self).__init__()
+
+ def show(self, req, id):
+ """Return data about the given volume."""
+ context = req.environ['nova.context']
+
+ try:
+ vol = self.volume_api.get(context, id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ return {'volume': _translate_volume_detail_view(context, vol)}
+
+ def delete(self, req, id):
+ """Delete a volume."""
+ context = req.environ['nova.context']
+
+ LOG.audit(_("Delete volume with id: %s"), id, context=context)
+
+ try:
+ self.volume_api.delete(context, volume_id=id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return exc.HTTPAccepted()
+
+ def index(self, req):
+ """Returns a summary list of volumes."""
+ return self._items(req, entity_maker=_translate_volume_summary_view)
+
+ def detail(self, req):
+ """Returns a detailed list of volumes."""
+ return self._items(req, entity_maker=_translate_volume_detail_view)
+
+ def _items(self, req, entity_maker):
+ """Returns a list of volumes, transformed through entity_maker."""
+ context = req.environ['nova.context']
+
+ volumes = self.volume_api.get_all(context)
+ limited_list = common.limited(volumes, req)
+ res = [entity_maker(context, vol) for vol in limited_list]
+ return {'volumes': res}
+
+ def create(self, req):
+ """Creates a new volume."""
+ context = req.environ['nova.context']
+
+ env = self._deserialize(req.body, req.get_content_type())
+ if not env:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ vol = env['volume']
+ size = vol['size']
+ LOG.audit(_("Create volume of %s GB"), size, context=context)
+ new_volume = self.volume_api.create(context, size,
+ vol.get('display_name'),
+ vol.get('display_description'))
+
+ # Work around problem that instance is lazy-loaded...
+ new_volume['instance'] = None
+
+ retval = _translate_volume_detail_view(context, new_volume)
+
+ return {'volume': retval}
+
+
+def _translate_attachment_detail_view(_context, vol):
+ """Maps keys for attachment details view."""
+
+ d = _translate_attachment_summary_view(_context, vol)
+
+ # No additional data / lookups at the moment
+
+ return d
+
+
+def _translate_attachment_summary_view(_context, vol):
+ """Maps keys for attachment summary view."""
+ d = {}
+
+ volume_id = vol['id']
+
+ # NOTE(justinsb): We use the volume id as the id of the attachment object
+ d['id'] = volume_id
+
+ d['volumeId'] = volume_id
+ if vol.get('instance_id'):
+ d['serverId'] = vol['instance_id']
+ if vol.get('mountpoint'):
+ d['device'] = vol['mountpoint']
+
+ return d
+
+
+class VolumeAttachmentController(wsgi.Controller):
+ """The volume attachment API controller for the Openstack API.
+
+ A child resource of the server. Note that we use the volume id
+ as the ID of the attachment (though this is not guaranteed externally)
+
+ """
+
+ _serialization_metadata = {
+ 'application/xml': {
+ 'attributes': {
+ 'volumeAttachment': ['id',
+ 'serverId',
+ 'volumeId',
+ 'device']}}}
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ self.volume_api = volume.API()
+ super(VolumeAttachmentController, self).__init__()
+
+ def index(self, req, server_id):
+ """Returns the list of volume attachments for a given instance."""
+ return self._items(req, server_id,
+ entity_maker=_translate_attachment_summary_view)
+
+ def show(self, req, server_id, id):
+ """Return data about the given volume attachment."""
+ context = req.environ['nova.context']
+
+ volume_id = id
+ try:
+ vol = self.volume_api.get(context, volume_id)
+ except exception.NotFound:
+ LOG.debug("volume_id not found")
+ return faults.Fault(exc.HTTPNotFound())
+
+ if str(vol['instance_id']) != server_id:
+ LOG.debug("instance_id != server_id")
+ return faults.Fault(exc.HTTPNotFound())
+
+ return {'volumeAttachment': _translate_attachment_detail_view(context,
+ vol)}
+
+ def create(self, req, server_id):
+ """Attach a volume to an instance."""
+ context = req.environ['nova.context']
+
+ env = self._deserialize(req.body, req.get_content_type())
+ if not env:
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+
+ instance_id = server_id
+ volume_id = env['volumeAttachment']['volumeId']
+ device = env['volumeAttachment']['device']
+
+ msg = _("Attach volume %(volume_id)s to instance %(server_id)s"
+ " at %(device)s") % locals()
+ LOG.audit(msg, context=context)
+
+ try:
+ self.compute_api.attach_volume(context,
+ instance_id=instance_id,
+ volume_id=volume_id,
+ device=device)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ # The attach is async
+ attachment = {}
+ attachment['id'] = volume_id
+ attachment['volumeId'] = volume_id
+
+ # NOTE(justinsb): And now, we have a problem...
+ # The attach is async, so there's a window in which we don't see
+ # the attachment (until the attachment completes). We could also
+ # get problems with concurrent requests. I think we need an
+ # attachment state, and to write to the DB here, but that's a bigger
+ # change.
+ # For now, we'll probably have to rely on libraries being smart
+
+ # TODO(justinsb): How do I return "accepted" here?
+ return {'volumeAttachment': attachment}
+
+ def update(self, _req, _server_id, _id):
+ """Update a volume attachment. We don't currently support this."""
+ return faults.Fault(exc.HTTPBadRequest())
+
+ def delete(self, req, server_id, id):
+ """Detach a volume from an instance."""
+ context = req.environ['nova.context']
+
+ volume_id = id
+ LOG.audit(_("Detach volume %s"), volume_id, context=context)
+
+ try:
+ vol = self.volume_api.get(context, volume_id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ if str(vol['instance_id']) != server_id:
+ LOG.debug("instance_id != server_id")
+ return faults.Fault(exc.HTTPNotFound())
+
+ self.compute_api.detach_volume(context,
+ volume_id=volume_id)
+
+ return exc.HTTPAccepted()
+
+ def _items(self, req, server_id, entity_maker):
+ """Returns a list of attachments, transformed through entity_maker."""
+ context = req.environ['nova.context']
+
+ try:
+ instance = self.compute_api.get(context, server_id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ volumes = instance['volumes']
+ limited_list = common.limited(volumes, req)
+ res = [entity_maker(context, vol) for vol in limited_list]
+ return {'volumeAttachments': res}
+
+
+class Volumes(extensions.ExtensionDescriptor):
+ def get_name(self):
+ return "Volumes"
+
+ def get_alias(self):
+ return "VOLUMES"
+
+ def get_description(self):
+ return "Volumes support"
+
+ def get_namespace(self):
+ return "http://docs.openstack.org/ext/volumes/api/v1.1"
+
+ def get_updated(self):
+ return "2011-03-25T00:00:00+00:00"
+
+ def get_resources(self):
+ resources = []
+
+ # NOTE(justinsb): No way to provide singular name ('volume')
+ # Does this matter?
+ res = extensions.ResourceExtension('volumes',
+ VolumeController(),
+ collection_actions={'detail': 'GET'}
+ )
+ resources.append(res)
+
+ res = extensions.ResourceExtension('volume_attachments',
+ VolumeAttachmentController(),
+ parent=dict(
+ member_name='server',
+ collection_name='servers'))
+ resources.append(res)
+
+ return resources
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
new file mode 100644
index 000000000..fb1dccb28
--- /dev/null
+++ b/nova/api/openstack/extensions.py
@@ -0,0 +1,450 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# Copyright 2011 Justin Santa Barbara
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import imp
+import inspect
+import os
+import sys
+import routes
+import webob.dec
+import webob.exc
+
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import wsgi
+from nova.api.openstack import faults
+
+
+LOG = logging.getLogger('extensions')
+
+
+FLAGS = flags.FLAGS
+
+
+class ExtensionDescriptor(object):
+ """Base class that defines the contract for extensions.
+
+ Note that you don't have to derive from this class to have a valid
+ extension; it is purely a convenience.
+
+ """
+
+ def get_name(self):
+ """The name of the extension.
+
+ e.g. 'Fox In Socks'
+
+ """
+ raise NotImplementedError()
+
+ def get_alias(self):
+ """The alias for the extension.
+
+ e.g. 'FOXNSOX'
+
+ """
+ raise NotImplementedError()
+
+ def get_description(self):
+ """Friendly description for the extension.
+
+ e.g. 'The Fox In Socks Extension'
+
+ """
+ raise NotImplementedError()
+
+ def get_namespace(self):
+ """The XML namespace for the extension.
+
+ e.g. 'http://www.fox.in.socks/api/ext/pie/v1.0'
+
+ """
+ raise NotImplementedError()
+
+ def get_updated(self):
+ """The timestamp when the extension was last updated.
+
+ e.g. '2011-01-22T13:25:27-06:00'
+
+ """
+ # NOTE(justinsb): Not sure of the purpose of this is, vs the XML NS
+ raise NotImplementedError()
+
+ def get_resources(self):
+ """List of extensions.ResourceExtension extension objects.
+
+ Resources define new nouns, and are accessible through URLs.
+
+ """
+ resources = []
+ return resources
+
+ def get_actions(self):
+ """List of extensions.ActionExtension extension objects.
+
+ Actions are verbs callable from the API.
+
+ """
+ actions = []
+ return actions
+
+ def get_response_extensions(self):
+ """List of extensions.ResponseExtension extension objects.
+
+ Response extensions are used to insert information into existing
+ response data.
+
+ """
+ response_exts = []
+ return response_exts
+
+
+class ActionExtensionController(wsgi.Controller):
+
+ def __init__(self, application):
+
+ self.application = application
+ self.action_handlers = {}
+
+ def add_action(self, action_name, handler):
+ self.action_handlers[action_name] = handler
+
+ def action(self, req, id):
+
+ input_dict = self._deserialize(req.body, req.get_content_type())
+ for action_name, handler in self.action_handlers.iteritems():
+ if action_name in input_dict:
+ return handler(input_dict, req, id)
+ # no action handler found (bump to downstream application)
+ res = self.application
+ return res
+
+
+class ResponseExtensionController(wsgi.Controller):
+
+ def __init__(self, application):
+ self.application = application
+ self.handlers = []
+
+ def add_handler(self, handler):
+ self.handlers.append(handler)
+
+ def process(self, req, *args, **kwargs):
+ res = req.get_response(self.application)
+ content_type = req.best_match_content_type()
+ # currently response handlers are un-ordered
+ for handler in self.handlers:
+ res = handler(res)
+ try:
+ body = res.body
+ headers = res.headers
+ except AttributeError:
+ body = self._serialize(res, content_type)
+ headers = {"Content-Type": content_type}
+ res = webob.Response()
+ res.body = body
+ res.headers = headers
+ return res
+
+
+class ExtensionController(wsgi.Controller):
+
+ def __init__(self, extension_manager):
+ self.extension_manager = extension_manager
+
+ def _translate(self, ext):
+ ext_data = {}
+ ext_data['name'] = ext.get_name()
+ ext_data['alias'] = ext.get_alias()
+ ext_data['description'] = ext.get_description()
+ ext_data['namespace'] = ext.get_namespace()
+ ext_data['updated'] = ext.get_updated()
+ ext_data['links'] = [] # TODO(dprince): implement extension links
+ return ext_data
+
+ def index(self, req):
+ extensions = []
+ for _alias, ext in self.extension_manager.extensions.iteritems():
+ extensions.append(self._translate(ext))
+ return dict(extensions=extensions)
+
+ def show(self, req, id):
+ # NOTE(dprince): the extensions alias is used as the 'id' for show
+ ext = self.extension_manager.extensions[id]
+ return self._translate(ext)
+
+ def delete(self, req, id):
+ raise faults.Fault(webob.exc.HTTPNotFound())
+
+ def create(self, req):
+ raise faults.Fault(webob.exc.HTTPNotFound())
+
+
+class ExtensionMiddleware(wsgi.Middleware):
+ """Extensions middleware for WSGI."""
+ @classmethod
+ def factory(cls, global_config, **local_config):
+ """Paste factory."""
+ def _factory(app):
+ return cls(app, **local_config)
+ return _factory
+
+ def _action_ext_controllers(self, application, ext_mgr, mapper):
+ """Return a dict of ActionExtensionController-s by collection."""
+ action_controllers = {}
+ for action in ext_mgr.get_actions():
+ if not action.collection in action_controllers.keys():
+ controller = ActionExtensionController(application)
+ mapper.connect("/%s/:(id)/action.:(format)" %
+ action.collection,
+ action='action',
+ controller=controller,
+ conditions=dict(method=['POST']))
+ mapper.connect("/%s/:(id)/action" % action.collection,
+ action='action',
+ controller=controller,
+ conditions=dict(method=['POST']))
+ action_controllers[action.collection] = controller
+
+ return action_controllers
+
+ def _response_ext_controllers(self, application, ext_mgr, mapper):
+ """Returns a dict of ResponseExtensionController-s by collection."""
+ response_ext_controllers = {}
+ for resp_ext in ext_mgr.get_response_extensions():
+ if not resp_ext.key in response_ext_controllers.keys():
+ controller = ResponseExtensionController(application)
+ mapper.connect(resp_ext.url_route + '.:(format)',
+ action='process',
+ controller=controller,
+ conditions=resp_ext.conditions)
+
+ mapper.connect(resp_ext.url_route,
+ action='process',
+ controller=controller,
+ conditions=resp_ext.conditions)
+ response_ext_controllers[resp_ext.key] = controller
+
+ return response_ext_controllers
+
+ def __init__(self, application, ext_mgr=None):
+
+ if ext_mgr is None:
+ ext_mgr = ExtensionManager(FLAGS.osapi_extensions_path)
+ self.ext_mgr = ext_mgr
+
+ mapper = routes.Mapper()
+
+ # extended resources
+ for resource in ext_mgr.get_resources():
+ LOG.debug(_('Extended resource: %s'),
+ resource.collection)
+ mapper.resource(resource.collection, resource.collection,
+ controller=resource.controller,
+ collection=resource.collection_actions,
+ member=resource.member_actions,
+ parent_resource=resource.parent)
+
+ # extended actions
+ action_controllers = self._action_ext_controllers(application, ext_mgr,
+ mapper)
+ for action in ext_mgr.get_actions():
+ LOG.debug(_('Extended action: %s'), action.action_name)
+ controller = action_controllers[action.collection]
+ controller.add_action(action.action_name, action.handler)
+
+ # extended responses
+ resp_controllers = self._response_ext_controllers(application, ext_mgr,
+ mapper)
+ for response_ext in ext_mgr.get_response_extensions():
+ LOG.debug(_('Extended response: %s'), response_ext.key)
+ controller = resp_controllers[response_ext.key]
+ controller.add_handler(response_ext.handler)
+
+ self._router = routes.middleware.RoutesMiddleware(self._dispatch,
+ mapper)
+
+ super(ExtensionMiddleware, self).__init__(application)
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """Route the incoming request with router."""
+ req.environ['extended.app'] = self.application
+ return self._router
+
+ @staticmethod
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def _dispatch(req):
+ """Dispatch the request.
+
+ Returns the routed WSGI app's response or defers to the extended
+ application.
+
+ """
+ match = req.environ['wsgiorg.routing_args'][1]
+ if not match:
+ return req.environ['extended.app']
+ app = match['controller']
+ return app
+
+
+class ExtensionManager(object):
+ """Load extensions from the configured extension path.
+
+ See nova/tests/api/openstack/extensions/foxinsocks/extension.py for an
+ example extension implementation.
+
+ """
+
+ def __init__(self, path):
+ LOG.audit(_('Initializing extension manager.'))
+
+ self.path = path
+ self.extensions = {}
+ self._load_all_extensions()
+
+ def get_resources(self):
+ """Returns a list of ResourceExtension objects."""
+ resources = []
+ resources.append(ResourceExtension('extensions',
+ ExtensionController(self)))
+ for alias, ext in self.extensions.iteritems():
+ try:
+ resources.extend(ext.get_resources())
+ except AttributeError:
+ # NOTE(dprince): Extension aren't required to have resource
+ # extensions
+ pass
+ return resources
+
+ def get_actions(self):
+ """Returns a list of ActionExtension objects."""
+ actions = []
+ for alias, ext in self.extensions.iteritems():
+ try:
+ actions.extend(ext.get_actions())
+ except AttributeError:
+ # NOTE(dprince): Extension aren't required to have action
+ # extensions
+ pass
+ return actions
+
+ def get_response_extensions(self):
+ """Returns a list of ResponseExtension objects."""
+ response_exts = []
+ for alias, ext in self.extensions.iteritems():
+ try:
+ response_exts.extend(ext.get_response_extensions())
+ except AttributeError:
+ # NOTE(dprince): Extension aren't required to have response
+ # extensions
+ pass
+ return response_exts
+
+ def _check_extension(self, extension):
+ """Checks for required methods in extension objects."""
+ try:
+ LOG.debug(_('Ext name: %s'), extension.get_name())
+ LOG.debug(_('Ext alias: %s'), extension.get_alias())
+ LOG.debug(_('Ext description: %s'), extension.get_description())
+ LOG.debug(_('Ext namespace: %s'), extension.get_namespace())
+ LOG.debug(_('Ext updated: %s'), extension.get_updated())
+ except AttributeError as ex:
+ LOG.exception(_("Exception loading extension: %s"), unicode(ex))
+
+ def _load_all_extensions(self):
+ """Load extensions from the configured path.
+
+ Load extensions from the configured path. The extension name is
+ constructed from the module_name. If your extension module was named
+ widgets.py the extension class within that module should be
+ 'Widgets'.
+
+ In addition, extensions are loaded from the 'contrib' directory.
+
+ See nova/tests/api/openstack/extensions/foxinsocks.py for an example
+ extension implementation.
+
+ """
+ if os.path.exists(self.path):
+ self._load_all_extensions_from_path(self.path)
+
+ contrib_path = os.path.join(os.path.dirname(__file__), "contrib")
+ if os.path.exists(contrib_path):
+ self._load_all_extensions_from_path(contrib_path)
+
+ def _load_all_extensions_from_path(self, path):
+ for f in os.listdir(path):
+ LOG.audit(_('Loading extension file: %s'), f)
+ mod_name, file_ext = os.path.splitext(os.path.split(f)[-1])
+ ext_path = os.path.join(path, f)
+ if file_ext.lower() == '.py' and not mod_name.startswith('_'):
+ mod = imp.load_source(mod_name, ext_path)
+ ext_name = mod_name[0].upper() + mod_name[1:]
+ new_ext_class = getattr(mod, ext_name, None)
+ if not new_ext_class:
+ LOG.warn(_('Did not find expected name '
+ '"%(ext_name)s" in %(file)s'),
+ {'ext_name': ext_name,
+ 'file': ext_path})
+ continue
+ new_ext = new_ext_class()
+ self._check_extension(new_ext)
+ self._add_extension(new_ext)
+
+ def _add_extension(self, ext):
+ alias = ext.get_alias()
+ LOG.audit(_('Loaded extension: %s'), alias)
+
+ self._check_extension(ext)
+
+ if alias in self.extensions:
+ raise exception.Error("Found duplicate extension: %s" % alias)
+ self.extensions[alias] = ext
+
+
+class ResponseExtension(object):
+ """Add data to responses from core nova OpenStack API controllers."""
+
+ def __init__(self, method, url_route, handler):
+ self.url_route = url_route
+ self.handler = handler
+ self.conditions = dict(method=[method])
+ self.key = "%s-%s" % (method, url_route)
+
+
+class ActionExtension(object):
+ """Add custom actions to core nova OpenStack API controllers."""
+
+ def __init__(self, collection, action_name, handler):
+ self.collection = collection
+ self.action_name = action_name
+ self.handler = handler
+
+
+class ResourceExtension(object):
+ """Add top level resources to the OpenStack API in nova."""
+
+ def __init__(self, collection, controller, parent=None,
+ collection_actions={}, member_actions={}):
+ self.collection = collection
+ self.controller = controller
+ self.parent = parent
+ self.collection_actions = collection_actions
+ self.member_actions = member_actions
diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py
index 224a7ef0b..940bd8771 100644
--- a/nova/api/openstack/faults.py
+++ b/nova/api/openstack/faults.py
@@ -42,7 +42,7 @@ class Fault(webob.exc.HTTPException):
"""Create a Fault for the given webob.exc.exception."""
self.wrapped_exc = exception
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Generate a WSGI response based on the exception passed to ctor."""
# Replace the body with fault details.
@@ -57,6 +57,47 @@ class Fault(webob.exc.HTTPException):
fault_data[fault_name]['retryAfter'] = retry
# 'code' is an attribute on the fault tag itself
metadata = {'application/xml': {'attributes': {fault_name: 'code'}}}
- serializer = wsgi.Serializer(req.environ, metadata)
- self.wrapped_exc.body = serializer.to_content_type(fault_data)
+ serializer = wsgi.Serializer(metadata)
+ content_type = req.best_match_content_type()
+ self.wrapped_exc.body = serializer.serialize(fault_data, content_type)
+ self.wrapped_exc.content_type = content_type
+ return self.wrapped_exc
+
+
+class OverLimitFault(webob.exc.HTTPException):
+ """
+ Rate-limited request response.
+ """
+
+ _serialization_metadata = {
+ "application/xml": {
+ "attributes": {
+ "overLimitFault": "code",
+ },
+ },
+ }
+
+ def __init__(self, message, details, retry_time):
+ """
+ Initialize new `OverLimitFault` with relevant information.
+ """
+ self.wrapped_exc = webob.exc.HTTPForbidden()
+ self.content = {
+ "overLimitFault": {
+ "code": self.wrapped_exc.status_int,
+ "message": message,
+ "details": details,
+ },
+ }
+
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, request):
+ """
+ Return the wrapped exception with a serialized body conforming to our
+ error format.
+ """
+ serializer = wsgi.Serializer(self._serialization_metadata)
+ content_type = request.best_match_content_type()
+ content = serializer.serialize(self.content, content_type)
+ self.wrapped_exc.body = content
return self.wrapped_exc
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index f620d4107..5b99b5a6f 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -15,13 +15,12 @@
# License for the specific language governing permissions and limitations
# under the License.
-from webob import exc
+import webob
-from nova.api.openstack import faults
-from nova.api.openstack import common
-from nova.compute import instance_types
+from nova import db
+from nova import exception
from nova import wsgi
-import nova.api.openstack
+from nova.api.openstack import views
class Controller(wsgi.Controller):
@@ -30,28 +29,50 @@ class Controller(wsgi.Controller):
_serialization_metadata = {
'application/xml': {
"attributes": {
- "flavor": ["id", "name", "ram", "disk"]}}}
+ "flavor": ["id", "name", "ram", "disk"],
+ "link": ["rel", "type", "href"],
+ }
+ }
+ }
def index(self, req):
"""Return all flavors in brief."""
- return dict(flavors=[dict(id=flavor['id'], name=flavor['name'])
- for flavor in self.detail(req)['flavors']])
+ items = self._get_flavors(req, is_detail=False)
+ return dict(flavors=items)
def detail(self, req):
"""Return all flavors in detail."""
- items = [self.show(req, id)['flavor'] for id in self._all_ids()]
- items = common.limited(items, req)
+ items = self._get_flavors(req, is_detail=True)
return dict(flavors=items)
+ def _get_flavors(self, req, is_detail=True):
+ """Helper function that returns a list of flavor dicts."""
+ ctxt = req.environ['nova.context']
+ flavors = db.api.instance_type_get_all(ctxt)
+ builder = self._get_view_builder(req)
+ items = [builder.build(flavor, is_detail=is_detail)
+ for flavor in flavors.values()]
+ return items
+
def show(self, req, id):
"""Return data about the given flavor id."""
- for name, val in instance_types.INSTANCE_TYPES.iteritems():
- if val['flavorid'] == int(id):
- item = dict(ram=val['memory_mb'], disk=val['local_gb'],
- id=val['flavorid'], name=name)
- return dict(flavor=item)
- raise faults.Fault(exc.HTTPNotFound())
-
- def _all_ids(self):
- """Return the list of all flavorids."""
- return [i['flavorid'] for i in instance_types.INSTANCE_TYPES.values()]
+ try:
+ ctxt = req.environ['nova.context']
+ flavor = db.api.instance_type_get_by_flavor_id(ctxt, id)
+ except exception.NotFound:
+ return webob.exc.HTTPNotFound()
+
+ builder = self._get_view_builder(req)
+ values = builder.build(flavor, is_detail=True)
+ return dict(flavor=values)
+
+
+class ControllerV10(Controller):
+ def _get_view_builder(self, req):
+ return views.flavors.ViewBuilder()
+
+
+class ControllerV11(Controller):
+ def _get_view_builder(self, req):
+ base_url = req.application_url
+ return views.flavors.ViewBuilderV11(base_url)
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
new file mode 100644
index 000000000..c9d6ac532
--- /dev/null
+++ b/nova/api/openstack/image_metadata.py
@@ -0,0 +1,93 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from webob import exc
+
+from nova import flags
+from nova import utils
+from nova import wsgi
+from nova.api.openstack import faults
+
+
+FLAGS = flags.FLAGS
+
+
+class Controller(wsgi.Controller):
+ """The image metadata API controller for the Openstack API"""
+
+ def __init__(self):
+ self.image_service = utils.import_object(FLAGS.image_service)
+ super(Controller, self).__init__()
+
+ def _get_metadata(self, context, image_id, image=None):
+ if not image:
+ image = self.image_service.show(context, image_id)
+ metadata = image.get('properties', {})
+ return metadata
+
+ def index(self, req, image_id):
+ """Returns the list of metadata for a given instance"""
+ context = req.environ['nova.context']
+ metadata = self._get_metadata(context, image_id)
+ return dict(metadata=metadata)
+
+ def show(self, req, image_id, id):
+ context = req.environ['nova.context']
+ metadata = self._get_metadata(context, image_id)
+ if id in metadata:
+ return {id: metadata[id]}
+ else:
+ return faults.Fault(exc.HTTPNotFound())
+
+ def create(self, req, image_id):
+ context = req.environ['nova.context']
+ body = self._deserialize(req.body, req.get_content_type())
+ img = self.image_service.show(context, image_id)
+ metadata = self._get_metadata(context, image_id, img)
+ if 'metadata' in body:
+ for key, value in body['metadata'].iteritems():
+ metadata[key] = value
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
+ return dict(metadata=metadata)
+
+ def update(self, req, image_id, id):
+ context = req.environ['nova.context']
+ body = self._deserialize(req.body, req.get_content_type())
+ if not id in body:
+ expl = _('Request body and URI mismatch')
+ raise exc.HTTPBadRequest(explanation=expl)
+ if len(body) > 1:
+ expl = _('Request body contains too many items')
+ raise exc.HTTPBadRequest(explanation=expl)
+ img = self.image_service.show(context, image_id)
+ metadata = self._get_metadata(context, image_id, img)
+ metadata[id] = body[id]
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
+
+ return req.body
+
+ def delete(self, req, image_id, id):
+ context = req.environ['nova.context']
+ img = self.image_service.show(context, image_id)
+ metadata = self._get_metadata(context, image_id)
+ if not id in metadata:
+ return faults.Fault(exc.HTTPNotFound())
+ metadata.pop(id)
+ img['properties'] = metadata
+ self.image_service.update(context, image_id, img, None)
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 9d56bc508..e77100d7b 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -1,6 +1,4 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 OpenStack LLC.
+# Copyright 2011 OpenStack LLC.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -15,154 +13,143 @@
# License for the specific language governing permissions and limitations
# under the License.
-import logging
+import datetime
-from webob import exc
+import webob.exc
from nova import compute
+from nova import exception
from nova import flags
+from nova import log
from nova import utils
from nova import wsgi
-import nova.api.openstack
from nova.api.openstack import common
from nova.api.openstack import faults
-import nova.image.service
+from nova.api.openstack.views import images as images_view
+LOG = log.getLogger('nova.api.openstack.images')
FLAGS = flags.FLAGS
-def _translate_keys(item):
- """
- Maps key names to Rackspace-like attributes for return
- also pares down attributes to those we want
- item is a dict
-
- Note: should be removed when the set of keys expected by the api
- and the set of keys returned by the image service are equivalent
-
- """
- # TODO(tr3buchet): this map is specific to s3 object store,
- # replace with a list of keys for _filter_keys later
- mapped_keys = {'status': 'imageState',
- 'id': 'imageId',
- 'name': 'imageLocation'}
-
- mapped_item = {}
- # TODO(tr3buchet):
- # this chunk of code works with s3 and the local image service/glance
- # when we switch to glance/local image service it can be replaced with
- # a call to _filter_keys, and mapped_keys can be changed to a list
- try:
- for k, v in mapped_keys.iteritems():
- # map s3 fields
- mapped_item[k] = item[v]
- except KeyError:
- # return only the fields api expects
- mapped_item = _filter_keys(item, mapped_keys.keys())
-
- return mapped_item
-
-
-def _translate_status(item):
- """
- Translates status of image to match current Rackspace api bindings
- item is a dict
-
- Note: should be removed when the set of statuses expected by the api
- and the set of statuses returned by the image service are equivalent
-
- """
- status_mapping = {
- 'pending': 'queued',
- 'decrypting': 'preparing',
- 'untarring': 'saving',
- 'available': 'active'}
- try:
- item['status'] = status_mapping[item['status']]
- except KeyError:
- # TODO(sirp): Performing translation of status (if necessary) here for
- # now. Perhaps this should really be done in EC2 API and
- # S3ImageService
- pass
-
- return item
-
-
-def _filter_keys(item, keys):
- """
- Filters all model attributes except for keys
- item is a dict
-
- """
- return dict((k, v) for k, v in item.iteritems() if k in keys)
-
-
-def _convert_image_id_to_hash(image):
- if 'imageId' in image:
- # Convert EC2-style ID (i-blah) to Rackspace-style (int)
- image_id = abs(hash(image['imageId']))
- image['imageId'] = image_id
- image['id'] = image_id
-
-
class Controller(wsgi.Controller):
+ """Base `wsgi.Controller` for retrieving/displaying images."""
_serialization_metadata = {
'application/xml': {
"attributes": {
"image": ["id", "name", "updated", "created", "status",
- "serverId", "progress"]}}}
+ "serverId", "progress"],
+ "link": ["rel", "type", "href"],
+ },
+ },
+ }
+
+ def __init__(self, image_service=None, compute_service=None):
+ """Initialize new `ImageController`.
- def __init__(self):
- self._service = utils.import_object(FLAGS.image_service)
+ :param compute_service: `nova.compute.api:API`
+ :param image_service: `nova.image.service:BaseImageService`
+ """
+ _default_service = utils.import_object(flags.FLAGS.image_service)
+
+ self._compute_service = compute_service or compute.API()
+ self._image_service = image_service or _default_service
def index(self, req):
- """Return all public images in brief"""
- items = self._service.index(req.environ['nova.context'])
- items = common.limited(items, req)
- items = [_filter_keys(item, ('id', 'name')) for item in items]
- return dict(images=items)
+ """Return an index listing of images available to the request.
+
+ :param req: `wsgi.Request` object
+ """
+ context = req.environ['nova.context']
+ images = self._image_service.index(context)
+ images = common.limited(images, req)
+ builder = self.get_builder(req).build
+ return dict(images=[builder(image, detail=False) for image in images])
def detail(self, req):
- """Return all public images in detail"""
- try:
- items = self._service.detail(req.environ['nova.context'])
- except NotImplementedError:
- items = self._service.index(req.environ['nova.context'])
- for image in items:
- _convert_image_id_to_hash(image)
+ """Return a detailed index listing of images available to the request.
- items = common.limited(items, req)
- items = [_translate_keys(item) for item in items]
- items = [_translate_status(item) for item in items]
- return dict(images=items)
+ :param req: `wsgi.Request` object.
+ """
+ context = req.environ['nova.context']
+ images = self._image_service.detail(context)
+ images = common.limited(images, req)
+ builder = self.get_builder(req).build
+ return dict(images=[builder(image, detail=True) for image in images])
def show(self, req, id):
- """Return data about the given image id"""
- image_id = common.get_image_id_from_image_hash(self._service,
- req.environ['nova.context'], id)
+ """Return detailed information about a specific image.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
+ context = req.environ['nova.context']
+
+ try:
+ image_id = int(id)
+ except ValueError:
+ explanation = _("Image not found.")
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
- image = self._service.show(req.environ['nova.context'], image_id)
- _convert_image_id_to_hash(image)
- return dict(image=image)
+ try:
+ image = self._image_service.show(context, image_id)
+ except exception.NotFound:
+ explanation = _("Image '%d' not found.") % (image_id)
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
+
+ return dict(image=self.get_builder(req).build(image, detail=True))
def delete(self, req, id):
- # Only public images are supported for now.
- raise faults.Fault(exc.HTTPNotFound())
+ """Delete an image, if allowed.
+
+ :param req: `wsgi.Request` object
+ :param id: Image identifier (integer)
+ """
+ image_id = id
+ context = req.environ['nova.context']
+ self._image_service.delete(context, image_id)
+ return webob.exc.HTTPNoContent()
def create(self, req):
+ """Snapshot a server instance and save the image.
+
+ :param req: `wsgi.Request` object
+ """
context = req.environ['nova.context']
- env = self._deserialize(req.body, req)
- instance_id = env["image"]["serverId"]
- name = env["image"]["name"]
+ content_type = req.get_content_type()
+ image = self._deserialize(req.body, content_type)
+
+ if not image:
+ raise webob.exc.HTTPBadRequest()
+
+ try:
+ server_id = image["image"]["serverId"]
+ image_name = image["image"]["name"]
+ except KeyError:
+ raise webob.exc.HTTPBadRequest()
+
+ image = self._compute_service.snapshot(context, server_id, image_name)
+ return self.get_builder(req).build(image, detail=True)
+
+ def get_builder(self, request):
+ """Indicates that you must use a Controller subclass."""
+ raise NotImplementedError
+
+
+class ControllerV10(Controller):
+ """Version 1.0 specific controller logic."""
+
+ def get_builder(self, request):
+ """Property to get the ViewBuilder class we need to use."""
+ base_url = request.application_url
+ return images_view.ViewBuilderV10(base_url)
- image_meta = compute.API().snapshot(
- context, instance_id, name)
- return dict(image=image_meta)
+class ControllerV11(Controller):
+ """Version 1.1 specific controller logic."""
- def update(self, req, id):
- # Users may not modify public images, and that's all that
- # we support for now.
- raise faults.Fault(exc.HTTPNotFound())
+ def get_builder(self, request):
+ """Property to get the ViewBuilder class we need to use."""
+ base_url = request.application_url
+ return images_view.ViewBuilderV11(base_url)
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
new file mode 100644
index 000000000..efc7d193d
--- /dev/null
+++ b/nova/api/openstack/limits.py
@@ -0,0 +1,358 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.import datetime
+
+"""
+Module dedicated functions/classes dealing with rate limiting requests.
+"""
+
+import copy
+import httplib
+import json
+import math
+import re
+import time
+import urllib
+import webob.exc
+
+from collections import defaultdict
+
+from webob.dec import wsgify
+
+from nova import wsgi
+from nova.api.openstack import faults
+from nova.wsgi import Controller
+from nova.wsgi import Middleware
+
+
+# Convenience constants for the limits dictionary passed to Limiter().
+PER_SECOND = 1
+PER_MINUTE = 60
+PER_HOUR = 60 * 60
+PER_DAY = 60 * 60 * 24
+
+
+class LimitsController(Controller):
+ """
+ Controller for accessing limits in the OpenStack API.
+ """
+
+ _serialization_metadata = {
+ "application/xml": {
+ "attributes": {
+ "limit": ["verb", "URI", "regex", "value", "unit",
+ "resetTime", "remaining", "name"],
+ },
+ "plurals": {
+ "rate": "limit",
+ },
+ },
+ }
+
+ def index(self, req):
+ """
+ Return all global and rate limit information.
+ """
+ abs_limits = {}
+ rate_limits = req.environ.get("nova.limits", [])
+
+ return {
+ "limits": {
+ "rate": rate_limits,
+ "absolute": abs_limits,
+ },
+ }
+
+
+class Limit(object):
+ """
+ Stores information about a limit for HTTP requets.
+ """
+
+ UNITS = {
+ 1: "SECOND",
+ 60: "MINUTE",
+ 60 * 60: "HOUR",
+ 60 * 60 * 24: "DAY",
+ }
+
+ def __init__(self, verb, uri, regex, value, unit):
+ """
+ Initialize a new `Limit`.
+
+ @param verb: HTTP verb (POST, PUT, etc.)
+ @param uri: Human-readable URI
+ @param regex: Regular expression format for this limit
+ @param value: Integer number of requests which can be made
+ @param unit: Unit of measure for the value parameter
+ """
+ self.verb = verb
+ self.uri = uri
+ self.regex = regex
+ self.value = int(value)
+ self.unit = unit
+ self.unit_string = self.display_unit().lower()
+ self.remaining = int(value)
+
+ if value <= 0:
+ raise ValueError("Limit value must be > 0")
+
+ self.last_request = None
+ self.next_request = None
+
+ self.water_level = 0
+ self.capacity = self.unit
+ self.request_value = float(self.capacity) / float(self.value)
+ self.error_message = _("Only %(value)s %(verb)s request(s) can be "\
+ "made to %(uri)s every %(unit_string)s." % self.__dict__)
+
+ def __call__(self, verb, url):
+ """
+ Represents a call to this limit from a relevant request.
+
+ @param verb: string http verb (POST, GET, etc.)
+ @param url: string URL
+ """
+ if self.verb != verb or not re.match(self.regex, url):
+ return
+
+ now = self._get_time()
+
+ if self.last_request is None:
+ self.last_request = now
+
+ leak_value = now - self.last_request
+
+ self.water_level -= leak_value
+ self.water_level = max(self.water_level, 0)
+ self.water_level += self.request_value
+
+ difference = self.water_level - self.capacity
+
+ self.last_request = now
+
+ if difference > 0:
+ self.water_level -= self.request_value
+ self.next_request = now + difference
+ return difference
+
+ cap = self.capacity
+ water = self.water_level
+ val = self.value
+
+ self.remaining = math.floor(((cap - water) / cap) * val)
+ self.next_request = now
+
+ def _get_time(self):
+ """Retrieve the current time. Broken out for testability."""
+ return time.time()
+
+ def display_unit(self):
+ """Display the string name of the unit."""
+ return self.UNITS.get(self.unit, "UNKNOWN")
+
+ def display(self):
+ """Return a useful representation of this class."""
+ return {
+ "verb": self.verb,
+ "URI": self.uri,
+ "regex": self.regex,
+ "value": self.value,
+ "remaining": int(self.remaining),
+ "unit": self.display_unit(),
+ "resetTime": int(self.next_request or self._get_time()),
+ }
+
+# "Limit" format is a dictionary with the HTTP verb, human-readable URI,
+# a regular-expression to match, value and unit of measure (PER_DAY, etc.)
+
+DEFAULT_LIMITS = [
+ Limit("POST", "*", ".*", 10, PER_MINUTE),
+ Limit("POST", "*/servers", "^/servers", 50, PER_DAY),
+ Limit("PUT", "*", ".*", 10, PER_MINUTE),
+ Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE),
+ Limit("DELETE", "*", ".*", 100, PER_MINUTE),
+]
+
+
+class RateLimitingMiddleware(Middleware):
+ """
+ Rate-limits requests passing through this middleware. All limit information
+ is stored in memory for this implementation.
+ """
+
+ def __init__(self, application, limits=None):
+ """
+ Initialize new `RateLimitingMiddleware`, which wraps the given WSGI
+ application and sets up the given limits.
+
+ @param application: WSGI application to wrap
+ @param limits: List of dictionaries describing limits
+ """
+ Middleware.__init__(self, application)
+ self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ @wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """
+ Represents a single call through this middleware. We should record the
+ request if we have a limit relevant to it. If no limit is relevant to
+ the request, ignore it.
+
+ If the request should be rate limited, return a fault telling the user
+ they are over the limit and need to retry later.
+ """
+ verb = req.method
+ url = req.url
+ context = req.environ.get("nova.context")
+
+ if context:
+ username = context.user_id
+ else:
+ username = None
+
+ delay, error = self._limiter.check_for_delay(verb, url, username)
+
+ if delay:
+ msg = _("This request was rate-limited.")
+ retry = time.time() + delay
+ return faults.OverLimitFault(msg, error, retry)
+
+ req.environ["nova.limits"] = self._limiter.get_limits(username)
+
+ return self.application
+
+
+class Limiter(object):
+ """
+ Rate-limit checking class which handles limits in memory.
+ """
+
+ def __init__(self, limits):
+ """
+ Initialize the new `Limiter`.
+
+ @param limits: List of `Limit` objects
+ """
+ self.limits = copy.deepcopy(limits)
+ self.levels = defaultdict(lambda: copy.deepcopy(limits))
+
+ def get_limits(self, username=None):
+ """
+ Return the limits for a given user.
+ """
+ return [limit.display() for limit in self.levels[username]]
+
+ def check_for_delay(self, verb, url, username=None):
+ """
+ Check the given verb/user/user triplet for limit.
+
+ @return: Tuple of delay (in seconds) and error message (or None, None)
+ """
+ delays = []
+
+ for limit in self.levels[username]:
+ delay = limit(verb, url)
+ if delay:
+ delays.append((delay, limit.error_message))
+
+ if delays:
+ delays.sort()
+ return delays[0]
+
+ return None, None
+
+
+class WsgiLimiter(object):
+ """
+ Rate-limit checking from a WSGI application. Uses an in-memory `Limiter`.
+
+ To use:
+ POST /<username> with JSON data such as:
+ {
+ "verb" : GET,
+ "path" : "/servers"
+ }
+
+ and receive a 204 No Content, or a 403 Forbidden with an X-Wait-Seconds
+ header containing the number of seconds to wait before the action would
+ succeed.
+ """
+
+ def __init__(self, limits=None):
+ """
+ Initialize the new `WsgiLimiter`.
+
+ @param limits: List of `Limit` objects
+ """
+ self._limiter = Limiter(limits or DEFAULT_LIMITS)
+
+ @wsgify(RequestClass=wsgi.Request)
+ def __call__(self, request):
+ """
+ Handles a call to this application. Returns 204 if the request is
+ acceptable to the limiter, else a 403 is returned with a relevant
+ header indicating when the request *will* succeed.
+ """
+ if request.method != "POST":
+ raise webob.exc.HTTPMethodNotAllowed()
+
+ try:
+ info = dict(json.loads(request.body))
+ except ValueError:
+ raise webob.exc.HTTPBadRequest()
+
+ username = request.path_info_pop()
+ verb = info.get("verb")
+ path = info.get("path")
+
+ delay, error = self._limiter.check_for_delay(verb, path, username)
+
+ if delay:
+ headers = {"X-Wait-Seconds": "%.2f" % delay}
+ return webob.exc.HTTPForbidden(headers=headers, explanation=error)
+ else:
+ return webob.exc.HTTPNoContent()
+
+
+class WsgiLimiterProxy(object):
+ """
+ Rate-limit requests based on answers from a remote source.
+ """
+
+ def __init__(self, limiter_address):
+ """
+ Initialize the new `WsgiLimiterProxy`.
+
+ @param limiter_address: IP/port combination of where to request limit
+ """
+ self.limiter_address = limiter_address
+
+ def check_for_delay(self, verb, path, username=None):
+ body = json.dumps({"verb": verb, "path": path})
+ headers = {"Content-Type": "application/json"}
+
+ conn = httplib.HTTPConnection(self.limiter_address)
+
+ if username:
+ conn.request("POST", "/%s" % (username), body, headers)
+ else:
+ conn.request("POST", "/", body, headers)
+
+ resp = conn.getresponse()
+
+ if 200 >= resp.status < 300:
+ return None, None
+
+ return resp.getheader("X-Wait-Seconds"), resp.read() or None
diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py
index cbb4b897e..88ffc3246 100644
--- a/nova/api/openstack/ratelimiting/__init__.py
+++ b/nova/api/openstack/ratelimiting/__init__.py
@@ -57,7 +57,7 @@ class RateLimitingMiddleware(wsgi.Middleware):
self.limiter = WSGIAppProxy(service_host)
super(RateLimitingMiddleware, self).__init__(application)
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
"""Rate limit the request.
@@ -183,7 +183,7 @@ class WSGIApp(object):
"""Create the WSGI application using the given Limiter instance."""
self.limiter = limiter
- @webob.dec.wsgify
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
parts = req.path_info.split('/')
# format: /limiter/<username>/<urlencoded action>
diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py
new file mode 100644
index 000000000..45bbac99d
--- /dev/null
+++ b/nova/api/openstack/server_metadata.py
@@ -0,0 +1,78 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from webob import exc
+
+from nova import compute
+from nova import wsgi
+from nova.api.openstack import faults
+
+
+class Controller(wsgi.Controller):
+ """ The server metadata API controller for the Openstack API """
+
+ def __init__(self):
+ self.compute_api = compute.API()
+ super(Controller, self).__init__()
+
+ def _get_metadata(self, context, server_id):
+ metadata = self.compute_api.get_instance_metadata(context, server_id)
+ meta_dict = {}
+ for key, value in metadata.iteritems():
+ meta_dict[key] = value
+ return dict(metadata=meta_dict)
+
+ def index(self, req, server_id):
+ """ Returns the list of metadata for a given instance """
+ context = req.environ['nova.context']
+ return self._get_metadata(context, server_id)
+
+ def create(self, req, server_id):
+ context = req.environ['nova.context']
+ body = self._deserialize(req.body, req.get_content_type())
+ self.compute_api.update_or_create_instance_metadata(context,
+ server_id,
+ body['metadata'])
+ return req.body
+
+ def update(self, req, server_id, id):
+ context = req.environ['nova.context']
+ body = self._deserialize(req.body, req.get_content_type())
+ if not id in body:
+ expl = _('Request body and URI mismatch')
+ raise exc.HTTPBadRequest(explanation=expl)
+ if len(body) > 1:
+ expl = _('Request body contains too many items')
+ raise exc.HTTPBadRequest(explanation=expl)
+ self.compute_api.update_or_create_instance_metadata(context,
+ server_id,
+ body)
+ return req.body
+
+ def show(self, req, server_id, id):
+ """ Return a single metadata item """
+ context = req.environ['nova.context']
+ data = self._get_metadata(context, server_id)
+ if id in data['metadata']:
+ return {id: data['metadata'][id]}
+ else:
+ return faults.Fault(exc.HTTPNotFound())
+
+ def delete(self, req, server_id, id):
+ """ Deletes an existing metadata """
+ context = req.environ['nova.context']
+ self.compute_api.delete_instance_metadata(context, server_id, id)
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 17c5519a1..6704a68ae 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -1,5 +1,3 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
# Copyright 2010 OpenStack LLC.
# All Rights Reserved.
#
@@ -15,107 +13,97 @@
# License for the specific language governing permissions and limitations
# under the License.
-import json
+import base64
+import hashlib
import traceback
from webob import exc
+from xml.dom import minidom
from nova import compute
+from nova import context
from nova import exception
from nova import flags
from nova import log as logging
-from nova import wsgi
+from nova import quota
from nova import utils
+from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
+import nova.api.openstack.views.addresses
+import nova.api.openstack.views.flavors
+import nova.api.openstack.views.servers
from nova.auth import manager as auth_manager
from nova.compute import instance_types
from nova.compute import power_state
import nova.api.openstack
+from nova.scheduler import api as scheduler_api
LOG = logging.getLogger('server')
-LOG.setLevel(logging.DEBUG)
-
-
FLAGS = flags.FLAGS
-def _translate_detail_keys(inst):
- """ Coerces into dictionary format, mapping everything to Rackspace-like
- attributes for return"""
- power_mapping = {
- None: 'build',
- power_state.NOSTATE: 'build',
- power_state.RUNNING: 'active',
- power_state.BLOCKED: 'active',
- power_state.SUSPENDED: 'suspended',
- power_state.PAUSED: 'paused',
- power_state.SHUTDOWN: 'active',
- power_state.SHUTOFF: 'active',
- power_state.CRASHED: 'error'}
- inst_dict = {}
-
- mapped_keys = dict(status='state', imageId='image_id',
- flavorId='instance_type', name='display_name', id='id')
-
- for k, v in mapped_keys.iteritems():
- inst_dict[k] = inst[v]
-
- inst_dict['status'] = power_mapping[inst_dict['status']]
- inst_dict['addresses'] = dict(public=[], private=[])
- inst_dict['metadata'] = {}
- inst_dict['hostId'] = ''
-
- return dict(server=inst_dict)
-
-
-def _translate_keys(inst):
- """ Coerces into dictionary format, excluding all model attributes
- save for id and name """
- return dict(server=dict(id=inst['id'], name=inst['display_name']))
-
-
class Controller(wsgi.Controller):
""" The Server API controller for the OpenStack API """
_serialization_metadata = {
- 'application/xml': {
+ "application/xml": {
"attributes": {
"server": ["id", "imageId", "name", "flavorId", "hostId",
- "status", "progress"]}}}
+ "status", "progress", "adminPass", "flavorRef",
+ "imageRef"],
+ "link": ["rel", "type", "href"],
+ },
+ },
+ }
def __init__(self):
self.compute_api = compute.API()
self._image_service = utils.import_object(FLAGS.image_service)
super(Controller, self).__init__()
+ def ips(self, req, id):
+ try:
+ instance = self.compute_api.get(req.environ['nova.context'], id)
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+
+ builder = self._get_addresses_view_builder(req)
+ return builder.build(instance)
+
def index(self, req):
""" Returns a list of server names and ids for a given user """
- return self._items(req, entity_maker=_translate_keys)
+ return self._items(req, is_detail=False)
def detail(self, req):
""" Returns a list of server details for a given user """
- return self._items(req, entity_maker=_translate_detail_keys)
+ return self._items(req, is_detail=True)
- def _items(self, req, entity_maker):
+ def _items(self, req, is_detail):
"""Returns a list of servers for a given user.
- entity_maker - either _translate_detail_keys or _translate_keys
+ builder - the response model builder
"""
instance_list = self.compute_api.get_all(req.environ['nova.context'])
- limited_list = common.limited(instance_list, req)
- res = [entity_maker(inst)['server'] for inst in limited_list]
- return dict(servers=res)
+ limited_list = self._limit_items(instance_list, req)
+ builder = self._get_view_builder(req)
+ servers = [builder.build(inst, is_detail)['server']
+ for inst in limited_list]
+ return dict(servers=servers)
+ @scheduler_api.redirect_handler
def show(self, req, id):
""" Returns server details by server id """
try:
- instance = self.compute_api.get(req.environ['nova.context'], id)
- return _translate_detail_keys(instance)
+ instance = self.compute_api.routing_get(
+ req.environ['nova.context'], id)
+ builder = self._get_view_builder(req)
+ return builder.build(instance, is_detail=True)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
+ @scheduler_api.redirect_handler
def delete(self, req, id):
""" Destroys a server """
try:
@@ -124,75 +112,233 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
- def _get_kernel_ramdisk_from_image(self, req, image_id):
- """
- Machine images are associated with Kernels and Ramdisk images via
- metadata stored in Glance as 'image_properties'
- """
- def lookup(param):
- _image_id = image_id
- try:
- return image['properties'][param]
- except KeyError:
- raise exception.NotFound(
- _("%(param)s property not found for image %(_image_id)s") %
- locals())
-
- image_id = str(image_id)
- image = self._image_service.show(req.environ['nova.context'], image_id)
- return lookup('kernel_id'), lookup('ramdisk_id')
-
def create(self, req):
""" Creates a new server for a given user """
- env = self._deserialize(req.body, req)
+ env = self._deserialize_create(req)
if not env:
return faults.Fault(exc.HTTPUnprocessableEntity())
- key_pair = auth_manager.AuthManager.get_key_pairs(
- req.environ['nova.context'])[0]
+ context = req.environ['nova.context']
+
+ key_name = None
+ key_data = None
+ key_pairs = auth_manager.AuthManager.get_key_pairs(context)
+ if key_pairs:
+ key_pair = key_pairs[0]
+ key_name = key_pair['name']
+ key_data = key_pair['public_key']
+
+ requested_image_id = self._image_id_from_req_data(env)
image_id = common.get_image_id_from_image_hash(self._image_service,
- req.environ['nova.context'], env['server']['imageId'])
+ context, requested_image_id)
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
req, image_id)
- instances = self.compute_api.create(
- req.environ['nova.context'],
- instance_types.get_by_flavor_id(env['server']['flavorId']),
- image_id,
- kernel_id=kernel_id,
- ramdisk_id=ramdisk_id,
- display_name=env['server']['name'],
- display_description=env['server']['name'],
- key_name=key_pair['name'],
- key_data=key_pair['public_key'])
- return _translate_keys(instances[0])
+ # Metadata is a list, not a Dictionary, because we allow duplicate keys
+ # (even though JSON can't encode this)
+ # In future, we may not allow duplicate keys.
+ # However, the CloudServers API is not definitive on this front,
+ # and we want to be compatible.
+ metadata = []
+ if env['server'].get('metadata'):
+ for k, v in env['server']['metadata'].items():
+ metadata.append({'key': k, 'value': v})
+
+ personality = env['server'].get('personality')
+ injected_files = []
+ if personality:
+ injected_files = self._get_injected_files(personality)
+
+ flavor_id = self._flavor_id_from_req_data(env)
+
+ if not 'name' in env['server']:
+ msg = _("Server name is not defined")
+ return exc.HTTPBadRequest(msg)
+
+ name = env['server']['name']
+ self._validate_server_name(name)
+ name = name.strip()
+
+ try:
+ (inst,) = self.compute_api.create(
+ context,
+ instance_types.get_by_flavor_id(flavor_id),
+ image_id,
+ kernel_id=kernel_id,
+ ramdisk_id=ramdisk_id,
+ display_name=name,
+ display_description=name,
+ key_name=key_name,
+ key_data=key_data,
+ metadata=metadata,
+ injected_files=injected_files)
+ except quota.QuotaError as error:
+ self._handle_quota_error(error)
+
+ inst['instance_type'] = flavor_id
+ inst['image_id'] = requested_image_id
+
+ builder = self._get_view_builder(req)
+ server = builder.build(inst, is_detail=True)
+ password = utils.generate_password(16)
+ server['server']['adminPass'] = password
+ self.compute_api.set_admin_password(context, server['server']['id'],
+ password)
+ return server
+
+ def _deserialize_create(self, request):
+ """
+ Deserialize a create request
+
+ Overrides normal behavior in the case of xml content
+ """
+ if request.content_type == "application/xml":
+ deserializer = ServerCreateRequestXMLDeserializer()
+ return deserializer.deserialize(request.body)
+ else:
+ return self._deserialize(request.body, request.get_content_type())
+
+ def _get_injected_files(self, personality):
+ """
+ Create a list of injected files from the personality attribute
+
+ At this time, injected_files must be formatted as a list of
+ (file_path, file_content) pairs for compatibility with the
+ underlying compute service.
+ """
+ injected_files = []
+
+ for item in personality:
+ try:
+ path = item['path']
+ contents = item['contents']
+ except KeyError as key:
+ expl = _('Bad personality format: missing %s') % key
+ raise exc.HTTPBadRequest(explanation=expl)
+ except TypeError:
+ expl = _('Bad personality format')
+ raise exc.HTTPBadRequest(explanation=expl)
+ try:
+ contents = base64.b64decode(contents)
+ except TypeError:
+ expl = _('Personality content for %s cannot be decoded') % path
+ raise exc.HTTPBadRequest(explanation=expl)
+ injected_files.append((path, contents))
+ return injected_files
+
+ def _handle_quota_error(self, error):
+ """
+ Reraise quota errors as api-specific http exceptions
+ """
+ if error.code == "OnsetFileLimitExceeded":
+ expl = _("Personality file limit exceeded")
+ raise exc.HTTPBadRequest(explanation=expl)
+ if error.code == "OnsetFilePathLimitExceeded":
+ expl = _("Personality file path too long")
+ raise exc.HTTPBadRequest(explanation=expl)
+ if error.code == "OnsetFileContentLimitExceeded":
+ expl = _("Personality file content too long")
+ raise exc.HTTPBadRequest(explanation=expl)
+ # if the original error is okay, just reraise it
+ raise error
+
+ @scheduler_api.redirect_handler
def update(self, req, id):
""" Updates the server name or password """
- inst_dict = self._deserialize(req.body, req)
+ if len(req.body) == 0:
+ raise exc.HTTPUnprocessableEntity()
+
+ inst_dict = self._deserialize(req.body, req.get_content_type())
if not inst_dict:
return faults.Fault(exc.HTTPUnprocessableEntity())
ctxt = req.environ['nova.context']
update_dict = {}
- if 'adminPass' in inst_dict['server']:
- update_dict['admin_pass'] = inst_dict['server']['adminPass']
- try:
- self.compute_api.set_admin_password(ctxt, id)
- except exception.TimeoutException, e:
- return exc.HTTPRequestTimeout()
+
if 'name' in inst_dict['server']:
- update_dict['display_name'] = inst_dict['server']['name']
+ name = inst_dict['server']['name']
+ self._validate_server_name(name)
+ update_dict['display_name'] = name.strip()
+
+ self._parse_update(ctxt, id, inst_dict, update_dict)
+
try:
self.compute_api.update(ctxt, id, **update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
+
return exc.HTTPNoContent()
+ def _validate_server_name(self, value):
+ if not isinstance(value, basestring):
+ msg = _("Server name is not a string or unicode")
+ raise exc.HTTPBadRequest(msg)
+
+ if value.strip() == '':
+ msg = _("Server name is an empty string")
+ raise exc.HTTPBadRequest(msg)
+
+ def _parse_update(self, context, id, inst_dict, update_dict):
+ pass
+
+ @scheduler_api.redirect_handler
def action(self, req, id):
- """ Multi-purpose method used to reboot, rebuild, and
- resize a server """
- input_dict = self._deserialize(req.body, req)
- #TODO(sandy): rebuild/resize not supported.
+ """Multi-purpose method used to reboot, rebuild, or
+ resize a server"""
+
+ actions = {
+ 'changePassword': self._action_change_password,
+ 'reboot': self._action_reboot,
+ 'resize': self._action_resize,
+ 'confirmResize': self._action_confirm_resize,
+ 'revertResize': self._action_revert_resize,
+ 'rebuild': self._action_rebuild,
+ }
+
+ input_dict = self._deserialize(req.body, req.get_content_type())
+ for key in actions.keys():
+ if key in input_dict:
+ return actions[key](input_dict, req, id)
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def _action_change_password(self, input_dict, req, id):
+ return exc.HTTPNotImplemented()
+
+ def _action_confirm_resize(self, input_dict, req, id):
+ try:
+ self.compute_api.confirm_resize(req.environ['nova.context'], id)
+ except Exception, e:
+ LOG.exception(_("Error in confirm-resize %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPNoContent()
+
+ def _action_revert_resize(self, input_dict, req, id):
+ try:
+ self.compute_api.revert_resize(req.environ['nova.context'], id)
+ except Exception, e:
+ LOG.exception(_("Error in revert-resize %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return exc.HTTPAccepted()
+
+ def _action_rebuild(self, input_dict, req, id):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def _action_resize(self, input_dict, req, id):
+ """ Resizes a given instance to the flavor size requested """
+ try:
+ if 'resize' in input_dict and 'flavorId' in input_dict['resize']:
+ flavor_id = input_dict['resize']['flavorId']
+ self.compute_api.resize(req.environ['nova.context'], id,
+ flavor_id)
+ else:
+ LOG.exception(_("Missing arguments for resize"))
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ except Exception, e:
+ LOG.exception(_("Error in resize %s"), e)
+ return faults.Fault(exc.HTTPBadRequest())
+ return faults.Fault(exc.HTTPAccepted())
+
+ def _action_reboot(self, input_dict, req, id):
try:
reboot_type = input_dict['reboot']['type']
except Exception:
@@ -205,6 +351,7 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
def lock(self, req, id):
"""
lock the instance with id
@@ -220,6 +367,7 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
def unlock(self, req, id):
"""
unlock the instance with id
@@ -235,6 +383,7 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
def get_lock(self, req, id):
"""
return the boolean state of (instance with id)'s lock
@@ -249,6 +398,37 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
+ def reset_network(self, req, id):
+ """
+ Reset networking on an instance (admin only).
+
+ """
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.reset_network(context, id)
+ except:
+ readable = traceback.format_exc()
+ LOG.exception(_("Compute.api::reset_network %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ @scheduler_api.redirect_handler
+ def inject_network_info(self, req, id):
+ """
+ Inject network info for an instance (admin only).
+
+ """
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.inject_network_info(context, id)
+ except:
+ readable = traceback.format_exc()
+ LOG.exception(_("Compute.api::inject_network_info %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ @scheduler_api.redirect_handler
def pause(self, req, id):
""" Permit Admins to Pause the server. """
ctxt = req.environ['nova.context']
@@ -260,6 +440,7 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
def unpause(self, req, id):
""" Permit Admins to Unpause the server. """
ctxt = req.environ['nova.context']
@@ -271,6 +452,7 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
def suspend(self, req, id):
"""permit admins to suspend the server"""
context = req.environ['nova.context']
@@ -282,6 +464,7 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
def resume(self, req, id):
"""permit admins to resume the server from suspend"""
context = req.environ['nova.context']
@@ -293,8 +476,33 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
+ def rescue(self, req, id):
+ """Permit users to rescue the server."""
+ context = req.environ["nova.context"]
+ try:
+ self.compute_api.rescue(context, id)
+ except:
+ readable = traceback.format_exc()
+ LOG.exception(_("compute.api::rescue %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ @scheduler_api.redirect_handler
+ def unrescue(self, req, id):
+ """Permit users to unrescue the server."""
+ context = req.environ["nova.context"]
+ try:
+ self.compute_api.unrescue(context, id)
+ except:
+ readable = traceback.format_exc()
+ LOG.exception(_("compute.api::unrescue %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ @scheduler_api.redirect_handler
def get_ajax_console(self, req, id):
- """ Returns a url to an instance's ajaxterm console. """
+ """Returns a url to an instance's ajaxterm console."""
try:
self.compute_api.get_ajax_console(req.environ['nova.context'],
int(id))
@@ -302,6 +510,17 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPNotFound())
return exc.HTTPAccepted()
+ @scheduler_api.redirect_handler
+ def get_vnc_console(self, req, id):
+ """Returns a url to an instance's ajaxterm console."""
+ try:
+ self.compute_api.get_vnc_console(req.environ['nova.context'],
+ int(id))
+ except exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return exc.HTTPAccepted()
+
+ @scheduler_api.redirect_handler
def diagnostics(self, req, id):
"""Permit Admins to retrieve server diagnostics."""
ctxt = req.environ["nova.context"]
@@ -320,3 +539,187 @@ class Controller(wsgi.Controller):
action=item.action,
error=item.error))
return dict(actions=actions)
+
+ def _get_kernel_ramdisk_from_image(self, req, image_id):
+ """Fetch an image from the ImageService, then if present, return the
+ associated kernel and ramdisk image IDs.
+ """
+ context = req.environ['nova.context']
+ image_meta = self._image_service.show(context, image_id)
+ # NOTE(sirp): extracted to a separate method to aid unit-testing, the
+ # new method doesn't need a request obj or an ImageService stub
+ kernel_id, ramdisk_id = self._do_get_kernel_ramdisk_from_image(
+ image_meta)
+ return kernel_id, ramdisk_id
+
+ @staticmethod
+ def _do_get_kernel_ramdisk_from_image(image_meta):
+ """Given an ImageService image_meta, return kernel and ramdisk image
+ ids if present.
+
+ This is only valid for `ami` style images.
+ """
+ image_id = image_meta['id']
+ if image_meta['status'] != 'active':
+ raise exception.Invalid(
+ _("Cannot build from image %(image_id)s, status not active") %
+ locals())
+
+ if image_meta['properties']['disk_format'] != 'ami':
+ return None, None
+
+ try:
+ kernel_id = image_meta['properties']['kernel_id']
+ except KeyError:
+ raise exception.NotFound(
+ _("Kernel not found for image %(image_id)s") % locals())
+
+ try:
+ ramdisk_id = image_meta['properties']['ramdisk_id']
+ except KeyError:
+ raise exception.NotFound(
+ _("Ramdisk not found for image %(image_id)s") % locals())
+
+ return kernel_id, ramdisk_id
+
+
+class ControllerV10(Controller):
+ def _image_id_from_req_data(self, data):
+ return data['server']['imageId']
+
+ def _flavor_id_from_req_data(self, data):
+ return data['server']['flavorId']
+
+ def _get_view_builder(self, req):
+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV10()
+ return nova.api.openstack.views.servers.ViewBuilderV10(
+ addresses_builder)
+
+ def _get_addresses_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV10(req)
+
+ def _limit_items(self, items, req):
+ return common.limited(items, req)
+
+ def _parse_update(self, context, server_id, inst_dict, update_dict):
+ if 'adminPass' in inst_dict['server']:
+ update_dict['admin_pass'] = inst_dict['server']['adminPass']
+ try:
+ self.compute_api.set_admin_password(context, server_id)
+ except exception.TimeoutException:
+ return exc.HTTPRequestTimeout()
+
+
+class ControllerV11(Controller):
+ def _image_id_from_req_data(self, data):
+ href = data['server']['imageRef']
+ return common.get_id_from_href(href)
+
+ def _flavor_id_from_req_data(self, data):
+ href = data['server']['flavorRef']
+ return common.get_id_from_href(href)
+
+ def _get_view_builder(self, req):
+ base_url = req.application_url
+ flavor_builder = nova.api.openstack.views.flavors.ViewBuilderV11(
+ base_url)
+ image_builder = nova.api.openstack.views.images.ViewBuilderV11(
+ base_url)
+ addresses_builder = nova.api.openstack.views.addresses.ViewBuilderV11()
+ return nova.api.openstack.views.servers.ViewBuilderV11(
+ addresses_builder, flavor_builder, image_builder, base_url)
+
+ def _get_addresses_view_builder(self, req):
+ return nova.api.openstack.views.addresses.ViewBuilderV11(req)
+
+ def _action_change_password(self, input_dict, req, id):
+ context = req.environ['nova.context']
+ if (not 'changePassword' in input_dict
+ or not 'adminPass' in input_dict['changePassword']):
+ msg = _("No adminPass was specified")
+ return exc.HTTPBadRequest(msg)
+ password = input_dict['changePassword']['adminPass']
+ if not isinstance(password, basestring) or password == '':
+ msg = _("Invalid adminPass")
+ return exc.HTTPBadRequest(msg)
+ self.compute_api.set_admin_password(context, id, password)
+ return exc.HTTPAccepted()
+
+ def _limit_items(self, items, req):
+ return common.limited_by_marker(items, req)
+
+
+class ServerCreateRequestXMLDeserializer(object):
+ """
+ Deserializer to handle xml-formatted server create requests.
+
+ Handles standard server attributes as well as optional metadata
+ and personality attributes
+ """
+
+ def deserialize(self, string):
+ """Deserialize an xml-formatted server create request"""
+ dom = minidom.parseString(string)
+ server = self._extract_server(dom)
+ return {'server': server}
+
+ def _extract_server(self, node):
+ """Marshal the server attribute of a parsed request"""
+ server = {}
+ server_node = self._find_first_child_named(node, 'server')
+ for attr in ["name", "imageId", "flavorId"]:
+ server[attr] = server_node.getAttribute(attr)
+ metadata = self._extract_metadata(server_node)
+ if metadata is not None:
+ server["metadata"] = metadata
+ personality = self._extract_personality(server_node)
+ if personality is not None:
+ server["personality"] = personality
+ return server
+
+ def _extract_metadata(self, server_node):
+ """Marshal the metadata attribute of a parsed request"""
+ metadata_node = self._find_first_child_named(server_node, "metadata")
+ if metadata_node is None:
+ return None
+ metadata = {}
+ for meta_node in self._find_children_named(metadata_node, "meta"):
+ key = meta_node.getAttribute("key")
+ metadata[key] = self._extract_text(meta_node)
+ return metadata
+
+ def _extract_personality(self, server_node):
+ """Marshal the personality attribute of a parsed request"""
+ personality_node = \
+ self._find_first_child_named(server_node, "personality")
+ if personality_node is None:
+ return None
+ personality = []
+ for file_node in self._find_children_named(personality_node, "file"):
+ item = {}
+ if file_node.hasAttribute("path"):
+ item["path"] = file_node.getAttribute("path")
+ item["contents"] = self._extract_text(file_node)
+ personality.append(item)
+ return personality
+
+ def _find_first_child_named(self, parent, name):
+ """Search a nodes children for the first child with a given name"""
+ for node in parent.childNodes:
+ if node.nodeName == name:
+ return node
+ return None
+
+ def _find_children_named(self, parent, name):
+ """Return all of a nodes children who have the given name"""
+ for node in parent.childNodes:
+ if node.nodeName == name:
+ yield node
+
+ def _extract_text(self, node):
+ """Get the text field contained by the given node"""
+ if len(node.childNodes) == 1:
+ child = node.childNodes[0]
+ if child.nodeType == child.TEXT_NODE:
+ return child.nodeValue
+ return ""
diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py
index bd3cc23a8..ee7991d7f 100644
--- a/nova/api/openstack/shared_ip_groups.py
+++ b/nova/api/openstack/shared_ip_groups.py
@@ -15,8 +15,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import logging
-
from webob import exc
from nova import wsgi
@@ -44,11 +42,11 @@ class Controller(wsgi.Controller):
def index(self, req):
""" Returns a list of Shared IP Groups for the user """
- return dict(sharedIpGroups=[])
+ raise faults.Fault(exc.HTTPNotImplemented())
def show(self, req, id):
""" Shows in-depth information on a specific Shared IP Group """
- return _translate_keys({})
+ raise faults.Fault(exc.HTTPNotImplemented())
def update(self, req, id):
""" You can't update a Shared IP Group """
@@ -60,7 +58,7 @@ class Controller(wsgi.Controller):
def detail(self, req):
""" Returns a complete list of Shared IP Groups """
- return _translate_detail_keys({})
+ raise faults.Fault(exc.HTTPNotImplemented())
def create(self, req):
""" Creates a new Shared IP group """
diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py
new file mode 100644
index 000000000..d3ab3d553
--- /dev/null
+++ b/nova/api/openstack/users.py
@@ -0,0 +1,104 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from webob import exc
+
+from nova import exception
+from nova import flags
+from nova import log as logging
+from nova import wsgi
+from nova.api.openstack import common
+from nova.api.openstack import faults
+from nova.auth import manager
+
+FLAGS = flags.FLAGS
+LOG = logging.getLogger('nova.api.openstack')
+
+
+def _translate_keys(user):
+ return dict(id=user.id,
+ name=user.name,
+ access=user.access,
+ secret=user.secret,
+ admin=user.admin)
+
+
+class Controller(wsgi.Controller):
+
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "user": ["id", "name", "access", "secret", "admin"]}}}
+
+ def __init__(self):
+ self.manager = manager.AuthManager()
+
+ def _check_admin(self, context):
+ """We cannot depend on the db layer to check for admin access
+ for the auth manager, so we do it here"""
+ if not context.is_admin:
+ raise exception.NotAuthorized(_("Not admin user"))
+
+ def index(self, req):
+ """Return all users in brief"""
+ users = self.manager.get_users()
+ users = common.limited(users, req)
+ users = [_translate_keys(user) for user in users]
+ return dict(users=users)
+
+ def detail(self, req):
+ """Return all users in detail"""
+ return self.index(req)
+
+ def show(self, req, id):
+ """Return data about the given user id"""
+
+ #NOTE(justinsb): The drivers are a little inconsistent in how they
+ # deal with "NotFound" - some throw, some return None.
+ try:
+ user = self.manager.get_user(id)
+ except exception.NotFound:
+ user = None
+
+ if user is None:
+ raise faults.Fault(exc.HTTPNotFound())
+
+ return dict(user=_translate_keys(user))
+
+ def delete(self, req, id):
+ self._check_admin(req.environ['nova.context'])
+ self.manager.delete_user(id)
+ return {}
+
+ def create(self, req):
+ self._check_admin(req.environ['nova.context'])
+ env = self._deserialize(req.body, req.get_content_type())
+ is_admin = env['user'].get('admin') in ('T', 'True', True)
+ name = env['user'].get('name')
+ access = env['user'].get('access')
+ secret = env['user'].get('secret')
+ user = self.manager.create_user(name, access, secret, is_admin)
+ return dict(user=_translate_keys(user))
+
+ def update(self, req, id):
+ self._check_admin(req.environ['nova.context'])
+ env = self._deserialize(req.body, req.get_content_type())
+ is_admin = env['user'].get('admin')
+ if is_admin is not None:
+ is_admin = is_admin in ('T', 'True', True)
+ access = env['user'].get('access')
+ secret = env['user'].get('secret')
+ self.manager.modify_user(id, access, secret, is_admin)
+ return dict(user=_translate_keys(self.manager.get_user(id)))
diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py
new file mode 100644
index 000000000..3f9d91934
--- /dev/null
+++ b/nova/api/openstack/versions.py
@@ -0,0 +1,60 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import webob
+import webob.dec
+
+from nova import wsgi
+import nova.api.openstack.views.versions
+
+
+class Versions(wsgi.Application):
+ @webob.dec.wsgify(RequestClass=wsgi.Request)
+ def __call__(self, req):
+ """Respond to a request for all OpenStack API versions."""
+ version_objs = [
+ {
+ "id": "v1.1",
+ "status": "CURRENT",
+ },
+ {
+ "id": "v1.0",
+ "status": "DEPRECATED",
+ },
+ ]
+
+ builder = nova.api.openstack.views.versions.get_view_builder(req)
+ versions = [builder.build(version) for version in version_objs]
+ response = dict(versions=versions)
+
+ metadata = {
+ "application/xml": {
+ "attributes": {
+ "version": ["status", "id"],
+ "link": ["rel", "href"],
+ }
+ }
+ }
+
+ content_type = req.best_match_content_type()
+ body = wsgi.Serializer(metadata).serialize(response, content_type)
+
+ response = webob.Response()
+ response.content_type = content_type
+ response.body = body
+
+ return response
diff --git a/nova/api/openstack/views/__init__.py b/nova/api/openstack/views/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/nova/api/openstack/views/__init__.py
diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py
new file mode 100644
index 000000000..90c77855b
--- /dev/null
+++ b/nova/api/openstack/views/addresses.py
@@ -0,0 +1,42 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova import utils
+from nova.api.openstack import common
+
+
+class ViewBuilder(object):
+ ''' Models a server addresses response as a python dictionary.'''
+
+ def build(self, inst):
+ raise NotImplementedError()
+
+
+class ViewBuilderV10(ViewBuilder):
+ def build(self, inst):
+ private_ips = utils.get_from_path(inst, 'fixed_ip/address')
+ public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ return dict(public=public_ips, private=private_ips)
+
+
+class ViewBuilderV11(ViewBuilder):
+ def build(self, inst):
+ private_ips = utils.get_from_path(inst, 'fixed_ip/address')
+ private_ips = [dict(version=4, addr=a) for a in private_ips]
+ public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+ public_ips = [dict(version=4, addr=a) for a in public_ips]
+ return dict(public=public_ips, private=private_ips)
diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py
new file mode 100644
index 000000000..462890ab2
--- /dev/null
+++ b/nova/api/openstack/views/flavors.py
@@ -0,0 +1,96 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+from nova.api.openstack import common
+
+
+class ViewBuilder(object):
+
+ def build(self, flavor_obj, is_detail=False):
+ """Generic method used to generate a flavor entity."""
+ if is_detail:
+ flavor = self._build_detail(flavor_obj)
+ else:
+ flavor = self._build_simple(flavor_obj)
+
+ self._build_extra(flavor)
+
+ return flavor
+
+ def _build_simple(self, flavor_obj):
+ """Build a minimal representation of a flavor."""
+ return {
+ "id": flavor_obj["flavorid"],
+ "name": flavor_obj["name"],
+ }
+
+ def _build_detail(self, flavor_obj):
+ """Build a more complete representation of a flavor."""
+ simple = self._build_simple(flavor_obj)
+
+ detail = {
+ "ram": flavor_obj["memory_mb"],
+ "disk": flavor_obj["local_gb"],
+ }
+
+ detail.update(simple)
+
+ return detail
+
+ def _build_extra(self, flavor_obj):
+ """Hook for version-specific changes to newly created flavor object."""
+ pass
+
+
+class ViewBuilderV11(ViewBuilder):
+ """Openstack API v1.1 flavors view builder."""
+
+ def __init__(self, base_url):
+ """
+ :param base_url: url of the root wsgi application
+ """
+ self.base_url = base_url
+
+ def _build_extra(self, flavor_obj):
+ flavor_obj["links"] = self._build_links(flavor_obj)
+
+ def _build_links(self, flavor_obj):
+ """Generate a container of links that refer to the provided flavor."""
+ href = self.generate_href(flavor_obj["id"])
+
+ links = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ },
+ ]
+
+ return links
+
+ def generate_href(self, flavor_id):
+ """Create an url that refers to a specific flavor id."""
+ return "%s/flavors/%s" % (self.base_url, flavor_id)
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
new file mode 100644
index 000000000..16195b050
--- /dev/null
+++ b/nova/api/openstack/views/images.py
@@ -0,0 +1,114 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os.path
+
+
+class ViewBuilder(object):
+ """Base class for generating responses to OpenStack API image requests."""
+
+ def __init__(self, base_url):
+ """Initialize new `ViewBuilder`."""
+ self._url = base_url
+
+ def _format_dates(self, image):
+ """Update all date fields to ensure standardized formatting."""
+ for attr in ['created_at', 'updated_at', 'deleted_at']:
+ if image.get(attr) is not None:
+ image[attr] = image[attr].strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ def _format_status(self, image):
+ """Update the status field to standardize format."""
+ status_mapping = {
+ 'pending': 'queued',
+ 'decrypting': 'preparing',
+ 'untarring': 'saving',
+ 'available': 'active',
+ 'killed': 'failed',
+ }
+
+ try:
+ image['status'] = status_mapping[image['status']].upper()
+ except KeyError:
+ image['status'] = image['status'].upper()
+
+ def generate_href(self, image_id):
+ """Return an href string pointing to this object."""
+ return os.path.join(self._url, "images", str(image_id))
+
+ def build(self, image_obj, detail=False):
+ """Return a standardized image structure for display by the API."""
+ properties = image_obj.get("properties", {})
+
+ self._format_dates(image_obj)
+
+ if "status" in image_obj:
+ self._format_status(image_obj)
+
+ image = {
+ "id": image_obj.get("id"),
+ "name": image_obj.get("name"),
+ }
+
+ if "instance_id" in properties:
+ try:
+ image["serverId"] = int(properties["instance_id"])
+ except ValueError:
+ pass
+
+ if detail:
+ image.update({
+ "created": image_obj.get("created_at"),
+ "updated": image_obj.get("updated_at"),
+ "status": image_obj.get("status"),
+ })
+
+ if image["status"] == "SAVING":
+ image["progress"] = 0
+
+ return image
+
+
+class ViewBuilderV10(ViewBuilder):
+ """OpenStack API v1.0 Image Builder"""
+ pass
+
+
+class ViewBuilderV11(ViewBuilder):
+ """OpenStack API v1.1 Image Builder"""
+
+ def build(self, image_obj, detail=False):
+ """Return a standardized image structure for display by the API."""
+ image = ViewBuilder.build(self, image_obj, detail)
+ href = self.generate_href(image_obj["id"])
+
+ image["links"] = [{
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ }]
+
+ return image
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
new file mode 100644
index 000000000..d24c025be
--- /dev/null
+++ b/nova/api/openstack/views/servers.py
@@ -0,0 +1,168 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import hashlib
+import os
+
+from nova.compute import power_state
+import nova.compute
+import nova.context
+from nova.api.openstack import common
+from nova.api.openstack.views import addresses as addresses_view
+from nova.api.openstack.views import flavors as flavors_view
+from nova.api.openstack.views import images as images_view
+from nova import utils
+
+
+class ViewBuilder(object):
+ """Model a server response as a python dictionary.
+
+ Public methods: build
+ Abstract methods: _build_image, _build_flavor
+
+ """
+
+ def __init__(self, addresses_builder):
+ self.addresses_builder = addresses_builder
+
+ def build(self, inst, is_detail):
+ """Return a dict that represenst a server."""
+ if is_detail:
+ server = self._build_detail(inst)
+ else:
+ server = self._build_simple(inst)
+
+ self._build_extra(server, inst)
+
+ return server
+
+ def _build_simple(self, inst):
+ """Return a simple model of a server."""
+ return dict(server=dict(id=inst['id'], name=inst['display_name']))
+
+ def _build_detail(self, inst):
+ """Returns a detailed model of a server."""
+ power_mapping = {
+ None: 'BUILD',
+ power_state.NOSTATE: 'BUILD',
+ power_state.RUNNING: 'ACTIVE',
+ power_state.BLOCKED: 'ACTIVE',
+ power_state.SUSPENDED: 'SUSPENDED',
+ power_state.PAUSED: 'PAUSED',
+ power_state.SHUTDOWN: 'ACTIVE',
+ power_state.SHUTOFF: 'ACTIVE',
+ power_state.CRASHED: 'ERROR',
+ power_state.FAILED: 'ERROR'}
+
+ inst_dict = {
+ 'id': int(inst['id']),
+ 'name': inst['display_name'],
+ 'addresses': self.addresses_builder.build(inst),
+ 'status': power_mapping[inst.get('state')]}
+
+ ctxt = nova.context.get_admin_context()
+ compute_api = nova.compute.API()
+ if compute_api.has_finished_migration(ctxt, inst['id']):
+ inst_dict['status'] = 'RESIZE-CONFIRM'
+
+ # Return the metadata as a dictionary
+ metadata = {}
+ for item in inst.get('metadata', []):
+ metadata[item['key']] = item['value']
+ inst_dict['metadata'] = metadata
+
+ inst_dict['hostId'] = ''
+ if inst.get('host'):
+ inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest()
+
+ self._build_image(inst_dict, inst)
+ self._build_flavor(inst_dict, inst)
+
+ return dict(server=inst_dict)
+
+ def _build_image(self, response, inst):
+ """Return the image sub-resource of a server."""
+ raise NotImplementedError()
+
+ def _build_flavor(self, response, inst):
+ """Return the flavor sub-resource of a server."""
+ raise NotImplementedError()
+
+ def _build_extra(self, response, inst):
+ pass
+
+
+class ViewBuilderV10(ViewBuilder):
+ """Model an Openstack API V1.0 server response."""
+
+ def _build_image(self, response, inst):
+ if 'image_id' in dict(inst):
+ response['imageId'] = inst['image_id']
+
+ def _build_flavor(self, response, inst):
+ if 'instance_type' in dict(inst):
+ response['flavorId'] = inst['instance_type']
+
+
+class ViewBuilderV11(ViewBuilder):
+ """Model an Openstack API V1.0 server response."""
+ def __init__(self, addresses_builder, flavor_builder, image_builder,
+ base_url):
+ ViewBuilder.__init__(self, addresses_builder)
+ self.flavor_builder = flavor_builder
+ self.image_builder = image_builder
+ self.base_url = base_url
+
+ def _build_image(self, response, inst):
+ if "image_id" in dict(inst):
+ image_id = inst.get("image_id")
+ response["imageRef"] = self.image_builder.generate_href(image_id)
+
+ def _build_flavor(self, response, inst):
+ if "instance_type" in dict(inst):
+ flavor_id = inst["instance_type"]
+ flavor_ref = self.flavor_builder.generate_href(flavor_id)
+ response["flavorRef"] = flavor_ref
+
+ def _build_extra(self, response, inst):
+ self._build_links(response, inst)
+
+ def _build_links(self, response, inst):
+ href = self.generate_href(inst["id"])
+
+ links = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/json",
+ "href": href,
+ },
+ {
+ "rel": "bookmark",
+ "type": "application/xml",
+ "href": href,
+ },
+ ]
+
+ response["server"]["links"] = links
+
+ def generate_href(self, server_id):
+ """Create an url that refers to a specific server id."""
+ return os.path.join(self.base_url, "servers", str(server_id))
diff --git a/nova/api/openstack/views/versions.py b/nova/api/openstack/views/versions.py
new file mode 100644
index 000000000..d0145c94a
--- /dev/null
+++ b/nova/api/openstack/views/versions.py
@@ -0,0 +1,59 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010-2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import os
+
+
+def get_view_builder(req):
+ base_url = req.application_url
+ return ViewBuilder(base_url)
+
+
+class ViewBuilder(object):
+
+ def __init__(self, base_url):
+ """
+ :param base_url: url of the root wsgi application
+ """
+ self.base_url = base_url
+
+ def build(self, version_data):
+ """Generic method used to generate a version entity."""
+ version = {
+ "id": version_data["id"],
+ "status": version_data["status"],
+ "links": self._build_links(version_data),
+ }
+
+ return version
+
+ def _build_links(self, version_data):
+ """Generate a container of links that refer to the provided version."""
+ href = self.generate_href(version_data["id"])
+
+ links = [
+ {
+ "rel": "self",
+ "href": href,
+ },
+ ]
+
+ return links
+
+ def generate_href(self, version_number):
+ """Create an url that refers to a specific version_number."""
+ return os.path.join(self.base_url, version_number)
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
new file mode 100644
index 000000000..846cb48a1
--- /dev/null
+++ b/nova/api/openstack/zones.py
@@ -0,0 +1,101 @@
+# Copyright 2011 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+import common
+
+from nova import db
+from nova import flags
+from nova import log as logging
+from nova import wsgi
+from nova.scheduler import api
+
+
+FLAGS = flags.FLAGS
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
+def _exclude_keys(item, keys):
+ return dict((k, v) for k, v in item.iteritems() if k not in keys)
+
+
+def _scrub_zone(zone):
+ return _exclude_keys(zone, ('username', 'password', 'created_at',
+ 'deleted', 'deleted_at', 'updated_at'))
+
+
+class Controller(wsgi.Controller):
+
+ _serialization_metadata = {
+ 'application/xml': {
+ "attributes": {
+ "zone": ["id", "api_url", "name", "capabilities"]}}}
+
+ def index(self, req):
+ """Return all zones in brief"""
+ # Ask the ZoneManager in the Scheduler for most recent data,
+ # or fall-back to the database ...
+ items = api.get_zone_list(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_scrub_zone(item) for item in items]
+ return dict(zones=items)
+
+ def detail(self, req):
+ """Return all zones in detail"""
+ return self.index(req)
+
+ def info(self, req):
+ """Return name and capabilities for this zone."""
+ items = api.get_zone_capabilities(req.environ['nova.context'])
+
+ zone = dict(name=FLAGS.zone_name)
+ caps = FLAGS.zone_capabilities
+ for cap in caps:
+ key, value = cap.split('=')
+ zone[key] = value
+ for item, (min_value, max_value) in items.iteritems():
+ zone[item] = "%s,%s" % (min_value, max_value)
+ return dict(zone=zone)
+
+ def show(self, req, id):
+ """Return data about the given zone id"""
+ zone_id = int(id)
+ zone = api.zone_get(req.environ['nova.context'], zone_id)
+ return dict(zone=_scrub_zone(zone))
+
+ def delete(self, req, id):
+ zone_id = int(id)
+ api.zone_delete(req.environ['nova.context'], zone_id)
+ return {}
+
+ def create(self, req):
+ context = req.environ['nova.context']
+ env = self._deserialize(req.body, req.get_content_type())
+ zone = api.zone_create(context, env["zone"])
+ return dict(zone=_scrub_zone(zone))
+
+ def update(self, req, id):
+ context = req.environ['nova.context']
+ env = self._deserialize(req.body, req.get_content_type())
+ zone_id = int(id)
+ zone = api.zone_update(context, zone_id, env["zone"])
+ return dict(zone=_scrub_zone(zone))