summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorKen Pepple <ken.pepple@gmail.com>2011-04-21 10:29:11 -0700
committerKen Pepple <ken.pepple@gmail.com>2011-04-21 10:29:11 -0700
commitf205dcf659697adaae0d85a042ea2ea7ffe5c1c7 (patch)
treeb2da90f0cb240450bc30b9d6ebc3c703145b9bb6 /nova/api
parentdfcdafde2dc202aa3325cd1cea8d809f56fdde57 (diff)
parentc796920198305e101c75bcbf4e027ba9e81975d7 (diff)
rebase 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/apirequest.py2
-rw-r--r--nova/api/ec2/cloud.py84
-rw-r--r--nova/api/openstack/__init__.py6
-rw-r--r--nova/api/openstack/accounts.py5
-rw-r--r--nova/api/openstack/auth.py20
-rw-r--r--nova/api/openstack/backup_schedules.py4
-rw-r--r--nova/api/openstack/common.py23
-rw-r--r--nova/api/openstack/consoles.py4
-rw-r--r--nova/api/openstack/contrib/volumes.py3
-rw-r--r--nova/api/openstack/extensions.py10
-rw-r--r--nova/api/openstack/faults.py7
-rw-r--r--nova/api/openstack/flavors.py7
-rw-r--r--nova/api/openstack/image_metadata.py15
-rw-r--r--nova/api/openstack/images.py8
-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.py67
-rw-r--r--nova/api/openstack/shared_ip_groups.py4
-rw-r--r--nova/api/openstack/users.py3
-rw-r--r--nova/api/openstack/views/addresses.py10
-rw-r--r--nova/api/openstack/views/images.py10
-rw-r--r--nova/api/openstack/views/servers.py6
-rw-r--r--nova/api/openstack/zones.py6
26 files changed, 396 insertions, 114 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/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 99520b302..4785d812a 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -110,7 +110,8 @@ class CloudController(object):
'genrootca.sh')
start = os.getcwd()
- os.makedirs(FLAGS.ca_path)
+ 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_path)
@@ -141,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'],
@@ -153,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'], 'ami')
data = {
'user-data': base64.b64decode(instance_ref['user_data']),
'meta-data': {
@@ -181,9 +187,9 @@ class CloudController(object):
'mpi': mpi}}
for image_type in ['kernel', 'ramdisk']:
- if '%s_id' % image_type in instance_ref:
+ if instance_ref.get('%s_id' % image_type):
ec2_id = self._image_ec2_id(instance_ref['%s_id' % image_type],
- image_type)
+ self._image_type(image_type))
data['meta-data']['%s-id' % image_type] = ec2_id
if False: # TODO(vish): store ancestor ids
@@ -436,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."))
@@ -658,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]
@@ -729,7 +735,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']
@@ -784,7 +793,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)
@@ -814,7 +823,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)),
@@ -871,13 +880,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):
@@ -885,32 +908,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')
+ image_type = self._image_type(image.get('container_format'))
ec2_id = self._image_ec2_id(image.get('id'), image_type)
name = image.get('name')
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')
if name:
i['imageLocation'] = "%s (%s)" % (image['properties'].
get('image_location'), name)
else:
i['imageLocation'] = image['properties'].get('image_location')
- i['imageState'] = image['properties'].get('image_state')
+ # 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')
- i['imageType'] = image_type
- i['isPublic'] = str(image['properties'].get('is_public', '')) == 'True'
+ 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
@@ -942,8 +975,9 @@ class CloudController(object):
image_location = kwargs['name']
metadata = {'properties': {'image_location': image_location}}
image = self.image_service.create(context, metadata)
+ image_type = self._image_type(image.get('container_format'))
image_id = self._image_ec2_id(image['id'],
- image['properties']['type'])
+ image_type)
msg = _("Registered image %(image_location)s with"
" id %(image_id)s") % locals()
LOG.audit(msg, context=context)
@@ -958,7 +992,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
@@ -983,7 +1017,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 7545eb0c9..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
@@ -144,6 +145,11 @@ class APIRouterV10(APIRouter):
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."""
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 f2d2d86e8..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 = {
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
index 75aeb0a5f..0b6dc944a 100644
--- a/nova/api/openstack/common.py
+++ b/nova/api/openstack/common.py
@@ -22,14 +22,19 @@ import webob
from nova import exception
from nova import flags
from nova import log as logging
+from nova import wsgi
-LOG = logging.getLogger('common')
+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.
@@ -111,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)
@@ -128,3 +139,9 @@ def get_id_from_href(href):
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/volumes.py b/nova/api/openstack/contrib/volumes.py
index 6efacce52..18de2ec71 100644
--- a/nova/api/openstack/contrib/volumes.py
+++ b/nova/api/openstack/contrib/volumes.py
@@ -322,8 +322,7 @@ class Volumes(extensions.ExtensionDescriptor):
# Does this matter?
res = extensions.ResourceExtension('volumes',
VolumeController(),
- collection_actions={'detail': 'GET'}
- )
+ collection_actions={'detail': 'GET'})
resources.append(res)
res = extensions.ResourceExtension('volume_attachments',
diff --git a/nova/api/openstack/extensions.py b/nova/api/openstack/extensions.py
index fb1dccb28..7ea7afef6 100644
--- a/nova/api/openstack/extensions.py
+++ b/nova/api/openstack/extensions.py
@@ -28,6 +28,7 @@ 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
@@ -115,7 +116,7 @@ class ExtensionDescriptor(object):
return response_exts
-class ActionExtensionController(wsgi.Controller):
+class ActionExtensionController(common.OpenstackController):
def __init__(self, application):
@@ -136,7 +137,7 @@ class ActionExtensionController(wsgi.Controller):
return res
-class ResponseExtensionController(wsgi.Controller):
+class ResponseExtensionController(common.OpenstackController):
def __init__(self, application):
self.application = application
@@ -155,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
@@ -163,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
diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py
index 940bd8771..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,7 +57,8 @@ 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
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 e3f93f635..34d4c27fc 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
-import datetime
-
import webob.exc
from nova import compute
@@ -22,7 +20,6 @@ from nova import exception
from nova import flags
from nova import log
from nova import utils
-from nova import wsgi
from nova.api.openstack import common
from nova.api.openstack import faults
from nova.api.openstack.views import images as images_view
@@ -32,7 +29,7 @@ LOG = log.getLogger('nova.api.openstack.images')
FLAGS = flags.FLAGS
-class Controller(wsgi.Controller):
+class Controller(common.OpenstackController):
"""Base `wsgi.Controller` for retrieving/displaying images."""
_serialization_metadata = {
@@ -153,3 +150,6 @@ class ControllerV11(Controller):
"""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 6704a68ae..22a9c632c 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -44,7 +44,7 @@ LOG = logging.getLogger('server')
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,21 +129,16 @@ 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:
@@ -160,9 +155,11 @@ class Controller(wsgi.Controller):
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,
@@ -170,17 +167,16 @@ class Controller(wsgi.Controller):
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 = utils.generate_password(16)
server['server']['adminPass'] = password
self.compute_api.set_admin_password(context, server['server']['id'],
password)
@@ -242,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 """
@@ -565,7 +565,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:
@@ -648,6 +648,19 @@ class ControllerV11(Controller):
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 ee7991d7f..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 = {
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/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 16195b050..9dec8a355 100644
--- a/nova/api/openstack/views/images.py
+++ b/nova/api/openstack/views/images.py
@@ -34,11 +34,11 @@ class ViewBuilder(object):
def _format_status(self, image):
"""Update the status field to standardize format."""
status_mapping = {
- 'pending': 'queued',
- 'decrypting': 'preparing',
- 'untarring': 'saving',
- 'available': 'active',
- 'killed': 'failed',
+ 'pending': 'QUEUED',
+ 'decrypting': 'PREPARING',
+ 'untarring': 'SAVING',
+ 'available': 'ACTIVE',
+ 'killed': 'FAILED',
}
try:
diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py
index d24c025be..e52bfaea3 100644
--- a/nova/api/openstack/views/servers.py
+++ b/nova/api/openstack/views/servers.py
@@ -82,7 +82,7 @@ class ViewBuilder(object):
# 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': {