diff options
| author | Josh Kearney <josh.kearney@rackspace.com> | 2011-03-18 15:42:41 -0500 |
|---|---|---|
| committer | Josh Kearney <josh.kearney@rackspace.com> | 2011-03-18 15:42:41 -0500 |
| commit | 9eed0c7fde080e68b56843c9f3e8846d4168f089 (patch) | |
| tree | 799f93fc7e5834bcd1f23d071759533fc94f5073 /nova/api | |
| parent | 8437d947a6e94baf7aa53746ffd34aa5c0f521d9 (diff) | |
| parent | c983f80c60b2c5714926a664d45d3fcd6bcf0438 (diff) | |
Merged trunk
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/ec2/__init__.py | 6 | ||||
| -rw-r--r-- | nova/api/ec2/cloud.py | 2 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py | 13 | ||||
| -rw-r--r-- | nova/api/openstack/auth.py | 2 | ||||
| -rw-r--r-- | nova/api/openstack/common.py | 4 | ||||
| -rw-r--r-- | nova/api/openstack/faults.py | 39 | ||||
| -rw-r--r-- | nova/api/openstack/flavors.py | 3 | ||||
| -rw-r--r-- | nova/api/openstack/limits.py | 358 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py | 98 | ||||
| -rw-r--r-- | nova/api/openstack/users.py | 17 | ||||
| -rw-r--r-- | nova/api/openstack/views/__init__.py | 0 | ||||
| -rw-r--r-- | nova/api/openstack/views/addresses.py | 54 | ||||
| -rw-r--r-- | nova/api/openstack/views/flavors.py | 51 | ||||
| -rw-r--r-- | nova/api/openstack/views/images.py | 51 | ||||
| -rw-r--r-- | nova/api/openstack/views/servers.py | 132 |
15 files changed, 754 insertions, 76 deletions
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index fccebca5d..20701cfa8 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -31,7 +31,7 @@ from nova import log as logging from nova import utils from nova import wsgi from nova.api.ec2 import apirequest -from nova.api.ec2 import cloud +from nova.api.ec2 import ec2utils from nova.auth import manager @@ -319,13 +319,13 @@ class Executor(wsgi.Application): except exception.InstanceNotFound as ex: LOG.info(_('InstanceNotFound raised: %s'), unicode(ex), context=context) - ec2_id = cloud.id_to_ec2_id(ex.instance_id) + ec2_id = ec2utils.id_to_ec2_id(ex.instance_id) message = _('Instance %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.VolumeNotFound as ex: LOG.info(_('VolumeNotFound raised: %s'), unicode(ex), context=context) - ec2_id = cloud.id_to_ec2_id(ex.volume_id, 'vol-%08x') + ec2_id = ec2utils.id_to_ec2_id(ex.volume_id, 'vol-%08x') message = _('Volume %s not found') % ec2_id return self._error(req, context, type(ex).__name__, message) except exception.NotFound as ex: diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 40a9da0e7..e257e44e7 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -959,7 +959,7 @@ class CloudController(object): raise exception.NotFound(_('Image %s not found') % image_id) internal_id = image['id'] del(image['id']) - raise Exception(image) + image['properties']['is_public'] = (operation_type == 'add') return self.image_service.update(context, internal_id, image) diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py index ce3cff337..b4c352b08 100644 --- a/nova/api/openstack/__init__.py +++ b/nova/api/openstack/__init__.py @@ -33,6 +33,7 @@ from nova.api.openstack import backup_schedules from nova.api.openstack import consoles from nova.api.openstack import flavors from nova.api.openstack import images +from nova.api.openstack import limits from nova.api.openstack import servers from nova.api.openstack import shared_ip_groups from nova.api.openstack import users @@ -114,12 +115,17 @@ class APIRouter(wsgi.Router): mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) + mapper.resource("flavor", "flavors", controller=flavors.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) @@ -128,8 +134,11 @@ class Versions(wsgi.Application): def __call__(self, req): """Respond to a request for all OpenStack API versions.""" response = { - "versions": [ - dict(status="CURRENT", id="v1.0")]} + "versions": [ + dict(status="DEPRECATED", id="v1.0"), + dict(status="CURRENT", id="v1.1"), + ], + } metadata = { "application/xml": { "attributes": dict(version=["status", "id"])}} diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py index f3a9bdeca..5aa5e099b 100644 --- a/nova/api/openstack/auth.py +++ b/nova/api/openstack/auth.py @@ -69,6 +69,8 @@ class AuthMiddleware(wsgi.Middleware): return faults.Fault(webob.exc.HTTPUnauthorized()) req.environ['nova.context'] = context.RequestContext(user, account) + version = req.path.split('/')[1].replace('v', '') + req.environ['api.version'] = version return self.application def has_authentication(self, req): diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py index 74ac21024..d6679de01 100644 --- a/nova/api/openstack/common.py +++ b/nova/api/openstack/common.py @@ -74,3 +74,7 @@ def get_image_id_from_image_hash(image_service, context, image_hash): if abs(hash(image_id)) == int(image_hash): return image_id raise exception.NotFound(image_hash) + + +def get_api_version(req): + return req.environ.get('api.version') diff --git a/nova/api/openstack/faults.py b/nova/api/openstack/faults.py index 2fd733299..0e9c4b26f 100644 --- a/nova/api/openstack/faults.py +++ b/nova/api/openstack/faults.py @@ -61,3 +61,42 @@ class Fault(webob.exc.HTTPException): content_type = req.best_match_content_type() self.wrapped_exc.body = serializer.serialize(fault_data, content_type) return self.wrapped_exc + + +class OverLimitFault(webob.exc.HTTPException): + """ + Rate-limited request response. + """ + + _serialization_metadata = { + "application/xml": { + "attributes": { + "overLimitFault": "code", + }, + }, + } + + def __init__(self, message, details, retry_time): + """ + Initialize new `OverLimitFault` with relevant information. + """ + self.wrapped_exc = webob.exc.HTTPForbidden() + self.content = { + "overLimitFault": { + "code": self.wrapped_exc.status_int, + "message": message, + "details": details, + }, + } + + @webob.dec.wsgify(RequestClass=wsgi.Request) + def __call__(self, request): + """ + Return the wrapped exception with a serialized body conforming to our + error format. + """ + serializer = wsgi.Serializer(self._serialization_metadata) + content_type = request.best_match_content_type() + content = serializer.serialize(self.content, content_type) + self.wrapped_exc.body = content + return self.wrapped_exc diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py index f3d040ba3..1c440b3a9 100644 --- a/nova/api/openstack/flavors.py +++ b/nova/api/openstack/flavors.py @@ -36,7 +36,7 @@ class Controller(wsgi.Controller): def index(self, req): """Return all flavors in brief.""" - return dict(flavors=[dict(id=flavor['id'], name=flavor['name']) + return dict(flavors=[dict(id=flavor['flavorid'], name=flavor['name']) for flavor in self.detail(req)['flavors']]) def detail(self, req): @@ -48,6 +48,7 @@ class Controller(wsgi.Controller): """Return data about the given flavor id.""" ctxt = req.environ['nova.context'] values = db.instance_type_get_by_flavor_id(ctxt, id) + values['id'] = values['flavorid'] return dict(flavor=values) raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/openstack/limits.py b/nova/api/openstack/limits.py new file mode 100644 index 000000000..efc7d193d --- /dev/null +++ b/nova/api/openstack/limits.py @@ -0,0 +1,358 @@ +# Copyright 2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License.import datetime + +""" +Module dedicated functions/classes dealing with rate limiting requests. +""" + +import copy +import httplib +import json +import math +import re +import time +import urllib +import webob.exc + +from collections import defaultdict + +from webob.dec import wsgify + +from nova import wsgi +from nova.api.openstack import faults +from nova.wsgi import Controller +from nova.wsgi import Middleware + + +# Convenience constants for the limits dictionary passed to Limiter(). +PER_SECOND = 1 +PER_MINUTE = 60 +PER_HOUR = 60 * 60 +PER_DAY = 60 * 60 * 24 + + +class LimitsController(Controller): + """ + Controller for accessing limits in the OpenStack API. + """ + + _serialization_metadata = { + "application/xml": { + "attributes": { + "limit": ["verb", "URI", "regex", "value", "unit", + "resetTime", "remaining", "name"], + }, + "plurals": { + "rate": "limit", + }, + }, + } + + def index(self, req): + """ + Return all global and rate limit information. + """ + abs_limits = {} + rate_limits = req.environ.get("nova.limits", []) + + return { + "limits": { + "rate": rate_limits, + "absolute": abs_limits, + }, + } + + +class Limit(object): + """ + Stores information about a limit for HTTP requets. + """ + + UNITS = { + 1: "SECOND", + 60: "MINUTE", + 60 * 60: "HOUR", + 60 * 60 * 24: "DAY", + } + + def __init__(self, verb, uri, regex, value, unit): + """ + Initialize a new `Limit`. + + @param verb: HTTP verb (POST, PUT, etc.) + @param uri: Human-readable URI + @param regex: Regular expression format for this limit + @param value: Integer number of requests which can be made + @param unit: Unit of measure for the value parameter + """ + self.verb = verb + self.uri = uri + self.regex = regex + self.value = int(value) + self.unit = unit + self.unit_string = self.display_unit().lower() + self.remaining = int(value) + + if value <= 0: + raise ValueError("Limit value must be > 0") + + self.last_request = None + self.next_request = None + + self.water_level = 0 + self.capacity = self.unit + self.request_value = float(self.capacity) / float(self.value) + self.error_message = _("Only %(value)s %(verb)s request(s) can be "\ + "made to %(uri)s every %(unit_string)s." % self.__dict__) + + def __call__(self, verb, url): + """ + Represents a call to this limit from a relevant request. + + @param verb: string http verb (POST, GET, etc.) + @param url: string URL + """ + if self.verb != verb or not re.match(self.regex, url): + return + + now = self._get_time() + + if self.last_request is None: + self.last_request = now + + leak_value = now - self.last_request + + self.water_level -= leak_value + self.water_level = max(self.water_level, 0) + self.water_level += self.request_value + + difference = self.water_level - self.capacity + + self.last_request = now + + if difference > 0: + self.water_level -= self.request_value + self.next_request = now + difference + return difference + + cap = self.capacity + water = self.water_level + val = self.value + + self.remaining = math.floor(((cap - water) / cap) * val) + self.next_request = now + + def _get_time(self): + """Retrieve the current time. Broken out for testability.""" + return time.time() + + def display_unit(self): + """Display the string name of the unit.""" + return self.UNITS.get(self.unit, "UNKNOWN") + + def display(self): + """Return a useful representation of this class.""" + return { + "verb": self.verb, + "URI": self.uri, + "regex": self.regex, + "value": self.value, + "remaining": int(self.remaining), + "unit": self.display_unit(), + "resetTime": int(self.next_request or self._get_time()), + } + +# "Limit" format is a dictionary with the HTTP verb, human-readable URI, +# a regular-expression to match, value and unit of measure (PER_DAY, etc.) + +DEFAULT_LIMITS = [ + Limit("POST", "*", ".*", 10, PER_MINUTE), + Limit("POST", "*/servers", "^/servers", 50, PER_DAY), + Limit("PUT", "*", ".*", 10, PER_MINUTE), + Limit("GET", "*changes-since*", ".*changes-since.*", 3, PER_MINUTE), + Limit("DELETE", "*", ".*", 100, PER_MINUTE), +] + + +class RateLimitingMiddleware(Middleware): + """ + Rate-limits requests passing through this middleware. All limit information + is stored in memory for this implementation. + """ + + def __init__(self, application, limits=None): + """ + Initialize new `RateLimitingMiddleware`, which wraps the given WSGI + application and sets up the given limits. + + @param application: WSGI application to wrap + @param limits: List of dictionaries describing limits + """ + Middleware.__init__(self, application) + self._limiter = Limiter(limits or DEFAULT_LIMITS) + + @wsgify(RequestClass=wsgi.Request) + def __call__(self, req): + """ + Represents a single call through this middleware. We should record the + request if we have a limit relevant to it. If no limit is relevant to + the request, ignore it. + + If the request should be rate limited, return a fault telling the user + they are over the limit and need to retry later. + """ + verb = req.method + url = req.url + context = req.environ.get("nova.context") + + if context: + username = context.user_id + else: + username = None + + delay, error = self._limiter.check_for_delay(verb, url, username) + + if delay: + msg = _("This request was rate-limited.") + retry = time.time() + delay + return faults.OverLimitFault(msg, error, retry) + + req.environ["nova.limits"] = self._limiter.get_limits(username) + + return self.application + + +class Limiter(object): + """ + Rate-limit checking class which handles limits in memory. + """ + + def __init__(self, limits): + """ + Initialize the new `Limiter`. + + @param limits: List of `Limit` objects + """ + self.limits = copy.deepcopy(limits) + self.levels = defaultdict(lambda: copy.deepcopy(limits)) + + def get_limits(self, username=None): + """ + Return the limits for a given user. + """ + return [limit.display() for limit in self.levels[username]] + + def check_for_delay(self, verb, url, username=None): + """ + Check the given verb/user/user triplet for limit. + + @return: Tuple of delay (in seconds) and error message (or None, None) + """ + delays = [] + + for limit in self.levels[username]: + delay = limit(verb, url) + if delay: + delays.append((delay, limit.error_message)) + + if delays: + delays.sort() + return delays[0] + + return None, None + + +class WsgiLimiter(object): + """ + Rate-limit checking from a WSGI application. Uses an in-memory `Limiter`. + + To use: + POST /<username> with JSON data such as: + { + "verb" : GET, + "path" : "/servers" + } + + and receive a 204 No Content, or a 403 Forbidden with an X-Wait-Seconds + header containing the number of seconds to wait before the action would + succeed. + """ + + def __init__(self, limits=None): + """ + Initialize the new `WsgiLimiter`. + + @param limits: List of `Limit` objects + """ + self._limiter = Limiter(limits or DEFAULT_LIMITS) + + @wsgify(RequestClass=wsgi.Request) + def __call__(self, request): + """ + Handles a call to this application. Returns 204 if the request is + acceptable to the limiter, else a 403 is returned with a relevant + header indicating when the request *will* succeed. + """ + if request.method != "POST": + raise webob.exc.HTTPMethodNotAllowed() + + try: + info = dict(json.loads(request.body)) + except ValueError: + raise webob.exc.HTTPBadRequest() + + username = request.path_info_pop() + verb = info.get("verb") + path = info.get("path") + + delay, error = self._limiter.check_for_delay(verb, path, username) + + if delay: + headers = {"X-Wait-Seconds": "%.2f" % delay} + return webob.exc.HTTPForbidden(headers=headers, explanation=error) + else: + return webob.exc.HTTPNoContent() + + +class WsgiLimiterProxy(object): + """ + Rate-limit requests based on answers from a remote source. + """ + + def __init__(self, limiter_address): + """ + Initialize the new `WsgiLimiterProxy`. + + @param limiter_address: IP/port combination of where to request limit + """ + self.limiter_address = limiter_address + + def check_for_delay(self, verb, path, username=None): + body = json.dumps({"verb": verb, "path": path}) + headers = {"Content-Type": "application/json"} + + conn = httplib.HTTPConnection(self.limiter_address) + + if username: + conn.request("POST", "/%s" % (username), body, headers) + else: + conn.request("POST", "/", body, headers) + + resp = conn.getresponse() + + if 200 >= resp.status < 300: + return None, None + + return resp.getheader("X-Wait-Seconds"), resp.read() or None diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 3ecd4fb01..830bc2659 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -29,70 +29,19 @@ from nova import wsgi from nova import utils from nova.api.openstack import common from nova.api.openstack import faults +from nova.api.openstack.views import servers as servers_views +from nova.api.openstack.views import addresses as addresses_views from nova.auth import manager as auth_manager from nova.compute import instance_types from nova.compute import power_state +from nova.quota import QuotaError import nova.api.openstack LOG = logging.getLogger('server') - - FLAGS = flags.FLAGS -def _translate_detail_keys(inst): - """ Coerces into dictionary format, mapping everything to Rackspace-like - attributes for return""" - power_mapping = { - None: 'build', - power_state.NOSTATE: 'build', - power_state.RUNNING: 'active', - power_state.BLOCKED: 'active', - power_state.SUSPENDED: 'suspended', - power_state.PAUSED: 'paused', - power_state.SHUTDOWN: 'active', - power_state.SHUTOFF: 'active', - power_state.CRASHED: 'error', - power_state.FAILED: 'error'} - inst_dict = {} - - mapped_keys = dict(status='state', imageId='image_id', - flavorId='instance_type', name='display_name', id='id') - - for k, v in mapped_keys.iteritems(): - inst_dict[k] = inst[v] - - inst_dict['status'] = power_mapping[inst_dict['status']] - inst_dict['addresses'] = dict(public=[], private=[]) - - # grab single private fixed ip - private_ips = utils.get_from_path(inst, 'fixed_ip/address') - inst_dict['addresses']['private'] = private_ips - - # grab all public floating ips - public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') - inst_dict['addresses']['public'] = public_ips - - # Return the metadata as a dictionary - metadata = {} - for item in inst['metadata']: - metadata[item['key']] = item['value'] - inst_dict['metadata'] = metadata - - inst_dict['hostId'] = '' - if inst['host']: - inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() - - return dict(server=inst_dict) - - -def _translate_keys(inst): - """ Coerces into dictionary format, excluding all model attributes - save for id and name """ - return dict(server=dict(id=inst['id'], name=inst['display_name'])) - - class Controller(wsgi.Controller): """ The Server API controller for the OpenStack API """ @@ -100,36 +49,49 @@ class Controller(wsgi.Controller): 'application/xml': { "attributes": { "server": ["id", "imageId", "name", "flavorId", "hostId", - "status", "progress", "adminPass"]}}} + "status", "progress", "adminPass", "flavorRef", + "imageRef"]}}} def __init__(self): self.compute_api = compute.API() self._image_service = utils.import_object(FLAGS.image_service) super(Controller, self).__init__() + def ips(self, req, id): + try: + instance = self.compute_api.get(req.environ['nova.context'], id) + except exception.NotFound: + return faults.Fault(exc.HTTPNotFound()) + + builder = addresses_views.get_view_builder(req) + return builder.build(instance) + def index(self, req): """ Returns a list of server names and ids for a given user """ - return self._items(req, entity_maker=_translate_keys) + return self._items(req, is_detail=False) def detail(self, req): """ Returns a list of server details for a given user """ - return self._items(req, entity_maker=_translate_detail_keys) + return self._items(req, is_detail=True) - def _items(self, req, entity_maker): + def _items(self, req, is_detail): """Returns a list of servers for a given user. - entity_maker - either _translate_detail_keys or _translate_keys + builder - the response model builder """ instance_list = self.compute_api.get_all(req.environ['nova.context']) limited_list = common.limited(instance_list, req) - res = [entity_maker(inst)['server'] for inst in limited_list] - return dict(servers=res) + builder = servers_views.get_view_builder(req) + servers = [builder.build(inst, is_detail)['server'] + for inst in limited_list] + return dict(servers=servers) def show(self, req, id): """ Returns server details by server id """ try: instance = self.compute_api.get(req.environ['nova.context'], id) - return _translate_detail_keys(instance) + builder = servers_views.get_view_builder(req) + return builder.build(instance, is_detail=True) except exception.NotFound: return faults.Fault(exc.HTTPNotFound()) @@ -172,8 +134,10 @@ class Controller(wsgi.Controller): for k, v in env['server']['metadata'].items(): metadata.append({'key': k, 'value': v}) - personality = env['server'].get('personality', []) - injected_files = self._get_injected_files(personality) + personality = env['server'].get('personality') + injected_files = [] + if personality: + injected_files = self._get_injected_files(personality) try: instances = self.compute_api.create( @@ -189,9 +153,10 @@ class Controller(wsgi.Controller): metadata=metadata, injected_files=injected_files) except QuotaError as error: - self._handle_quota_error(error) + self._handle_quota_errors(error) - server = _translate_keys(instances[0]) + builder = servers_views.get_view_builder(req) + server = builder.build(instances[0], is_detail=False) password = "%s%s" % (server['server']['name'][:4], utils.generate_password(12)) server['server']['adminPass'] = password @@ -220,6 +185,7 @@ class Controller(wsgi.Controller): underlying compute service. """ injected_files = [] + for item in personality: try: path = item['path'] diff --git a/nova/api/openstack/users.py b/nova/api/openstack/users.py index ebd0f4512..d3ab3d553 100644 --- a/nova/api/openstack/users.py +++ b/nova/api/openstack/users.py @@ -13,13 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import common +from webob import exc from nova import exception from nova import flags from nova import log as logging from nova import wsgi - +from nova.api.openstack import common +from nova.api.openstack import faults from nova.auth import manager FLAGS = flags.FLAGS @@ -63,7 +64,17 @@ class Controller(wsgi.Controller): def show(self, req, id): """Return data about the given user id""" - user = self.manager.get_user(id) + + #NOTE(justinsb): The drivers are a little inconsistent in how they + # deal with "NotFound" - some throw, some return None. + try: + user = self.manager.get_user(id) + except exception.NotFound: + user = None + + if user is None: + raise faults.Fault(exc.HTTPNotFound()) + return dict(user=_translate_keys(user)) def delete(self, req, id): diff --git a/nova/api/openstack/views/__init__.py b/nova/api/openstack/views/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/nova/api/openstack/views/__init__.py diff --git a/nova/api/openstack/views/addresses.py b/nova/api/openstack/views/addresses.py new file mode 100644 index 000000000..9d392aace --- /dev/null +++ b/nova/api/openstack/views/addresses.py @@ -0,0 +1,54 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova import utils +from nova.api.openstack import common + + +def get_view_builder(req): + ''' + A factory method that returns the correct builder based on the version of + the api requested. + ''' + version = common.get_api_version(req) + if version == '1.1': + return ViewBuilder_1_1() + else: + return ViewBuilder_1_0() + + +class ViewBuilder(object): + ''' Models a server addresses response as a python dictionary.''' + + def build(self, inst): + raise NotImplementedError() + + +class ViewBuilder_1_0(ViewBuilder): + def build(self, inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + return dict(public=public_ips, private=private_ips) + + +class ViewBuilder_1_1(ViewBuilder): + def build(self, inst): + private_ips = utils.get_from_path(inst, 'fixed_ip/address') + private_ips = [dict(version=4, addr=a) for a in private_ips] + public_ips = utils.get_from_path(inst, 'fixed_ip/floating_ips/address') + public_ips = [dict(version=4, addr=a) for a in public_ips] + return dict(public=public_ips, private=private_ips) diff --git a/nova/api/openstack/views/flavors.py b/nova/api/openstack/views/flavors.py new file mode 100644 index 000000000..dd2e75a7a --- /dev/null +++ b/nova/api/openstack/views/flavors.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import common + + +def get_view_builder(req): + ''' + A factory method that returns the correct builder based on the version of + the api requested. + ''' + version = common.get_api_version(req) + base_url = req.application_url + if version == '1.1': + return ViewBuilder_1_1(base_url) + else: + return ViewBuilder_1_0() + + +class ViewBuilder(object): + def __init__(self): + pass + + def build(self, flavor_obj): + raise NotImplementedError() + + +class ViewBuilder_1_1(ViewBuilder): + def __init__(self, base_url): + self.base_url = base_url + + def generate_href(self, flavor_id): + return "%s/flavors/%s" % (self.base_url, flavor_id) + + +class ViewBuilder_1_0(ViewBuilder): + pass diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py new file mode 100644 index 000000000..2369a8f9d --- /dev/null +++ b/nova/api/openstack/views/images.py @@ -0,0 +1,51 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from nova.api.openstack import common + + +def get_view_builder(req): + ''' + A factory method that returns the correct builder based on the version of + the api requested. + ''' + version = common.get_api_version(req) + base_url = req.application_url + if version == '1.1': + return ViewBuilder_1_1(base_url) + else: + return ViewBuilder_1_0() + + +class ViewBuilder(object): + def __init__(self): + pass + + def build(self, image_obj): + raise NotImplementedError() + + +class ViewBuilder_1_1(ViewBuilder): + def __init__(self, base_url): + self.base_url = base_url + + def generate_href(self, image_id): + return "%s/images/%s" % (self.base_url, image_id) + + +class ViewBuilder_1_0(ViewBuilder): + pass diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py new file mode 100644 index 000000000..261acfed0 --- /dev/null +++ b/nova/api/openstack/views/servers.py @@ -0,0 +1,132 @@ +# vim: tabstop=4 shiftwidth=4 softtabstop=4 + +# Copyright 2010-2011 OpenStack LLC. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import hashlib +from nova.compute import power_state +from nova.api.openstack import common +from nova.api.openstack.views import addresses as addresses_view +from nova.api.openstack.views import flavors as flavors_view +from nova.api.openstack.views import images as images_view +from nova import utils + + +def get_view_builder(req): + ''' + A factory method that returns the correct builder based on the version of + the api requested. + ''' + version = common.get_api_version(req) + addresses_builder = addresses_view.get_view_builder(req) + if version == '1.1': + flavor_builder = flavors_view.get_view_builder(req) + image_builder = images_view.get_view_builder(req) + return ViewBuilder_1_1(addresses_builder, flavor_builder, + image_builder) + else: + return ViewBuilder_1_0(addresses_builder) + + +class ViewBuilder(object): + ''' + Models a server response as a python dictionary. + Abstract methods: _build_image, _build_flavor + ''' + + def __init__(self, addresses_builder): + self.addresses_builder = addresses_builder + + def build(self, inst, is_detail): + """ + Coerces into dictionary format, mapping everything to + Rackspace-like attributes for return + """ + if is_detail: + return self._build_detail(inst) + else: + return self._build_simple(inst) + + def _build_simple(self, inst): + return dict(server=dict(id=inst['id'], name=inst['display_name'])) + + def _build_detail(self, inst): + power_mapping = { + None: 'build', + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.SUSPENDED: 'suspended', + power_state.PAUSED: 'paused', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error', + power_state.FAILED: 'error'} + inst_dict = {} + + #mapped_keys = dict(status='state', imageId='image_id', + # flavorId='instance_type', name='display_name', id='id') + + mapped_keys = dict(status='state', name='display_name', id='id') + + for k, v in mapped_keys.iteritems(): + inst_dict[k] = inst[v] + + inst_dict['status'] = power_mapping[inst_dict['status']] + inst_dict['addresses'] = self.addresses_builder.build(inst) + + # Return the metadata as a dictionary + metadata = {} + for item in inst['metadata']: + metadata[item['key']] = item['value'] + inst_dict['metadata'] = metadata + + inst_dict['hostId'] = '' + if inst['host']: + inst_dict['hostId'] = hashlib.sha224(inst['host']).hexdigest() + + self._build_image(inst_dict, inst) + self._build_flavor(inst_dict, inst) + + return dict(server=inst_dict) + + def _build_image(self, response, inst): + raise NotImplementedError() + + def _build_flavor(self, response, inst): + raise NotImplementedError() + + +class ViewBuilder_1_0(ViewBuilder): + def _build_image(self, response, inst): + response["imageId"] = inst["image_id"] + + def _build_flavor(self, response, inst): + response["flavorId"] = inst["instance_type"] + + +class ViewBuilder_1_1(ViewBuilder): + def __init__(self, addresses_builder, flavor_builder, image_builder): + ViewBuilder.__init__(self, addresses_builder) + self.flavor_builder = flavor_builder + self.image_builder = image_builder + + def _build_image(self, response, inst): + image_id = inst["image_id"] + response["imageRef"] = self.image_builder.generate_href(image_id) + + def _build_flavor(self, response, inst): + flavor_id = inst["instance_type"] + response["flavorRef"] = self.flavor_builder.generate_href(flavor_id) |
