summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorCerberus <matt.dietz@rackspace.com>2011-04-22 10:39:35 -0500
committerCerberus <matt.dietz@rackspace.com>2011-04-22 10:39:35 -0500
commitc03e9805328afe1d03fa65ac93d2b91ba04c229e (patch)
tree7234fe551b9f6e32b80e91753dbf91a2181d4b9a /nova/api
parent14718afef1cc79b4d41f490be677caf3e4191e2b (diff)
parent8af2a2d720b97ef17565d57a9b8b028d449a9c84 (diff)
Merge from trunk
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/__init__.py2
-rw-r--r--nova/api/direct.py96
-rw-r--r--nova/api/ec2/admin.py2
-rw-r--r--nova/api/ec2/apirequest.py2
-rw-r--r--nova/api/ec2/cloud.py127
-rw-r--r--nova/api/openstack/__init__.py37
-rw-r--r--nova/api/openstack/accounts.py5
-rw-r--r--nova/api/openstack/auth.py20
-rw-r--r--nova/api/openstack/backup_schedules.py10
-rw-r--r--nova/api/openstack/common.py27
-rw-r--r--nova/api/openstack/consoles.py4
-rw-r--r--nova/api/openstack/contrib/__init__.py22
-rw-r--r--nova/api/openstack/contrib/volumes.py335
-rw-r--r--nova/api/openstack/extensions.py227
-rw-r--r--nova/api/openstack/faults.py8
-rw-r--r--nova/api/openstack/flavors.py7
-rw-r--r--nova/api/openstack/image_metadata.py15
-rw-r--r--nova/api/openstack/images.py315
-rw-r--r--nova/api/openstack/ips.py72
-rw-r--r--nova/api/openstack/limits.py4
-rw-r--r--nova/api/openstack/server_metadata.py32
-rw-r--r--nova/api/openstack/servers.py168
-rw-r--r--nova/api/openstack/shared_ip_groups.py10
-rw-r--r--nova/api/openstack/users.py3
-rw-r--r--nova/api/openstack/versions.py10
-rw-r--r--nova/api/openstack/views/addresses.py10
-rw-r--r--nova/api/openstack/views/images.py98
-rw-r--r--nova/api/openstack/views/servers.py28
-rw-r--r--nova/api/openstack/zones.py6
29 files changed, 1250 insertions, 452 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py
index 0fedbbfad..747015af5 100644
--- a/nova/api/__init__.py
+++ b/nova/api/__init__.py
@@ -15,5 +15,3 @@
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
-
-"""No-op __init__ for directory full of api goodies."""
diff --git a/nova/api/direct.py b/nova/api/direct.py
index e5f33cee4..8ceae299c 100644
--- a/nova/api/direct.py
+++ b/nova/api/direct.py
@@ -44,14 +44,33 @@ from nova import utils
from nova import wsgi
+# Global storage for registering modules.
ROUTES = {}
def register_service(path, handle):
+ """Register a service handle at a given path.
+
+ Services registered in this way will be made available to any instances of
+ nova.api.direct.Router.
+
+ :param path: `routes` path, can be a basic string like "/path"
+ :param handle: an object whose methods will be made available via the api
+
+ """
ROUTES[path] = handle
class Router(wsgi.Router):
+ """A simple WSGI router configured via `register_service`.
+
+ This is a quick way to attach multiple services to a given endpoint.
+ It will automatically load the routes registered in the `ROUTES` global.
+
+ TODO(termie): provide a paste-deploy version of this.
+
+ """
+
def __init__(self, mapper=None):
if mapper is None:
mapper = routes.Mapper()
@@ -66,6 +85,24 @@ class Router(wsgi.Router):
class DelegatedAuthMiddleware(wsgi.Middleware):
+ """A simple and naive authentication middleware.
+
+ Designed mostly to provide basic support for alternative authentication
+ schemes, this middleware only desires the identity of the user and will
+ generate the appropriate nova.context.RequestContext for the rest of the
+ application. This allows any middleware above it in the stack to
+ authenticate however it would like while only needing to conform to a
+ minimal interface.
+
+ Expects two headers to determine identity:
+ - X-OpenStack-User
+ - X-OpenStack-Project
+
+ This middleware is tied to identity management and will need to be kept
+ in sync with any changes to the way identity is dealt with internally.
+
+ """
+
def process_request(self, request):
os_user = request.headers['X-OpenStack-User']
os_project = request.headers['X-OpenStack-Project']
@@ -74,6 +111,20 @@ class DelegatedAuthMiddleware(wsgi.Middleware):
class JsonParamsMiddleware(wsgi.Middleware):
+ """Middleware to allow method arguments to be passed as serialized JSON.
+
+ Accepting arguments as JSON is useful for accepting data that may be more
+ complex than simple primitives.
+
+ In this case we accept it as urlencoded data under the key 'json' as in
+ json=<urlencoded_json> but this could be extended to accept raw JSON
+ in the POST body.
+
+ Filters out the parameters `self`, `context` and anything beginning with
+ an underscore.
+
+ """
+
def process_request(self, request):
if 'json' not in request.params:
return
@@ -92,6 +143,13 @@ class JsonParamsMiddleware(wsgi.Middleware):
class PostParamsMiddleware(wsgi.Middleware):
+ """Middleware to allow method arguments to be passed as POST parameters.
+
+ Filters out the parameters `self`, `context` and anything beginning with
+ an underscore.
+
+ """
+
def process_request(self, request):
params_parsed = request.params
params = {}
@@ -106,12 +164,21 @@ class PostParamsMiddleware(wsgi.Middleware):
class Reflection(object):
- """Reflection methods to list available methods."""
+ """Reflection methods to list available methods.
+
+ This is an object that expects to be registered via register_service.
+ These methods allow the endpoint to be self-describing. They introspect
+ the exposed methods and provide call signatures and documentation for
+ them allowing quick experimentation.
+
+ """
+
def __init__(self):
self._methods = {}
self._controllers = {}
def _gather_methods(self):
+ """Introspect available methods and generate documentation for them."""
methods = {}
controllers = {}
for route, handler in ROUTES.iteritems():
@@ -185,6 +252,16 @@ class Reflection(object):
class ServiceWrapper(wsgi.Controller):
+ """Wrapper to dynamically povide a WSGI controller for arbitrary objects.
+
+ With lightweight introspection allows public methods on the object to
+ be accesed via simple WSGI routing and parameters and serializes the
+ return values.
+
+ Automatically used be nova.api.direct.Router to wrap registered instances.
+
+ """
+
def __init__(self, service_handle):
self.service_handle = service_handle
@@ -206,10 +283,14 @@ 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 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())
+ content_type = req.best_match_content_type()
+ default_xmlns = self.get_default_xmlns(req)
+ return self._serialize(result, content_type, default_xmlns)
except:
raise exception.Error("returned non-serializable type: %s"
% result)
@@ -256,7 +337,16 @@ class Limited(object):
class Proxy(object):
- """Pretend a Direct API endpoint is an object."""
+ """Pretend a Direct API endpoint is an object.
+
+ This is mostly useful in testing at the moment though it should be easily
+ extendable to provide a basic API library functionality.
+
+ In testing we use this to stub out internal objects to verify that results
+ from the API are serializable.
+
+ """
+
def __init__(self, app, prefix=None):
self.app = app
self.prefix = prefix
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index 6a5609d4a..ea94d9c1f 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -266,7 +266,7 @@ class AdminController(object):
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
+ if (instance['image_id'] == str(FLAGS.vpn_image_id)
and not instance['state_description'] in
['shutting_down', 'shutdown']):
return instance
diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py
index d7ad08d2f..6672e60bb 100644
--- a/nova/api/ec2/apirequest.py
+++ b/nova/api/ec2/apirequest.py
@@ -196,7 +196,7 @@ class APIRequest(object):
elif isinstance(data, datetime.datetime):
data_el.appendChild(
xml.createTextNode(_database_to_isoformat(data)))
- elif data != None:
+ elif data is not None:
data_el.appendChild(xml.createTextNode(str(data)))
return data_el
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index 0da642318..9f4c0c05e 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -103,10 +103,18 @@ class CloudController(object):
# Gen root CA, if we don't have one
root_ca_path = os.path.join(FLAGS.ca_path, FLAGS.ca_file)
if not os.path.exists(root_ca_path):
+ genrootca_sh_path = os.path.join(os.path.dirname(__file__),
+ os.path.pardir,
+ os.path.pardir,
+ 'CA',
+ 'genrootca.sh')
+
start = os.getcwd()
+ if not os.path.exists(FLAGS.ca_path):
+ os.makedirs(FLAGS.ca_path)
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_path)
os.chdir(start)
def _get_mpi_data(self, context, project_id):
@@ -134,6 +142,11 @@ class CloudController(object):
instance_ref = self.compute_api.get_all(ctxt, fixed_ip=address)
if instance_ref is None:
return None
+
+ # This ensures that all attributes of the instance
+ # are populated.
+ instance_ref = db.instance_get(ctxt, instance_ref['id'])
+
mpi = self._get_mpi_data(ctxt, instance_ref['project_id'])
if instance_ref['key_name']:
keys = {'0': {'_name': instance_ref['key_name'],
@@ -146,7 +159,7 @@ class CloudController(object):
floating_ip = db.instance_get_floating_address(ctxt,
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')
+ image_ec2_id = self.image_ec2_id(instance_ref['image_id'])
data = {
'user-data': base64.b64decode(instance_ref['user_data']),
'meta-data': {
@@ -174,9 +187,9 @@ class CloudController(object):
'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)
+ if instance_ref.get('%s_id' % image_type):
+ ec2_id = self.image_ec2_id(instance_ref['%s_id' % image_type],
+ self._image_type(image_type))
data['meta-data']['%s-id' % image_type] = ec2_id
if False: # TODO(vish): store ancestor ids
@@ -429,7 +442,7 @@ class CloudController(object):
group_name)
criteria = self._revoke_rule_args_to_dict(context, **kwargs)
- if criteria == None:
+ if criteria is None:
raise exception.ApiError(_("Not enough parameters to build a "
"valid rule."))
@@ -536,6 +549,13 @@ class CloudController(object):
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 = []
@@ -593,7 +613,7 @@ class CloudController(object):
# 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))]}
+ return self._format_volume(context, dict(volume))
def delete_volume(self, context, volume_id, **kwargs):
volume_id = ec2utils.ec2_id_to_id(volume_id)
@@ -644,7 +664,7 @@ class CloudController(object):
'volumeId': ec2utils.id_to_ec2_id(volume_id, 'vol-%08x')}
def _convert_to_set(self, lst, label):
- if lst == None or lst == []:
+ if lst is None or lst == []:
return None
if not isinstance(lst, list):
lst = [lst]
@@ -683,13 +703,13 @@ class CloudController(object):
instances = self.compute_api.get_all(context, **kwargs)
for instance in instances:
if not context.is_admin:
- if instance['image_id'] == FLAGS.vpn_image_id:
+ if instance['image_id'] == str(FLAGS.vpn_image_id):
continue
i = {}
instance_id = instance['id']
ec2_id = ec2utils.id_to_ec2_id(instance_id)
i['instanceId'] = ec2_id
- i['imageId'] = self._image_ec2_id(instance['image_id'])
+ i['imageId'] = self.image_ec2_id(instance['image_id'])
i['instanceState'] = {
'code': instance['state'],
'name': instance['state_description']}
@@ -706,7 +726,9 @@ class CloudController(object):
instance['mac_address'])
i['privateDnsName'] = fixed_addr
+ i['privateIpAddress'] = fixed_addr
i['publicDnsName'] = floating_addr
+ i['ipAddress'] = floating_addr or fixed_addr
i['dnsName'] = i['publicDnsName'] or i['privateDnsName']
i['keyName'] = instance['key_name']
@@ -715,7 +737,10 @@ class CloudController(object):
instance['project_id'],
instance['host'])
i['productCodesSet'] = self._convert_to_set([], 'product_codes')
- i['instanceType'] = instance['instance_type']
+ if instance['instance_type']:
+ i['instanceType'] = instance['instance_type'].get('name')
+ else:
+ i['instanceType'] = None
i['launchTime'] = instance['created_at']
i['amiLaunchIndex'] = instance['launch_index']
i['displayName'] = instance['display_name']
@@ -750,6 +775,8 @@ class CloudController(object):
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']
@@ -768,7 +795,7 @@ class CloudController(object):
def allocate_address(self, context, **kwargs):
LOG.audit(_("Allocate address"), context=context)
public_ip = self.network_api.allocate_floating_ip(context)
- return {'addressSet': [{'publicIp': public_ip}]}
+ return {'publicIp': public_ip}
def release_address(self, context, public_ip, **kwargs):
LOG.audit(_("Release address %s"), public_ip, context=context)
@@ -798,7 +825,7 @@ class CloudController(object):
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(
+ instance_type=instance_types.get_instance_type_by_name(
kwargs.get('instance_type', None)),
image_id=self._get_image(context, kwargs['image_id'])['id'],
min_count=int(kwargs.get('min_count', max_count)),
@@ -855,13 +882,27 @@ class CloudController(object):
self.compute_api.update(context, instance_id=instance_id, **kwargs)
return True
- _type_prefix_map = {'machine': 'ami',
- 'kernel': 'aki',
- 'ramdisk': 'ari'}
+ @staticmethod
+ def _image_type(image_type):
+ """Converts to a three letter image type.
- def _image_ec2_id(self, image_id, image_type='machine'):
- prefix = self._type_prefix_map[image_type]
- template = prefix + '-%08x'
+ aki, kernel => aki
+ ari, ramdisk => ari
+ anything else => ami
+
+ """
+ if image_type == 'kernel':
+ return 'aki'
+ if image_type == 'ramdisk':
+ return 'ari'
+ if image_type not in ['aki', 'ari']:
+ return 'ami'
+ return image_type
+
+ @staticmethod
+ def image_ec2_id(image_id, image_type='ami'):
+ """Returns image ec2_id using id and three letter type."""
+ template = image_type + '-%08x'
return ec2utils.id_to_ec2_id(int(image_id), template=template)
def _get_image(self, context, ec2_id):
@@ -869,29 +910,42 @@ class CloudController(object):
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)
+ try:
+ return self.image_service.show_by_name(context, ec2_id)
+ except exception.NotFound:
+ raise exception.NotFound(_('Image %s not found') % ec2_id)
def _format_image(self, image):
"""Convert from format defined by BaseImageService to S3 format."""
i = {}
- image_type = image['properties'].get('type')
- ec2_id = self._image_ec2_id(image.get('id'), image_type)
+ image_type = self._image_type(image.get('container_format'))
+ 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
+ i['imageId'] = ec2_id
kernel_id = image['properties'].get('kernel_id')
if kernel_id:
- i['kernelId'] = self._image_ec2_id(kernel_id, 'kernel')
+ i['kernelId'] = self.image_ec2_id(kernel_id, 'aki')
ramdisk_id = image['properties'].get('ramdisk_id')
if ramdisk_id:
- i['ramdiskId'] = self._image_ec2_id(ramdisk_id, 'ramdisk')
+ i['ramdiskId'] = self.image_ec2_id(ramdisk_id, 'ari')
i['imageOwnerId'] = image['properties'].get('owner_id')
- i['imageLocation'] = image['properties'].get('image_location')
- i['imageState'] = image['properties'].get('image_state')
- i['type'] = image_type
- i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True'
+ if name:
+ i['imageLocation'] = "%s (%s)" % (image['properties'].
+ get('image_location'), name)
+ else:
+ i['imageLocation'] = image['properties'].get('image_location')
+ # NOTE(vish): fallback status if image_state isn't set
+ state = image.get('status')
+ if state == 'active':
+ state = 'available'
+ i['imageState'] = image['properties'].get('image_state', state)
+ i['displayName'] = name
+ i['description'] = image.get('description')
+ display_mapping = {'aki': 'kernel',
+ 'ari': 'ramdisk',
+ 'ami': 'machine'}
+ i['imageType'] = display_mapping.get(image_type)
+ i['isPublic'] = image.get('is_public') == True
i['architecture'] = image['properties'].get('architecture')
return i
@@ -923,8 +977,9 @@ class CloudController(object):
image_location = kwargs['name']
metadata = {'properties': {'image_location': image_location}}
image = self.image_service.create(context, metadata)
- image_id = self._image_ec2_id(image['id'],
- image['properties']['type'])
+ image_type = self._image_type(image.get('container_format'))
+ image_id = self.image_ec2_id(image['id'],
+ image_type)
msg = _("Registered image %(image_location)s with"
" id %(image_id)s") % locals()
LOG.audit(msg, context=context)
@@ -939,7 +994,7 @@ class CloudController(object):
except exception.NotFound:
raise exception.NotFound(_('Image %s not found') % image_id)
result = {'imageId': image_id, 'launchPermission': []}
- if image['properties']['is_public']:
+ if image['is_public']:
result['launchPermission'].append({'group': 'all'})
return result
@@ -964,7 +1019,7 @@ class CloudController(object):
internal_id = image['id']
del(image['id'])
- image['properties']['is_public'] = (operation_type == 'add')
+ image['is_public'] = (operation_type == 'add')
return self.image_service.update(context, internal_id, image)
def update_image(self, context, image_id, **kwargs):
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index 8fabbce8e..5e76a06f7 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -34,6 +34,7 @@ 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 ips
from nova.api.openstack import limits
from nova.api.openstack import servers
from nova.api.openstack import server_metadata
@@ -106,26 +107,16 @@ class APIRouter(wsgi.Router):
controller=accounts.Controller(),
collection={'detail': 'GET'})
- mapper.resource("backup_schedule", "backup_schedule",
- controller=backup_schedules.Controller(),
- parent_resource=dict(member_name='server',
- collection_name='servers'))
-
mapper.resource("console", "consoles",
controller=consoles.Controller(),
parent_resource=dict(member_name='server',
collection_name='servers'))
- mapper.resource("image", "images", controller=images.Controller(),
- collection={'detail': 'GET'})
-
- mapper.resource("shared_ip_group", "shared_ip_groups",
- collection={'detail': 'GET'},
- controller=shared_ip_groups.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."""
@@ -137,10 +128,28 @@ class APIRouterV10(APIRouter):
collection={'detail': 'GET'},
member=self.server_members)
+ mapper.resource("image", "images",
+ controller=images.ControllerV10(),
+ collection={'detail': 'GET'})
+
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())
+
+ mapper.resource("backup_schedule", "backup_schedule",
+ controller=backup_schedules.Controller(),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
+ mapper.resource("ip", "ips", controller=ips.Controller(),
+ collection=dict(public='GET', private='GET'),
+ parent_resource=dict(member_name='server',
+ collection_name='servers'))
+
class APIRouterV11(APIRouter):
"""Define routes specific to OpenStack API V1.1."""
@@ -152,6 +161,10 @@ class APIRouterV11(APIRouter):
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',
diff --git a/nova/api/openstack/accounts.py b/nova/api/openstack/accounts.py
index 86066fa20..6e3763e47 100644
--- a/nova/api/openstack/accounts.py
+++ b/nova/api/openstack/accounts.py
@@ -13,15 +13,14 @@
# 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 common
from nova.api.openstack import faults
FLAGS = flags.FLAGS
@@ -35,7 +34,7 @@ def _translate_keys(account):
manager=account.project_manager_id)
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
_serialization_metadata = {
'application/xml': {
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index f3a9bdeca..311e6bde9 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -55,6 +55,9 @@ class AuthMiddleware(wsgi.Middleware):
user = self.get_user_by_authentication(req)
accounts = self.auth.get_projects(user=user)
if not user:
+ token = req.headers["X-Auth-Token"]
+ msg = _("%(user)s could not be found with token '%(token)s'")
+ LOG.warn(msg % locals())
return faults.Fault(webob.exc.HTTPUnauthorized())
if accounts:
@@ -66,6 +69,8 @@ class AuthMiddleware(wsgi.Middleware):
if not self.auth.is_admin(user) and \
not self.auth.is_project_member(user, account):
+ msg = _("%(user)s must be an admin or a member of %(account)s")
+ LOG.warn(msg % locals())
return faults.Fault(webob.exc.HTTPUnauthorized())
req.environ['nova.context'] = context.RequestContext(user, account)
@@ -82,12 +87,16 @@ class AuthMiddleware(wsgi.Middleware):
# honor it
path_info = req.path_info
if len(path_info) > 1:
- return faults.Fault(webob.exc.HTTPUnauthorized())
+ msg = _("Authentication requests must be made against a version "
+ "root (e.g. /v1.0 or /v1.1).")
+ LOG.warn(msg)
+ return faults.Fault(webob.exc.HTTPUnauthorized(explanation=msg))
try:
username = req.headers['X-Auth-User']
key = req.headers['X-Auth-Key']
- except KeyError:
+ except KeyError as ex:
+ LOG.warn(_("Could not find %s in request.") % ex)
return faults.Fault(webob.exc.HTTPUnauthorized())
token, user = self._authorize_user(username, key, req)
@@ -100,6 +109,7 @@ class AuthMiddleware(wsgi.Middleware):
res.headers['X-CDN-Management-Url'] = token.cdn_management_url
res.content_type = 'text/plain'
res.status = '204'
+ LOG.debug(_("Successfully authenticated '%s'") % username)
return res
else:
return faults.Fault(webob.exc.HTTPUnauthorized())
@@ -139,6 +149,7 @@ class AuthMiddleware(wsgi.Middleware):
try:
user = self.auth.get_user_from_access_key(key)
except exception.NotFound:
+ LOG.warn(_("User not found with provided API key."))
user = None
if user and user.name == username:
@@ -153,4 +164,9 @@ class AuthMiddleware(wsgi.Middleware):
token_dict['user_id'] = user.id
token = self.db.auth_token_create(ctxt, token_dict)
return token, user
+ elif user and user.name != username:
+ msg = _("Provided API key is valid, but not for user "
+ "'%(username)s'") % locals()
+ LOG.warn(msg)
+
return None, None
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 7abb5f884..4bf744046 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -19,7 +19,7 @@ import time
from webob import exc
-from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
import nova.image.service
@@ -29,7 +29,7 @@ def _translate_keys(inst):
return dict(backupSchedule=inst)
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
""" The backup schedule API controller for the Openstack API """
_serialization_metadata = {
@@ -42,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 8cad1273a..0b6dc944a 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -21,10 +21,20 @@ import webob
from nova import exception
from nova import flags
+from nova import log as logging
+from nova import wsgi
+
+
+LOG = logging.getLogger('nova.api.openstack.common')
+
FLAGS = flags.FLAGS
+XML_NS_V10 = 'http://docs.rackspacecloud.com/servers/api/v1.0'
+XML_NS_V11 = 'http://docs.openstack.org/compute/api/v1.1'
+
+
def limited(items, request, max_limit=FLAGS.osapi_max_limit):
"""
Return a slice of items according to requested offset and limit.
@@ -106,8 +116,14 @@ def get_image_id_from_image_hash(image_service, context, image_hash):
items = image_service.index(context)
for image in items:
image_id = image['id']
- if abs(hash(image_id)) == int(image_hash):
- return image_id
+ try:
+ if abs(hash(image_id)) == int(image_hash):
+ return image_id
+ except ValueError:
+ msg = _("Requested image_id has wrong format: %s,"
+ "should have numerical format") % image_id
+ LOG.error(msg)
+ raise Exception(msg)
raise exception.NotFound(image_hash)
@@ -121,4 +137,11 @@ def get_id_from_href(href):
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'))
+
+
+class OpenstackController(wsgi.Controller):
+ def get_default_xmlns(self, req):
+ # Use V10 by default
+ return XML_NS_V10
diff --git a/nova/api/openstack/consoles.py b/nova/api/openstack/consoles.py
index 8c291c2eb..1a77f25d7 100644
--- a/nova/api/openstack/consoles.py
+++ b/nova/api/openstack/consoles.py
@@ -19,7 +19,7 @@ from webob import exc
from nova import console
from nova import exception
-from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
@@ -43,7 +43,7 @@ def _translate_detail_keys(cons):
return dict(console=info)
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
"""The Consoles Controller for the Openstack API"""
_serialization_metadata = {
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..18de2ec71
--- /dev/null
+++ b/nova/api/openstack/contrib/volumes.py
@@ -0,0 +1,335 @@
+# 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
index 9d98d849a..7ea7afef6 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -1,6 +1,7 @@
# 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
@@ -16,15 +17,18 @@
# 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 common
from nova.api.openstack import faults
@@ -34,7 +38,85 @@ LOG = logging.getLogger('extensions')
FLAGS = flags.FLAGS
-class ActionExtensionController(wsgi.Controller):
+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(common.OpenstackController):
def __init__(self, application):
@@ -55,7 +137,7 @@ class ActionExtensionController(wsgi.Controller):
return res
-class ResponseExtensionController(wsgi.Controller):
+class ResponseExtensionController(common.OpenstackController):
def __init__(self, application):
self.application = application
@@ -74,7 +156,8 @@ class ResponseExtensionController(wsgi.Controller):
body = res.body
headers = res.headers
except AttributeError:
- body = self._serialize(res, content_type)
+ default_xmlns = None
+ body = self._serialize(res, content_type, default_xmlns)
headers = {"Content-Type": content_type}
res = webob.Response()
res.body = body
@@ -82,7 +165,7 @@ class ResponseExtensionController(wsgi.Controller):
return res
-class ExtensionController(wsgi.Controller):
+class ExtensionController(common.OpenstackController):
def __init__(self, extension_manager):
self.extension_manager = extension_manager
@@ -94,45 +177,38 @@ class ExtensionController(wsgi.Controller):
ext_data['description'] = ext.get_description()
ext_data['namespace'] = ext.get_namespace()
ext_data['updated'] = ext.get_updated()
- ext_data['links'] = [] # TODO: implement extension links
+ 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():
+ for _alias, ext in self.extension_manager.extensions.iteritems():
extensions.append(self._translate(ext))
return dict(extensions=extensions)
def show(self, req, id):
- # NOTE: the extensions alias is used as the 'id' for show
+ # 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(exc.HTTPNotFound())
+ raise faults.Fault(webob.exc.HTTPNotFound())
def create(self, req):
- raise faults.Fault(exc.HTTPNotFound())
-
- def delete(self, req, id):
- raise faults.Fault(exc.HTTPNotFound())
+ raise faults.Fault(webob.exc.HTTPNotFound())
class ExtensionMiddleware(wsgi.Middleware):
- """
- Extensions middleware that intercepts configured routes for extensions.
- """
+ """Extensions middleware for WSGI."""
@classmethod
def factory(cls, global_config, **local_config):
- """ paste factory """
+ """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 objects by collection
- """
+ """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():
@@ -151,9 +227,7 @@ class ExtensionMiddleware(wsgi.Middleware):
return action_controllers
def _response_ext_controllers(self, application, ext_mgr, mapper):
- """
- Return a dict of ResponseExtensionController objects by collection
- """
+ """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():
@@ -212,18 +286,18 @@ class ExtensionMiddleware(wsgi.Middleware):
@webob.dec.wsgify(RequestClass=wsgi.Request)
def __call__(self, req):
- """
- Route the incoming request with router.
- """
+ """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:
@@ -233,10 +307,11 @@ class ExtensionMiddleware(wsgi.Middleware):
class ExtensionManager(object):
- """
- Load extensions from the configured extension path.
- See nova/tests/api/openstack/extensions/foxinsocks.py for an example
- extension implementation.
+ """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):
@@ -244,12 +319,10 @@ class ExtensionManager(object):
self.path = path
self.extensions = {}
- self._load_extensions()
+ self._load_all_extensions()
def get_resources(self):
- """
- returns a list of ResourceExtension objects
- """
+ """Returns a list of ResourceExtension objects."""
resources = []
resources.append(ResourceExtension('extensions',
ExtensionController(self)))
@@ -257,40 +330,37 @@ class ExtensionManager(object):
try:
resources.extend(ext.get_resources())
except AttributeError:
- # NOTE: Extension aren't required to have resource extensions
+ # NOTE(dprince): Extension aren't required to have resource
+ # extensions
pass
return resources
def get_actions(self):
- """
- returns a list of ActionExtension objects
- """
+ """Returns a list of ActionExtension objects."""
actions = []
for alias, ext in self.extensions.iteritems():
try:
actions.extend(ext.get_actions())
except AttributeError:
- # NOTE: Extension aren't required to have action extensions
+ # NOTE(dprince): Extension aren't required to have action
+ # extensions
pass
return actions
def get_response_extensions(self):
- """
- returns a list of ResponseExtension objects
- """
+ """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: Extension aren't required to have response extensions
+ # 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.
- """
+ """Checks for required methods in extension objects."""
try:
LOG.debug(_('Ext name: %s'), extension.get_name())
LOG.debug(_('Ext alias: %s'), extension.get_alias())
@@ -300,40 +370,59 @@ class ExtensionManager(object):
except AttributeError as ex:
LOG.exception(_("Exception loading extension: %s"), unicode(ex))
- def _load_extensions(self):
- """
+ 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 not os.path.exists(self.path):
- return
+ if os.path.exists(self.path):
+ self._load_all_extensions_from_path(self.path)
- for f in os.listdir(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(self.path, f)
- if file_ext.lower() == '.py':
+ 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:]
- try:
- new_ext = getattr(mod, ext_name)()
- self._check_extension(new_ext)
- self.extensions[new_ext.get_alias()] = new_ext
- except AttributeError as ex:
- LOG.exception(_("Exception loading extension: %s"),
- unicode(ex))
+ 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):
- """
- ResponseExtension objects can be used to add data to responses from
- core nova OpenStack API controllers.
- """
+ """Add data to responses from core nova OpenStack API controllers."""
def __init__(self, method, url_route, handler):
self.url_route = url_route
@@ -343,10 +432,7 @@ class ResponseExtension(object):
class ActionExtension(object):
- """
- ActionExtension objects can be used to add custom actions to core nova
- nova OpenStack API controllers.
- """
+ """Add custom actions to core nova OpenStack API controllers."""
def __init__(self, collection, action_name, handler):
self.collection = collection
@@ -355,10 +441,7 @@ class ActionExtension(object):
class ResourceExtension(object):
- """
- ResourceExtension objects can be used to add top level resources
- to the OpenStack API in nova.
- """
+ """Add top level resources to the OpenStack API in nova."""
def __init__(self, collection, controller, parent=None,
collection_actions={}, member_actions={}):
diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py
index 0e9c4b26f..87118ce19 100644
--- a/nova/api/openstack/faults.py
+++ b/nova/api/openstack/faults.py
@@ -20,10 +20,10 @@ import webob.dec
import webob.exc
from nova import wsgi
+from nova.api.openstack import common
class Fault(webob.exc.HTTPException):
-
"""An RS API fault response."""
_fault_names = {
@@ -47,7 +47,7 @@ class Fault(webob.exc.HTTPException):
"""Generate a WSGI response based on the exception passed to ctor."""
# Replace the body with fault details.
code = self.wrapped_exc.status_int
- fault_name = self._fault_names.get(code, "computeFault")
+ fault_name = self._fault_names.get(code, "cloudServersFault")
fault_data = {
fault_name: {
'code': code,
@@ -57,9 +57,11 @@ 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(metadata)
+ default_xmlns = common.XML_NS_V10
+ serializer = wsgi.Serializer(metadata, default_xmlns)
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
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index 5b99b5a6f..40787bd17 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -19,11 +19,11 @@ import webob
from nova import db
from nova import exception
-from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import views
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
"""Flavor controller for the OpenStack API."""
_serialization_metadata = {
@@ -76,3 +76,6 @@ class ControllerV11(Controller):
def _get_view_builder(self, req):
base_url = req.application_url
return views.flavors.ViewBuilderV11(base_url)
+
+ def get_default_xmlns(self, req):
+ return common.XML_NS_V11
diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py
index c9d6ac532..1eccc0174 100644
--- a/nova/api/openstack/image_metadata.py
+++ b/nova/api/openstack/image_metadata.py
@@ -18,15 +18,17 @@
from webob import exc
from nova import flags
+from nova import quota
from nova import utils
from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
FLAGS = flags.FLAGS
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
"""The image metadata API controller for the Openstack API"""
def __init__(self):
@@ -39,6 +41,15 @@ class Controller(wsgi.Controller):
metadata = image.get('properties', {})
return metadata
+ def _check_quota_limit(self, context, metadata):
+ if metadata is None:
+ return
+ num_metadata = len(metadata)
+ quota_metadata = quota.allowed_metadata_items(context, num_metadata)
+ if quota_metadata < num_metadata:
+ expl = _("Image metadata limit exceeded")
+ raise exc.HTTPBadRequest(explanation=expl)
+
def index(self, req, image_id):
"""Returns the list of metadata for a given instance"""
context = req.environ['nova.context']
@@ -61,6 +72,7 @@ class Controller(wsgi.Controller):
if 'metadata' in body:
for key, value in body['metadata'].iteritems():
metadata[key] = value
+ self._check_quota_limit(context, metadata)
img['properties'] = metadata
self.image_service.update(context, image_id, img, None)
return dict(metadata=metadata)
@@ -77,6 +89,7 @@ class Controller(wsgi.Controller):
img = self.image_service.show(context, image_id)
metadata = self._get_metadata(context, image_id, img)
metadata[id] = body[id]
+ self._check_quota_limit(context, metadata)
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 79852ecc6..77baf5947 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,248 +13,143 @@
# License for the specific language governing permissions and limitations
# under the License.
-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
-
-
-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
-
-
-def _translate_s3_like_images(image_metadata):
- """Work-around for leaky S3ImageService abstraction"""
- api_metadata = image_metadata.copy()
- _convert_image_id_to_hash(api_metadata)
- api_metadata = _translate_keys(api_metadata)
- _translate_status(api_metadata)
- return api_metadata
-
-
-def _translate_from_image_service_to_api(image_metadata):
- """Translate from ImageService to OpenStack API style attribute names
-
- This involves 4 steps:
-
- 1. Filter out attributes that the OpenStack API doesn't need
-
- 2. Translate from base image attributes from names used by
- BaseImageService to names used by OpenStack API
-
- 3. Add in any image properties
-
- 4. Format values according to API spec (for example dates must
- look like "2010-08-10T12:00:00Z")
- """
- service_metadata = image_metadata.copy()
- properties = service_metadata.pop('properties', {})
-
- # 1. Filter out unecessary attributes
- api_keys = ['id', 'name', 'updated_at', 'created_at', 'status']
- api_metadata = utils.subset_dict(service_metadata, api_keys)
-
- # 2. Translate base image attributes
- api_map = {'updated_at': 'updated', 'created_at': 'created'}
- api_metadata = utils.map_dict_keys(api_metadata, api_map)
-
- # 3. Add in any image properties
- # 3a. serverId is used for backups and snapshots
- try:
- api_metadata['serverId'] = int(properties['instance_id'])
- except KeyError:
- pass # skip if it's not present
- except ValueError:
- pass # skip if it's not an integer
-
- # 3b. Progress special case
- # TODO(sirp): ImageService doesn't have a notion of progress yet, so for
- # now just fake it
- if service_metadata['status'] == 'saving':
- api_metadata['progress'] = 0
-
- # 4. Format values
- # 4a. Format Image Status (API requires uppercase)
- api_metadata['status'] = _format_status_for_api(api_metadata['status'])
-
- # 4b. Format timestamps
- for attr in ('created', 'updated'):
- if attr in api_metadata:
- api_metadata[attr] = _format_datetime_for_api(
- api_metadata[attr])
-
- return api_metadata
-
-
-def _format_status_for_api(status):
- """Return status in a format compliant with OpenStack API"""
- mapping = {'queued': 'QUEUED',
- 'preparing': 'PREPARING',
- 'saving': 'SAVING',
- 'active': 'ACTIVE',
- 'killed': 'FAILED'}
- return mapping[status]
-
-
-def _format_datetime_for_api(datetime_):
- """Stringify datetime objects in a format compliant with OpenStack API"""
- API_DATETIME_FMT = '%Y-%m-%dT%H:%M:%SZ'
- return datetime_.strftime(API_DATETIME_FMT)
-
-
-def _safe_translate(image_metadata):
- """Translate attributes for OpenStack API, temporary workaround for
- S3ImageService attribute leakage.
- """
- # FIXME(sirp): The S3ImageService appears to be leaking implementation
- # details, including its internal attribute names, and internal
- # `status` values. Working around it for now.
- s3_like_image = ('imageId' in image_metadata)
- if s3_like_image:
- translate = _translate_s3_like_images
- else:
- translate = _translate_from_image_service_to_api
- return translate(image_metadata)
-
-
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
+ """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):
- self._service = utils.import_object(FLAGS.image_service)
+ def __init__(self, image_service=None, compute_service=None):
+ """Initialize new `ImageController`.
+
+ :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"""
+ """Return an index listing of images available to the request.
+
+ :param req: `wsgi.Request` object
+ """
context = req.environ['nova.context']
- image_metas = self._service.index(context)
- image_metas = common.limited(image_metas, req)
- return dict(images=image_metas)
+ 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"""
+ """Return a detailed index listing of images available to the request.
+
+ :param req: `wsgi.Request` object.
+ """
context = req.environ['nova.context']
- image_metas = self._service.detail(context)
- image_metas = common.limited(image_metas, req)
- api_image_metas = [_safe_translate(image_meta)
- for image_meta in image_metas]
- return dict(images=api_image_metas)
+ 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"""
+ """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))
+
try:
- image_id = common.get_image_id_from_image_hash(
- self._service, context, id)
+ image = self._image_service.show(context, image_id)
except exception.NotFound:
- raise faults.Fault(exc.HTTPNotFound())
+ explanation = _("Image '%d' not found.") % (image_id)
+ raise faults.Fault(webob.exc.HTTPNotFound(explanation=explanation))
- image_meta = self._service.show(context, image_id)
- api_image_meta = _safe_translate(image_meta)
- return dict(image=api_image_meta)
+ 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.get_content_type())
- instance_id = env["image"]["serverId"]
- name = env["image"]["name"]
- image_meta = compute.API().snapshot(
- context, instance_id, name)
- api_image_meta = _safe_translate(image_meta)
- return dict(image=api_image_meta)
-
- 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())
+ 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)
+
+
+class ControllerV11(Controller):
+ """Version 1.1 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.ViewBuilderV11(base_url)
+
+ def get_default_xmlns(self, req):
+ return common.XML_NS_V11
diff --git a/nova/api/openstack/ips.py b/nova/api/openstack/ips.py
new file mode 100644
index 000000000..778e9ba1a
--- /dev/null
+++ b/nova/api/openstack/ips.py
@@ -0,0 +1,72 @@
+# 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 time
+
+from webob import exc
+
+import nova
+import nova.api.openstack.views.addresses
+from nova.api.openstack import common
+from nova.api.openstack import faults
+
+
+class Controller(common.OpenstackController):
+ """The servers addresses API controller for the Openstack API."""
+
+ _serialization_metadata = {
+ 'application/xml': {
+ 'list_collections': {
+ 'public': {'item_name': 'ip', 'item_key': 'addr'},
+ 'private': {'item_name': 'ip', 'item_key': 'addr'},
+ },
+ },
+ }
+
+ def __init__(self):
+ self.compute_api = nova.compute.API()
+ self.builder = nova.api.openstack.views.addresses.ViewBuilderV10()
+
+ def index(self, req, server_id):
+ try:
+ instance = self.compute_api.get(req.environ['nova.context'], id)
+ except nova.exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return {'addresses': self.builder.build(instance)}
+
+ def public(self, req, server_id):
+ try:
+ instance = self.compute_api.get(req.environ['nova.context'], id)
+ except nova.exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return {'public': self.builder.build_public_parts(instance)}
+
+ def private(self, req, server_id):
+ try:
+ instance = self.compute_api.get(req.environ['nova.context'], id)
+ except nova.exception.NotFound:
+ return faults.Fault(exc.HTTPNotFound())
+ return {'private': self.builder.build_private_parts(instance)}
+
+ def show(self, req, server_id, id):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def create(self, req, server_id):
+ return faults.Fault(exc.HTTPNotImplemented())
+
+ def delete(self, req, server_id, id):
+ return faults.Fault(exc.HTTPNotImplemented())
diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py
index efc7d193d..9877af191 100644
--- a/nova/api/openstack/limits.py
+++ b/nova/api/openstack/limits.py
@@ -31,8 +31,8 @@ from collections import defaultdict
from webob.dec import wsgify
from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
-from nova.wsgi import Controller
from nova.wsgi import Middleware
@@ -43,7 +43,7 @@ PER_HOUR = 60 * 60
PER_DAY = 60 * 60 * 24
-class LimitsController(Controller):
+class LimitsController(common.OpenstackController):
"""
Controller for accessing limits in the OpenStack API.
"""
diff --git a/nova/api/openstack/server_metadata.py b/nova/api/openstack/server_metadata.py
index 45bbac99d..fd64ee4fb 100644
--- a/nova/api/openstack/server_metadata.py
+++ b/nova/api/openstack/server_metadata.py
@@ -18,11 +18,13 @@
from webob import exc
from nova import compute
+from nova import quota
from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
""" The server metadata API controller for the Openstack API """
def __init__(self):
@@ -43,10 +45,14 @@ class Controller(wsgi.Controller):
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'])
+ data = self._deserialize(req.body, req.get_content_type())
+ metadata = data.get('metadata')
+ try:
+ self.compute_api.update_or_create_instance_metadata(context,
+ server_id,
+ metadata)
+ except quota.QuotaError as error:
+ self._handle_quota_error(error)
return req.body
def update(self, req, server_id, id):
@@ -58,9 +64,13 @@ class Controller(wsgi.Controller):
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)
+ try:
+ self.compute_api.update_or_create_instance_metadata(context,
+ server_id,
+ body)
+ except quota.QuotaError as error:
+ self._handle_quota_error(error)
+
return req.body
def show(self, req, server_id, id):
@@ -76,3 +86,9 @@ class Controller(wsgi.Controller):
""" Deletes an existing metadata """
context = req.environ['nova.context']
self.compute_api.delete_instance_metadata(context, server_id, id)
+
+ def _handle_quota_error(self, error):
+ """Reraise quota errors as api-specific http exceptions."""
+ if error.code == "MetadataLimitExceeded":
+ raise exc.HTTPBadRequest(explanation=error.message)
+ raise error
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 75a305a14..415c0995f 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -40,11 +40,11 @@ import nova.api.openstack
from nova.scheduler import api as scheduler_api
-LOG = logging.getLogger('server')
+LOG = logging.getLogger('nova.api.openstack.servers')
FLAGS = flags.FLAGS
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
""" The Server API controller for the OpenStack API """
_serialization_metadata = {
@@ -55,6 +55,13 @@ class Controller(wsgi.Controller):
"imageRef"],
"link": ["rel", "type", "href"],
},
+ "dict_collections": {
+ "metadata": {"item_name": "meta", "item_key": "key"},
+ },
+ "list_collections": {
+ "public": {"item_name": "ip", "item_key": "addr"},
+ "private": {"item_name": "ip", "item_key": "addr"},
+ },
},
}
@@ -63,15 +70,6 @@ class Controller(wsgi.Controller):
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, is_detail=False)
@@ -120,6 +118,8 @@ class Controller(wsgi.Controller):
context = req.environ['nova.context']
+ password = self._get_server_admin_password(env['server'])
+
key_name = None
key_data = None
key_pairs = auth_manager.AuthManager.get_key_pairs(context)
@@ -129,50 +129,54 @@ class Controller(wsgi.Controller):
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,
- context, requested_image_id)
+ try:
+ image_id = common.get_image_id_from_image_hash(self._image_service,
+ context, requested_image_id)
+ except:
+ msg = _("Can not find requested image")
+ return faults.Fault(exc.HTTPBadRequest(msg))
+
kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image(
req, image_id)
- # 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_type = \
+ instance_types.get_instance_type_by_flavor_id(flavor_id)
(inst,) = self.compute_api.create(
context,
- instance_types.get_by_flavor_id(flavor_id),
+ inst_type,
image_id,
kernel_id=kernel_id,
ramdisk_id=ramdisk_id,
- display_name=env['server']['name'],
- display_description=env['server']['name'],
+ display_name=name,
+ display_description=name,
key_name=key_name,
key_data=key_data,
- metadata=metadata,
+ metadata=env['server'].get('metadata', {}),
injected_files=injected_files)
except quota.QuotaError as error:
self._handle_quota_error(error)
- inst['instance_type'] = flavor_id
+ inst['instance_type'] = inst_type
inst['image_id'] = requested_image_id
builder = self._get_view_builder(req)
server = builder.build(inst, is_detail=True)
- password = "%s%s" % (server['server']['name'][:4],
- utils.generate_password(12))
server['server']['adminPass'] = password
self.compute_api.set_admin_password(context, server['server']['id'],
password)
@@ -234,6 +238,10 @@ class Controller(wsgi.Controller):
# if the original error is okay, just reraise it
raise error
+ def _get_server_admin_password(self, server):
+ """ Determine the admin password for a server on creation """
+ return utils.generate_password(16)
+
@scheduler_api.redirect_handler
def update(self, req, id):
""" Updates the server name or password """
@@ -246,31 +254,45 @@ class Controller(wsgi.Controller):
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:
- 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, or
resize a server"""
actions = {
- 'reboot': self._action_reboot,
- 'resize': self._action_resize,
+ '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,
+ 'revertResize': self._action_revert_resize,
+ 'rebuild': self._action_rebuild,
}
input_dict = self._deserialize(req.body, req.get_content_type())
@@ -279,6 +301,9 @@ class Controller(wsgi.Controller):
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)
@@ -296,6 +321,7 @@ class Controller(wsgi.Controller):
return exc.HTTPAccepted()
def _action_rebuild(self, input_dict, req, id):
+ LOG.debug(_("Rebuild server action is not implemented"))
return faults.Fault(exc.HTTPNotImplemented())
def _action_resize(self, input_dict, req, id):
@@ -311,18 +337,20 @@ class Controller(wsgi.Controller):
except Exception, e:
LOG.exception(_("Error in resize %s"), e)
return faults.Fault(exc.HTTPBadRequest())
- return faults.Fault(exc.HTTPAccepted())
+ return exc.HTTPAccepted()
def _action_reboot(self, input_dict, req, id):
- try:
+ if 'reboot' in input_dict and 'type' in input_dict['reboot']:
reboot_type = input_dict['reboot']['type']
- except Exception:
- raise faults.Fault(exc.HTTPNotImplemented())
+ else:
+ LOG.exception(_("Missing argument 'type' for reboot"))
+ return faults.Fault(exc.HTTPUnprocessableEntity())
try:
# TODO(gundlach): pass reboot_type, support soft reboot in
# virt driver
self.compute_api.reboot(req.environ['nova.context'], id)
- except:
+ except Exception, e:
+ LOG.exception(_("Error in reboot %s"), e)
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
@@ -477,7 +505,7 @@ class Controller(wsgi.Controller):
@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))
@@ -486,6 +514,16 @@ class Controller(wsgi.Controller):
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"]
@@ -530,7 +568,7 @@ class Controller(wsgi.Controller):
_("Cannot build from image %(image_id)s, status not active") %
locals())
- if image_meta['properties']['disk_format'] != 'ami':
+ if image_meta.get('container_format') != 'ami':
return None, None
try:
@@ -566,6 +604,14 @@ class ControllerV10(Controller):
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):
@@ -589,9 +635,35 @@ class ControllerV11(Controller):
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)
+ def _get_server_admin_password(self, server):
+ """ Determine the admin password for a server on creation """
+ password = server.get('adminPass')
+ if password is None:
+ return utils.generate_password(16)
+ if not isinstance(password, basestring) or password == '':
+ msg = _("Invalid adminPass")
+ raise exc.HTTPBadRequest(msg)
+ return password
+
+ def get_default_xmlns(self, req):
+ return common.XML_NS_V11
+
class ServerCreateRequestXMLDeserializer(object):
"""
diff --git a/nova/api/openstack/shared_ip_groups.py b/nova/api/openstack/shared_ip_groups.py
index 5d78f9377..996db3648 100644
--- a/nova/api/openstack/shared_ip_groups.py
+++ b/nova/api/openstack/shared_ip_groups.py
@@ -17,7 +17,7 @@
from webob import exc
-from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
@@ -32,7 +32,7 @@ def _translate_detail_keys(inst):
return dict(sharedIpGroups=inst)
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
""" The Shared IP Groups Controller for the Openstack API """
_serialization_metadata = {
@@ -42,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 """
@@ -58,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
index d3ab3d553..077ccfc79 100644
--- a/nova/api/openstack/users.py
+++ b/nova/api/openstack/users.py
@@ -18,7 +18,6 @@ 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
@@ -35,7 +34,7 @@ def _translate_keys(user):
admin=user.admin)
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
_serialization_metadata = {
'application/xml': {
diff --git a/nova/api/openstack/versions.py b/nova/api/openstack/versions.py
index 33f1dd628..3f9d91934 100644
--- a/nova/api/openstack/versions.py
+++ b/nova/api/openstack/versions.py
@@ -15,8 +15,8 @@
# License for the specific language governing permissions and limitations
# under the License.
+import webob
import webob.dec
-import webob.exc
from nova import wsgi
import nova.api.openstack.views.versions
@@ -51,4 +51,10 @@ class Versions(wsgi.Application):
}
content_type = req.best_match_content_type()
- return wsgi.Serializer(metadata).serialize(response, 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/addresses.py b/nova/api/openstack/views/addresses.py
index 90c77855b..2810cce39 100644
--- a/nova/api/openstack/views/addresses.py
+++ b/nova/api/openstack/views/addresses.py
@@ -28,10 +28,16 @@ class ViewBuilder(object):
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')
+ private_ips = self.build_private_parts(inst)
+ public_ips = self.build_public_parts(inst)
return dict(public=public_ips, private=private_ips)
+ def build_public_parts(self, inst):
+ return utils.get_from_path(inst, 'fixed_ip/floating_ips/address')
+
+ def build_private_parts(self, inst):
+ return utils.get_from_path(inst, 'fixed_ip/address')
+
class ViewBuilderV11(ViewBuilder):
def build(self, inst):
diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py
index a6c6ad7d1..9dec8a355 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -15,20 +15,100 @@
# License for the specific language governing permissions and limitations
# under the License.
-from nova.api.openstack import common
+import os.path
class ViewBuilder(object):
- def __init__(self):
- pass
+ """Base class for generating responses to OpenStack API image requests."""
- def build(self, image_obj):
- raise NotImplementedError()
+ 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')
-class ViewBuilderV11(ViewBuilder):
- def __init__(self, base_url):
- self.base_url = base_url
+ 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 "%s/images/%s" % (self.base_url, 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
index 4e7f62eb3..e52bfaea3 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -57,16 +57,16 @@ class ViewBuilder(object):
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'}
+ 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']),
@@ -77,12 +77,12 @@ class ViewBuilder(object):
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'
+ inst_dict['status'] = 'RESIZE-CONFIRM'
# Return the metadata as a dictionary
metadata = {}
for item in inst.get('metadata', []):
- metadata[item['key']] = item['value']
+ metadata[item['key']] = str(item['value'])
inst_dict['metadata'] = metadata
inst_dict['hostId'] = ''
@@ -115,7 +115,7 @@ class ViewBuilderV10(ViewBuilder):
def _build_flavor(self, response, inst):
if 'instance_type' in dict(inst):
- response['flavorId'] = inst['instance_type']
+ response['flavorId'] = inst['instance_type']['flavorid']
class ViewBuilderV11(ViewBuilder):
@@ -134,7 +134,7 @@ class ViewBuilderV11(ViewBuilder):
def _build_flavor(self, response, inst):
if "instance_type" in dict(inst):
- flavor_id = inst["instance_type"]
+ flavor_id = inst["instance_type"]['flavorid']
flavor_ref = self.flavor_builder.generate_href(flavor_id)
response["flavorRef"] = flavor_ref
diff --git a/nova/api/openstack/zones.py b/nova/api/openstack/zones.py
index 846cb48a1..227ffecdc 100644
--- a/nova/api/openstack/zones.py
+++ b/nova/api/openstack/zones.py
@@ -13,12 +13,10 @@
# 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.api.openstack import common
from nova.scheduler import api
@@ -43,7 +41,7 @@ def _scrub_zone(zone):
'deleted', 'deleted_at', 'updated_at'))
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
_serialization_metadata = {
'application/xml': {