summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorNachi Ueno <ueno.nachi@lab.ntt.co.jp>2011-01-04 04:18:26 -0500
committerNachi Ueno <ueno.nachi@lab.ntt.co.jp>2011-01-04 04:18:26 -0500
commit4267c1f2042f6e13cd9c77eced7f02750927ee83 (patch)
treeddddf4611449d9fc95f2933d2a0c6fbc89a12a1c /nova/api
parentc5c58cb20def79401a374f863983a343139b53f3 (diff)
parentdd1e36b9690a2c2de18c565c496b25295a13d0aa (diff)
Merged and fiexed conflicts with r515
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/__init__.py5
-rw-r--r--nova/api/cloudpipe/__init__.py69
-rw-r--r--nova/api/ec2/__init__.py136
-rw-r--r--nova/api/ec2/admin.py1
-rw-r--r--nova/api/ec2/apirequest.py4
-rw-r--r--nova/api/ec2/cloud.py151
-rw-r--r--nova/api/ec2/metadatarequesthandler.py17
-rw-r--r--nova/api/openstack/__init__.py160
-rw-r--r--nova/api/openstack/auth.py66
-rw-r--r--nova/api/openstack/backup_schedules.py16
-rw-r--r--nova/api/openstack/common.py36
-rw-r--r--nova/api/openstack/flavors.py3
-rw-r--r--nova/api/openstack/images.py96
-rw-r--r--nova/api/openstack/ratelimiting/__init__.py102
-rw-r--r--nova/api/openstack/servers.py96
-rw-r--r--nova/api/openstack/sharedipgroups.py45
16 files changed, 701 insertions, 302 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py
index 80f9f2109..26fed847b 100644
--- a/nova/api/__init__.py
+++ b/nova/api/__init__.py
@@ -24,14 +24,13 @@ Root WSGI middleware for all API controllers.
:ec2api_subdomain: subdomain running the EC2 API (default: ec2)
"""
+import logging
import routes
import webob.dec
from nova import flags
-from nova import utils
from nova import wsgi
-from nova.api import cloudpipe
from nova.api import ec2
from nova.api import openstack
from nova.api.ec2 import metadatarequesthandler
@@ -41,6 +40,7 @@ flags.DEFINE_string('osapi_subdomain', 'api',
'subdomain running the OpenStack API')
flags.DEFINE_string('ec2api_subdomain', 'ec2',
'subdomain running the EC2 API')
+
FLAGS = flags.FLAGS
@@ -80,7 +80,6 @@ class API(wsgi.Router):
mapper.connect('%s/{path_info:.*}' % s, controller=mrh,
conditions=ec2api_subdomain)
- mapper.connect("/cloudpipe/{path_info:.*}", controller=cloudpipe.API())
super(API, self).__init__(mapper)
@webob.dec.wsgify
diff --git a/nova/api/cloudpipe/__init__.py b/nova/api/cloudpipe/__init__.py
deleted file mode 100644
index 6d40990a8..000000000
--- a/nova/api/cloudpipe/__init__.py
+++ /dev/null
@@ -1,69 +0,0 @@
-# vim: tabstop=4 shiftwidth=4 softtabstop=4
-
-# Copyright 2010 United States Government as represented by the
-# Administrator of the National Aeronautics and Space Administration.
-# All Rights Reserved.
-#
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-REST API Request Handlers for CloudPipe
-"""
-
-import logging
-import urllib
-import webob
-import webob.dec
-import webob.exc
-
-from nova import crypto
-from nova import wsgi
-from nova.auth import manager
-from nova.api.ec2 import cloud
-
-
-_log = logging.getLogger("api")
-_log.setLevel(logging.DEBUG)
-
-
-class API(wsgi.Application):
-
- def __init__(self):
- self.controller = cloud.CloudController()
-
- @webob.dec.wsgify
- def __call__(self, req):
- if req.method == 'POST':
- return self.sign_csr(req)
- _log.debug("Cloudpipe path is %s" % req.path_info)
- if req.path_info.endswith("/getca/"):
- return self.send_root_ca(req)
- return webob.exc.HTTPNotFound()
-
- def get_project_id_from_ip(self, ip):
- # TODO(eday): This was removed with the ORM branch, fix!
- instance = self.controller.get_instance_by_ip(ip)
- return instance['project_id']
-
- def send_root_ca(self, req):
- _log.debug("Getting root ca")
- project_id = self.get_project_id_from_ip(req.remote_addr)
- res = webob.Response()
- res.headers["Content-Type"] = "text/plain"
- res.body = crypto.fetch_ca(project_id)
- return res
-
- def sign_csr(self, req):
- project_id = self.get_project_id_from_ip(req.remote_addr)
- cert = self.str_params['cert']
- return crypto.sign_csr(urllib.unquote(cert), project_id)
diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py
index a6ee16c33..aa3bfaeb4 100644
--- a/nova/api/ec2/__init__.py
+++ b/nova/api/ec2/__init__.py
@@ -26,8 +26,8 @@ import webob
import webob.dec
import webob.exc
-from nova import exception
from nova import context
+from nova import exception
from nova import flags
from nova import wsgi
from nova.api.ec2 import apirequest
@@ -37,16 +37,82 @@ from nova.auth import manager
FLAGS = flags.FLAGS
+flags.DEFINE_boolean('use_forwarded_for', False,
+ 'Treat X-Forwarded-For as the canonical remote address. '
+ 'Only enable this if you have a sanitizing proxy.')
+flags.DEFINE_boolean('use_lockout', False,
+ 'Whether or not to use lockout middleware.')
+flags.DEFINE_integer('lockout_attempts', 5,
+ 'Number of failed auths before lockout.')
+flags.DEFINE_integer('lockout_minutes', 15,
+ 'Number of minutes to lockout if triggered.')
+flags.DEFINE_integer('lockout_window', 15,
+ 'Number of minutes for lockout window.')
+flags.DEFINE_list('lockout_memcached_servers', None,
+ 'Memcached servers or None for in process cache.')
+
+
_log = logging.getLogger("api")
_log.setLevel(logging.DEBUG)
class API(wsgi.Middleware):
-
"""Routing for all EC2 API requests."""
def __init__(self):
self.application = Authenticate(Router(Authorizer(Executor())))
+ if FLAGS.use_lockout:
+ self.application = Lockout(self.application)
+
+
+class Lockout(wsgi.Middleware):
+ """Lockout for x minutes on y failed auths in a z minute period.
+
+ x = lockout_timeout flag
+ y = lockout_window flag
+ z = lockout_attempts flag
+
+ Uses memcached if lockout_memcached_servers flag is set, otherwise it
+ uses a very simple in-proccess cache. Due to the simplicity of
+ the implementation, the timeout window is started with the first
+ failed request, so it will block if there are x failed logins within
+ that period.
+
+ There is a possible race condition where simultaneous requests could
+ sneak in before the lockout hits, but this is extremely rare and would
+ only result in a couple of extra failed attempts."""
+
+ def __init__(self, application):
+ """middleware can use fake for testing."""
+ if FLAGS.lockout_memcached_servers:
+ import memcache
+ else:
+ from nova import fakememcache as memcache
+ self.mc = memcache.Client(FLAGS.lockout_memcached_servers,
+ debug=0)
+ super(Lockout, self).__init__(application)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ access_key = str(req.params['AWSAccessKeyId'])
+ failures_key = "authfailures-%s" % access_key
+ failures = int(self.mc.get(failures_key) or 0)
+ if failures >= FLAGS.lockout_attempts:
+ detail = "Too many failed authentications."
+ raise webob.exc.HTTPForbidden(detail=detail)
+ res = req.get_response(self.application)
+ if res.status_int == 403:
+ failures = self.mc.incr(failures_key)
+ if failures is None:
+ # NOTE(vish): To use incr, failures has to be a string.
+ self.mc.set(failures_key, '1', time=FLAGS.lockout_window * 60)
+ elif failures >= FLAGS.lockout_attempts:
+ _log.warn('Access key %s has had %d failed authentications'
+ ' and will be locked out for %d minutes.' %
+ (access_key, failures, FLAGS.lockout_minutes))
+ self.mc.set(failures_key, str(failures),
+ time=FLAGS.lockout_minutes * 60)
+ return res
class Authenticate(wsgi.Middleware):
@@ -77,13 +143,16 @@ class Authenticate(wsgi.Middleware):
req.host,
req.path)
except exception.Error, ex:
- logging.debug("Authentication Failure: %s" % ex)
+ logging.debug(_("Authentication Failure: %s") % ex)
raise webob.exc.HTTPForbidden()
# Authenticated!
+ remote_address = req.remote_addr
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
ctxt = context.RequestContext(user=user,
project=project,
- remote_address=req.remote_addr)
+ remote_address=remote_address)
req.environ['ec2.context'] = ctxt
return self.application
@@ -120,9 +189,9 @@ class Router(wsgi.Middleware):
except:
raise webob.exc.HTTPBadRequest()
- _log.debug('action: %s' % action)
+ _log.debug(_('action: %s') % action)
for key, value in args.items():
- _log.debug('arg: %s\t\tval: %s' % (key, value))
+ _log.debug(_('arg: %s\t\tval: %s') % (key, value))
# Success!
req.environ['ec2.controller'] = controller
@@ -225,10 +294,9 @@ class Executor(wsgi.Application):
args = req.environ['ec2.action_args']
api_request = apirequest.APIRequest(controller, action)
+ result = None
try:
result = api_request.send(context, **args)
- req.headers['Content-Type'] = 'text/xml'
- return result
except exception.ApiError as ex:
if ex.code:
@@ -238,6 +306,12 @@ class Executor(wsgi.Application):
# TODO(vish): do something more useful with unknown exceptions
except Exception as ex:
return self._error(req, type(ex).__name__, str(ex))
+ else:
+ resp = webob.Response()
+ resp.status = 200
+ resp.headers['Content-Type'] = 'text/xml'
+ resp.body = str(result)
+ return resp
def _error(self, req, code, message):
logging.error("%s: %s", code, message)
@@ -249,3 +323,49 @@ class Executor(wsgi.Application):
'<Message>%s</Message></Error></Errors>'
'<RequestID>?</RequestID></Response>' % (code, message))
return resp
+
+
+class Versions(wsgi.Application):
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Respond to a request for all EC2 versions."""
+ # available api versions
+ versions = [
+ '1.0',
+ '2007-01-19',
+ '2007-03-01',
+ '2007-08-29',
+ '2007-10-10',
+ '2007-12-15',
+ '2008-02-01',
+ '2008-09-01',
+ '2009-04-04',
+ ]
+ return ''.join('%s\n' % v for v in versions)
+
+
+def authenticate_factory(global_args, **local_args):
+ def authenticator(app):
+ return Authenticate(app)
+ return authenticator
+
+
+def router_factory(global_args, **local_args):
+ def router(app):
+ return Router(app)
+ return router
+
+
+def authorizer_factory(global_args, **local_args):
+ def authorizer(app):
+ return Authorizer(app)
+ return authorizer
+
+
+def executor_factory(global_args, **local_args):
+ return Executor()
+
+
+def versions_factory(global_args, **local_args):
+ return Versions()
diff --git a/nova/api/ec2/admin.py b/nova/api/ec2/admin.py
index 1c6ab688d..fac01369e 100644
--- a/nova/api/ec2/admin.py
+++ b/nova/api/ec2/admin.py
@@ -168,6 +168,7 @@ class AdminController(object):
# FIXME(vish): these host commands don't work yet, perhaps some of the
# required data can be retrieved from service objects?
+
def describe_hosts(self, _context, **_kwargs):
"""Returns status info for all nodes. Includes:
* Disk Space
diff --git a/nova/api/ec2/apirequest.py b/nova/api/ec2/apirequest.py
index 5758781b6..a90fbeb0c 100644
--- a/nova/api/ec2/apirequest.py
+++ b/nova/api/ec2/apirequest.py
@@ -92,8 +92,8 @@ class APIRequest(object):
method = getattr(self.controller,
_camelcase_to_underscore(self.action))
except AttributeError:
- _error = ('Unsupported API request: controller = %s,'
- 'action = %s') % (self.controller, self.action)
+ _error = _('Unsupported API request: controller = %s,'
+ 'action = %s') % (self.controller, self.action)
_log.warning(_error)
# TODO: Raise custom exception, trap in apiserver,
# and reraise as 400 error.
diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py
index ff7a2f3cd..8c925ffee 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -27,7 +27,6 @@ import datetime
import logging
import re
import os
-import time
from nova import context
import IPy
@@ -114,7 +113,7 @@ class CloudController(object):
start = os.getcwd()
os.chdir(FLAGS.ca_path)
# TODO(vish): Do this with M2Crypto instead
- utils.runthis("Generating root CA: %s", "sh genrootca.sh")
+ utils.runthis(_("Generating root CA: %s"), "sh genrootca.sh")
os.chdir(start)
def _get_mpi_data(self, context, project_id):
@@ -189,22 +188,63 @@ class CloudController(object):
return data
def describe_availability_zones(self, context, **kwargs):
+ if ('zone_name' in kwargs and
+ 'verbose' in kwargs['zone_name'] and
+ context.is_admin):
+ return self._describe_availability_zones_verbose(context,
+ **kwargs)
+ else:
+ return self._describe_availability_zones(context, **kwargs)
+
+ def _describe_availability_zones(self, context, **kwargs):
return {'availabilityZoneInfo': [{'zoneName': 'nova',
'zoneState': 'available'}]}
+ def _describe_availability_zones_verbose(self, context, **kwargs):
+ rv = {'availabilityZoneInfo': [{'zoneName': 'nova',
+ 'zoneState': 'available'}]}
+
+ services = db.service_get_all(context)
+ now = db.get_time()
+ hosts = []
+ for host in [service['host'] for service in services]:
+ if not host in hosts:
+ hosts.append(host)
+ for host in hosts:
+ rv['availabilityZoneInfo'].append({'zoneName': '|- %s' % host,
+ 'zoneState': ''})
+ hsvcs = [service for service in services \
+ if service['host'] == host]
+ for svc in hsvcs:
+ delta = now - (svc['updated_at'] or svc['created_at'])
+ alive = (delta.seconds <= FLAGS.service_down_time)
+ art = (alive and ":-)") or "XXX"
+ active = 'enabled'
+ if svc['disabled']:
+ active = 'disabled'
+ rv['availabilityZoneInfo'].append({
+ 'zoneName': '| |- %s' % svc['binary'],
+ 'zoneState': '%s %s %s' % (active, art,
+ svc['updated_at'])})
+ return rv
+
def describe_regions(self, context, region_name=None, **kwargs):
if FLAGS.region_list:
regions = []
for region in FLAGS.region_list:
- name, _sep, url = region.partition('=')
+ name, _sep, host = region.partition('=')
+ endpoint = '%s://%s:%s%s' % (FLAGS.ec2_prefix,
+ host,
+ FLAGS.cc_port,
+ FLAGS.ec2_suffix)
regions.append({'regionName': name,
- 'regionEndpoint': url})
+ 'regionEndpoint': endpoint})
else:
regions = [{'regionName': 'nova',
- 'regionEndpoint': FLAGS.ec2_url}]
- if region_name:
- regions = [r for r in regions if r['regionName'] in region_name]
- return {'regionInfo': regions}
+ 'regionEndpoint': '%s://%s:%s%s' % (FLAGS.ec2_prefix,
+ FLAGS.cc_host,
+ FLAGS.cc_port,
+ FLAGS.ec2_suffix)}]
def describe_snapshots(self,
context,
@@ -319,11 +359,11 @@ class CloudController(object):
ip_protocol = str(ip_protocol)
if ip_protocol.upper() not in ['TCP', 'UDP', 'ICMP']:
- raise InvalidInputException('%s is not a valid ipProtocol' %
+ raise InvalidInputException(_('%s is not a valid ipProtocol') %
(ip_protocol,))
if ((min(from_port, to_port) < -1) or
(max(from_port, to_port) > 65535)):
- raise InvalidInputException('Invalid port range')
+ raise InvalidInputException(_('Invalid port range'))
values['protocol'] = ip_protocol
values['from_port'] = from_port
@@ -361,7 +401,8 @@ class CloudController(object):
criteria = self._revoke_rule_args_to_dict(context, **kwargs)
if criteria == None:
- raise exception.ApiError("No rule for the specified parameters.")
+ raise exception.ApiError(_("No rule for the specified "
+ "parameters."))
for rule in security_group.rules:
match = True
@@ -372,7 +413,7 @@ class CloudController(object):
db.security_group_rule_destroy(context, rule['id'])
self._trigger_refresh_security_group(context, security_group)
return True
- raise exception.ApiError("No rule for the specified parameters.")
+ raise exception.ApiError(_("No rule for the specified parameters."))
# TODO(soren): This has only been tested with Boto as the client.
# Unfortunately, it seems Boto is using an old API
@@ -388,8 +429,8 @@ class CloudController(object):
values['parent_group_id'] = security_group.id
if self._security_group_rule_exists(security_group, values):
- raise exception.ApiError('This rule already exists in group %s' %
- group_name)
+ raise exception.ApiError(_('This rule already exists in group %s')
+ % group_name)
security_group_rule = db.security_group_rule_create(context, values)
@@ -417,7 +458,7 @@ class CloudController(object):
def create_security_group(self, context, group_name, group_description):
self.compute_api.ensure_default_security_group(context)
if db.security_group_exists(context, context.project_id, group_name):
- raise exception.ApiError('group %s already exists' % group_name)
+ raise exception.ApiError(_('group %s already exists') % group_name)
group = {'user_id': context.user.id,
'project_id': context.project_id,
@@ -530,13 +571,13 @@ class CloudController(object):
def attach_volume(self, context, volume_id, instance_id, device, **kwargs):
volume_ref = db.volume_get_by_ec2_id(context, volume_id)
if not re.match("^/dev/[a-z]d[a-z]+$", device):
- raise exception.ApiError("Invalid device specified: %s. "
- "Example device: /dev/vdb" % device)
+ raise exception.ApiError(_("Invalid device specified: %s. "
+ "Example device: /dev/vdb") % device)
# TODO(vish): abstract status checking?
if volume_ref['status'] != "available":
- raise exception.ApiError("Volume status must be available")
+ raise exception.ApiError(_("Volume status must be available"))
if volume_ref['attach_status'] == "attached":
- raise exception.ApiError("Volume is already attached")
+ raise exception.ApiError(_("Volume is already attached"))
internal_id = ec2_id_to_internal_id(instance_id)
instance_ref = self.compute_api.get_instance(context, internal_id)
host = instance_ref['host']
@@ -558,10 +599,10 @@ class CloudController(object):
instance_ref = db.volume_get_instance(context.elevated(),
volume_ref['id'])
if not instance_ref:
- raise exception.ApiError("Volume isn't attached to anything!")
+ raise exception.ApiError(_("Volume isn't attached to anything!"))
# TODO(vish): abstract status checking?
if volume_ref['status'] == "available":
- raise exception.ApiError("Volume is already detached")
+ raise exception.ApiError(_("Volume is already detached"))
try:
host = instance_ref['host']
rpc.cast(context,
@@ -696,23 +737,29 @@ class CloudController(object):
def allocate_address(self, context, **kwargs):
# check quota
if quota.allowed_floating_ips(context, 1) < 1:
- logging.warn("Quota exceeeded for %s, tried to allocate address",
+ logging.warn(_("Quota exceeeded for %s, tried to allocate "
+ "address"),
context.project_id)
- raise quota.QuotaError("Address quota exceeded. You cannot "
- "allocate any more addresses")
- network_topic = self._get_network_topic(context)
+ raise quota.QuotaError(_("Address quota exceeded. You cannot "
+ "allocate any more addresses"))
+ # NOTE(vish): We don't know which network host should get the ip
+ # when we allocate, so just send it to any one. This
+ # will probably need to move into a network supervisor
+ # at some point.
public_ip = rpc.call(context,
- network_topic,
+ FLAGS.network_topic,
{"method": "allocate_floating_ip",
"args": {"project_id": context.project_id}})
return {'addressSet': [{'publicIp': public_ip}]}
def release_address(self, context, public_ip, **kwargs):
- # NOTE(vish): Should we make sure this works?
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): We don't know which network host should get the ip
+ # when we deallocate, so just send it to any one. This
+ # will probably need to move into a network supervisor
+ # at some point.
rpc.cast(context,
- network_topic,
+ FLAGS.network_topic,
{"method": "deallocate_floating_ip",
"args": {"floating_address": floating_ip_ref['address']}})
return {'releaseResponse': ["Address released."]}
@@ -723,7 +770,10 @@ class CloudController(object):
fixed_address = db.instance_get_fixed_address(context,
instance_ref['id'])
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): Perhaps we should just pass this on to compute and
+ # let compute communicate with network.
+ network_topic = self.compute_api.get_network_topic(context,
+ internal_id)
rpc.cast(context,
network_topic,
{"method": "associate_floating_ip",
@@ -733,24 +783,18 @@ class CloudController(object):
def disassociate_address(self, context, public_ip, **kwargs):
floating_ip_ref = db.floating_ip_get_by_address(context, public_ip)
- network_topic = self._get_network_topic(context)
+ # NOTE(vish): Get the topic from the host name of the network of
+ # the associated fixed ip.
+ if not floating_ip_ref.get('fixed_ip'):
+ raise exception.ApiError('Address is not associated.')
+ host = floating_ip_ref['fixed_ip']['network']['host']
+ topic = db.queue_get_for(context, FLAGS.network_topic, host)
rpc.cast(context,
- network_topic,
+ topic,
{"method": "disassociate_floating_ip",
"args": {"floating_address": floating_ip_ref['address']}})
return {'disassociateResponse': ["Address disassociated."]}
- def _get_network_topic(self, context):
- """Retrieves the network host for a project"""
- network_ref = self.network_manager.get_network(context)
- host = network_ref['host']
- if not host:
- host = rpc.call(context,
- FLAGS.network_topic,
- {"method": "set_network_host",
- "args": {"network_id": network_ref['id']}})
- return db.queue_get_for(context, FLAGS.network_topic, host)
-
def run_instances(self, context, **kwargs):
max_count = int(kwargs.get('max_count', 1))
instances = self.compute_api.create_instances(context,
@@ -758,12 +802,15 @@ class CloudController(object):
kwargs['image_id'],
min_count=int(kwargs.get('min_count', max_count)),
max_count=max_count,
- kernel_id=kwargs.get('kernel_id'),
+ kernel_id=kwargs.get('kernel_id', None),
ramdisk_id=kwargs.get('ramdisk_id'),
display_name=kwargs.get('display_name'),
description=kwargs.get('display_description'),
key_name=kwargs.get('key_name'),
+ user_data=kwargs.get('user_data'),
security_group=kwargs.get('security_group'),
+ availability_zone=kwargs.get('placement', {}).get(
+ 'AvailabilityZone'),
generate_hostname=internal_id_to_ec2_id)
return self._format_run_instances(context,
instances[0]['reservation_id'])
@@ -812,7 +859,7 @@ class CloudController(object):
# TODO: return error if not authorized
volume_ref = db.volume_get_by_ec2_id(context, volume_id)
if volume_ref['status'] != "available":
- raise exception.ApiError("Volume status must be available")
+ raise exception.ApiError(_("Volume status must be available"))
now = datetime.datetime.utcnow()
db.volume_update(context, volume_ref['id'], {'status': 'deleting',
'terminated_at': now})
@@ -843,11 +890,12 @@ class CloudController(object):
def describe_image_attribute(self, context, image_id, attribute, **kwargs):
if attribute != 'launchPermission':
- raise exception.ApiError('attribute not supported: %s' % attribute)
+ raise exception.ApiError(_('attribute not supported: %s')
+ % attribute)
try:
image = self.image_service.show(context, image_id)
except IndexError:
- raise exception.ApiError('invalid id: %s' % image_id)
+ raise exception.ApiError(_('invalid id: %s') % image_id)
result = {'image_id': image_id, 'launchPermission': []}
if image['isPublic']:
result['launchPermission'].append({'group': 'all'})
@@ -857,13 +905,14 @@ class CloudController(object):
operation_type, **kwargs):
# TODO(devcamcar): Support users and groups other than 'all'.
if attribute != 'launchPermission':
- raise exception.ApiError('attribute not supported: %s' % attribute)
+ raise exception.ApiError(_('attribute not supported: %s')
+ % attribute)
if not 'user_group' in kwargs:
- raise exception.ApiError('user or group not specified')
+ raise exception.ApiError(_('user or group not specified'))
if len(kwargs['user_group']) != 1 and kwargs['user_group'][0] != 'all':
- raise exception.ApiError('only group "all" is supported')
+ raise exception.ApiError(_('only group "all" is supported'))
if not operation_type in ['add', 'remove']:
- raise exception.ApiError('operation_type must be add or remove')
+ raise exception.ApiError(_('operation_type must be add or remove'))
return self.image_service.modify(context, image_id, operation_type)
def update_image(self, context, image_id, **kwargs):
diff --git a/nova/api/ec2/metadatarequesthandler.py b/nova/api/ec2/metadatarequesthandler.py
index 2f4f414cc..a57a6698a 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -23,9 +23,13 @@ import logging
import webob.dec
import webob.exc
+from nova import flags
from nova.api.ec2 import cloud
+FLAGS = flags.FLAGS
+
+
class MetadataRequestHandler(object):
"""Serve metadata from the EC2 API."""
@@ -63,12 +67,19 @@ class MetadataRequestHandler(object):
@webob.dec.wsgify
def __call__(self, req):
cc = cloud.CloudController()
- meta_data = cc.get_metadata(req.remote_addr)
+ remote_address = req.remote_addr
+ if FLAGS.use_forwarded_for:
+ remote_address = req.headers.get('X-Forwarded-For', remote_address)
+ meta_data = cc.get_metadata(remote_address)
if meta_data is None:
- logging.error('Failed to get metadata for ip: %s' %
- req.remote_addr)
+ logging.error(_('Failed to get metadata for ip: %s') %
+ remote_address)
raise webob.exc.HTTPNotFound()
data = self.lookup(req.path_info, meta_data)
if data is None:
raise webob.exc.HTTPNotFound()
return self.print_data(data)
+
+
+def metadata_factory(global_args, **local_args):
+ return MetadataRequestHandler()
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index b9ecbd9b8..a1430caed 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -20,7 +20,6 @@
WSGI middleware for OpenStack API controllers.
"""
-import json
import time
import logging
@@ -41,14 +40,17 @@ from nova.api.openstack import images
from nova.api.openstack import ratelimiting
from nova.api.openstack import servers
from nova.api.openstack import sharedipgroups
-from nova.auth import manager
FLAGS = flags.FLAGS
-flags.DEFINE_string('nova_api_auth',
- 'nova.api.openstack.auth.BasicApiAuthManager',
+flags.DEFINE_string('os_api_auth',
+ 'nova.api.openstack.auth.AuthMiddleware',
'The auth mechanism to use for the OpenStack API implemenation')
+flags.DEFINE_string('os_api_ratelimiting',
+ 'nova.api.openstack.ratelimiting.RateLimitingMiddleware',
+ 'Default ratelimiting implementation for the Openstack API')
+
flags.DEFINE_bool('allow_admin_api',
False,
'When True, this API service will accept admin operations.')
@@ -58,7 +60,10 @@ class API(wsgi.Middleware):
"""WSGI entry point for all OpenStack API requests."""
def __init__(self):
- app = AuthMiddleware(RateLimitingMiddleware(APIRouter()))
+ auth_middleware = utils.import_class(FLAGS.os_api_auth)
+ ratelimiting_middleware = \
+ utils.import_class(FLAGS.os_api_ratelimiting)
+ app = auth_middleware(ratelimiting_middleware(APIRouter()))
super(API, self).__init__(app)
@webob.dec.wsgify
@@ -66,102 +71,12 @@ class API(wsgi.Middleware):
try:
return req.get_response(self.application)
except Exception as ex:
- logging.warn("Caught error: %s" % str(ex))
- logging.debug(traceback.format_exc())
+ logging.warn(_("Caught error: %s") % str(ex))
+ logging.error(traceback.format_exc())
exc = webob.exc.HTTPInternalServerError(explanation=str(ex))
return faults.Fault(exc)
-class AuthMiddleware(wsgi.Middleware):
- """Authorize the openstack API request or return an HTTP Forbidden."""
-
- def __init__(self, application):
- self.auth_driver = utils.import_class(FLAGS.nova_api_auth)()
- super(AuthMiddleware, self).__init__(application)
-
- @webob.dec.wsgify
- def __call__(self, req):
- if 'X-Auth-Token' not in req.headers:
- return self.auth_driver.authenticate(req)
-
- user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"])
-
- if not user:
- return faults.Fault(webob.exc.HTTPUnauthorized())
-
- req.environ['nova.context'] = context.RequestContext(user, user)
- return self.application
-
-
-class RateLimitingMiddleware(wsgi.Middleware):
- """Rate limit incoming requests according to the OpenStack rate limits."""
-
- def __init__(self, application, service_host=None):
- """Create a rate limiting middleware that wraps the given application.
-
- By default, rate counters are stored in memory. If service_host is
- specified, the middleware instead relies on the ratelimiting.WSGIApp
- at the given host+port to keep rate counters.
- """
- super(RateLimitingMiddleware, self).__init__(application)
- if not service_host:
- #TODO(gundlach): These limits were based on limitations of Cloud
- #Servers. We should revisit them in Nova.
- self.limiter = ratelimiting.Limiter(limits={
- 'DELETE': (100, ratelimiting.PER_MINUTE),
- 'PUT': (10, ratelimiting.PER_MINUTE),
- 'POST': (10, ratelimiting.PER_MINUTE),
- 'POST servers': (50, ratelimiting.PER_DAY),
- 'GET changes-since': (3, ratelimiting.PER_MINUTE),
- })
- else:
- self.limiter = ratelimiting.WSGIAppProxy(service_host)
-
- @webob.dec.wsgify
- def __call__(self, req):
- """Rate limit the request.
-
- If the request should be rate limited, return a 413 status with a
- Retry-After header giving the time when the request would succeed.
- """
- action_name = self.get_action_name(req)
- if not action_name:
- # Not rate limited
- return self.application
- delay = self.get_delay(action_name,
- req.environ['nova.context'].user_id)
- if delay:
- # TODO(gundlach): Get the retry-after format correct.
- exc = webob.exc.HTTPRequestEntityTooLarge(
- explanation='Too many requests.',
- headers={'Retry-After': time.time() + delay})
- raise faults.Fault(exc)
- return self.application
-
- def get_delay(self, action_name, username):
- """Return the delay for the given action and username, or None if
- the action would not be rate limited.
- """
- if action_name == 'POST servers':
- # "POST servers" is a POST, so it counts against "POST" too.
- # Attempt the "POST" first, lest we are rate limited by "POST" but
- # use up a precious "POST servers" call.
- delay = self.limiter.perform("POST", username=username)
- if delay:
- return delay
- return self.limiter.perform(action_name, username=username)
-
- def get_action_name(self, req):
- """Return the action name for this request."""
- if req.method == 'GET' and 'changes-since' in req.GET:
- return 'GET changes-since'
- if req.method == 'POST' and req.path_info.startswith('/servers'):
- return 'POST servers'
- if req.method in ['PUT', 'POST', 'DELETE']:
- return req.method
- return None
-
-
class APIRouter(wsgi.Router):
"""
Routes requests on the OpenStack API to the appropriate controller
@@ -170,11 +85,22 @@ class APIRouter(wsgi.Router):
def __init__(self):
mapper = routes.Mapper()
+
+ server_members = {'action': 'POST'}
+ if FLAGS.allow_admin_api:
+ logging.debug("Including admin operations in API.")
+ server_members['pause'] = 'POST'
+ server_members['unpause'] = 'POST'
+ server_members["diagnostics"] = "GET"
+ server_members["actions"] = "GET"
+ server_members['suspend'] = 'POST'
+ server_members['resume'] = 'POST'
+
mapper.resource("server", "servers", controller=servers.Controller(),
collection={'detail': 'GET'},
- member={'action': 'POST'})
+ member=server_members)
- mapper.resource("backup_schedule", "backup_schedules",
+ mapper.resource("backup_schedule", "backup_schedule",
controller=backup_schedules.Controller(),
parent_resource=dict(member_name='server',
collection_name='servers'))
@@ -186,27 +112,25 @@ class APIRouter(wsgi.Router):
mapper.resource("sharedipgroup", "sharedipgroups",
controller=sharedipgroups.Controller())
- if FLAGS.allow_admin_api:
- logging.debug("Including admin operations in API.")
- # TODO: Place routes for admin operations here.
-
super(APIRouter, self).__init__(mapper)
-def limited(items, req):
- """Return a slice of items according to requested offset and limit.
+class Versions(wsgi.Application):
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Respond to a request for all OpenStack API versions."""
+ response = {
+ "versions": [
+ dict(status="CURRENT", id="v1.0")]}
+ metadata = {
+ "application/xml": {
+ "attributes": dict(version=["status", "id"])}}
+ return wsgi.Serializer(req.environ, metadata).to_content_type(response)
- items - a sliceable
- req - wobob.Request possibly containing offset and limit GET variables.
- offset is where to start in the list, and limit is the maximum number
- of items to return.
- If limit is not specified, 0, or > 1000, defaults to 1000.
- """
- offset = int(req.GET.get('offset', 0))
- limit = int(req.GET.get('limit', 0))
- if not limit:
- limit = 1000
- limit = min(1000, limit)
- range_end = offset + limit
- return items[offset:range_end]
+def router_factory(global_cof, **local_conf):
+ return APIRouter()
+
+
+def versions_factory(global_conf, **local_conf):
+ return Versions()
diff --git a/nova/api/openstack/auth.py b/nova/api/openstack/auth.py
index fcda97ab1..00e817c8d 100644
--- a/nova/api/openstack/auth.py
+++ b/nova/api/openstack/auth.py
@@ -1,3 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 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
+
import datetime
import hashlib
import json
@@ -7,29 +24,46 @@ import webob.exc
import webob.dec
from nova import auth
+from nova import context
from nova import db
from nova import flags
from nova import manager
from nova import utils
+from nova import wsgi
from nova.api.openstack import faults
FLAGS = flags.FLAGS
-class Context(object):
- pass
-
-
-class BasicApiAuthManager(object):
- """ Implements a somewhat rudimentary version of OpenStack Auth"""
+class AuthMiddleware(wsgi.Middleware):
+ """Authorize the openstack API request or return an HTTP Forbidden."""
- def __init__(self, db_driver=None):
+ def __init__(self, application, db_driver=None):
if not db_driver:
db_driver = FLAGS.db_driver
self.db = utils.import_object(db_driver)
self.auth = auth.manager.AuthManager()
- self.context = Context()
- super(BasicApiAuthManager, self).__init__()
+ super(AuthMiddleware, self).__init__(application)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ if not self.has_authentication(req):
+ return self.authenticate(req)
+
+ user = self.get_user_by_authentication(req)
+
+ if not user:
+ return faults.Fault(webob.exc.HTTPUnauthorized())
+
+ project = self.auth.get_project(FLAGS.default_project)
+ req.environ['nova.context'] = context.RequestContext(user, project)
+ return self.application
+
+ def has_authentication(self, req):
+ return 'X-Auth-Token' in req.headers
+
+ def get_user_by_authentication(self, req):
+ return self.authorize_token(req.headers["X-Auth-Token"])
def authenticate(self, req):
# Unless the request is explicitly made against /<version>/ don't
@@ -68,11 +102,12 @@ class BasicApiAuthManager(object):
This method will also remove the token if the timestamp is older than
2 days ago.
"""
- token = self.db.auth_get_token(self.context, token_hash)
+ ctxt = context.get_admin_context()
+ token = self.db.auth_get_token(ctxt, token_hash)
if token:
delta = datetime.datetime.now() - token.created_at
if delta.days >= 2:
- self.db.auth_destroy_token(self.context, token)
+ self.db.auth_destroy_token(ctxt, token)
else:
return self.auth.get_user(token.user_id)
return None
@@ -84,6 +119,7 @@ class BasicApiAuthManager(object):
key - string API key
req - webob.Request object
"""
+ ctxt = context.get_admin_context()
user = self.auth.get_user_from_access_key(key)
if user and user.name == username:
token_hash = hashlib.sha1('%s%s%f' % (username, key,
@@ -95,6 +131,12 @@ class BasicApiAuthManager(object):
token_dict['server_management_url'] = req.url
token_dict['storage_url'] = ''
token_dict['user_id'] = user.id
- token = self.db.auth_create_token(self.context, token_dict)
+ token = self.db.auth_create_token(ctxt, token_dict)
return token, user
return None, None
+
+
+def auth_factory(global_conf, **local_conf):
+ def auth(app):
+ return AuthMiddleware(app)
+ return auth
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 3ed691d7b..fcc07bdd3 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -23,12 +23,25 @@ from nova.api.openstack import faults
import nova.image.service
+def _translate_keys(inst):
+ """ Coerces the backup schedule into proper dictionary format """
+ return dict(backupSchedule=inst)
+
+
class Controller(wsgi.Controller):
+ """ The backup schedule API controller for the Openstack API """
+
+ _serialization_metadata = {
+ 'application/xml': {
+ 'attributes': {
+ 'backupSchedule': []}}}
+
def __init__(self):
pass
def index(self, req, server_id):
- return faults.Fault(exc.HTTPNotFound())
+ """ Returns the list of backup schedules for a given instance """
+ return _translate_keys({})
def create(self, req, server_id):
""" No actual update method required, since the existing API allows
@@ -36,4 +49,5 @@ class Controller(wsgi.Controller):
return faults.Fault(exc.HTTPNotFound())
def delete(self, req, server_id, id):
+ """ Deletes an existing backup schedule """
return faults.Fault(exc.HTTPNotFound())
diff --git a/nova/api/openstack/common.py b/nova/api/openstack/common.py
new file mode 100644
index 000000000..ac0572c96
--- /dev/null
+++ b/nova/api/openstack/common.py
@@ -0,0 +1,36 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 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.
+
+
+def limited(items, req):
+ """Return a slice of items according to requested offset and limit.
+
+ items - a sliceable
+ req - wobob.Request possibly containing offset and limit GET variables.
+ offset is where to start in the list, and limit is the maximum number
+ of items to return.
+
+ If limit is not specified, 0, or > 1000, defaults to 1000.
+ """
+
+ offset = int(req.GET.get('offset', 0))
+ limit = int(req.GET.get('limit', 0))
+ if not limit:
+ limit = 1000
+ limit = min(1000, limit)
+ range_end = offset + limit
+ return items[offset:range_end]
diff --git a/nova/api/openstack/flavors.py b/nova/api/openstack/flavors.py
index f23f74fd1..f620d4107 100644
--- a/nova/api/openstack/flavors.py
+++ b/nova/api/openstack/flavors.py
@@ -18,6 +18,7 @@
from webob import exc
from nova.api.openstack import faults
+from nova.api.openstack import common
from nova.compute import instance_types
from nova import wsgi
import nova.api.openstack
@@ -39,7 +40,7 @@ class Controller(wsgi.Controller):
def detail(self, req):
"""Return all flavors in detail."""
items = [self.show(req, id)['flavor'] for id in self._all_ids()]
- items = nova.api.openstack.limited(items, req)
+ items = common.limited(items, req)
return dict(flavors=items)
def show(self, req, id):
diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py
index 4a0a8e6f1..867ee5a7e 100644
--- a/nova/api/openstack/images.py
+++ b/nova/api/openstack/images.py
@@ -22,12 +22,73 @@ from nova import utils
from nova import wsgi
import nova.api.openstack
import nova.image.service
-from nova.api.openstack import faults
+from nova.api.openstack import common
+from nova.api.openstack import faults
+from nova.compute import api as compute_api
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'}
+ item['status'] = status_mapping[item['status']]
+ return item
+
+
+def _filter_keys(item, keys):
+ """
+ Filters all model attributes except for keys
+ item is a dict
+
+ """
+ return dict((k, v) for k, v in item.iteritems() if k in keys)
+
+
class Controller(wsgi.Controller):
_serialization_metadata = {
@@ -40,24 +101,25 @@ class Controller(wsgi.Controller):
self._service = utils.import_object(FLAGS.image_service)
def index(self, req):
- """Return all public images in brief."""
- return dict(images=[dict(id=img['id'], name=img['name'])
- for img in self.detail(req)['images']])
+ """Return all public images in brief"""
+ items = self._service.index(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_filter_keys(item, ('id', 'name')) for item in items]
+ return dict(images=items)
def detail(self, req):
- """Return all public images in detail."""
+ """Return all public images in detail"""
try:
- images = self._service.detail(req.environ['nova.context'])
- images = nova.api.openstack.limited(images, req)
+ items = self._service.detail(req.environ['nova.context'])
except NotImplementedError:
- # Emulate detail() using repeated calls to show()
- images = self._service.index(ctxt)
- images = nova.api.openstack.limited(images, req)
- images = [self._service.show(ctxt, i['id']) for i in images]
- return dict(images=images)
+ items = self._service.index(req.environ['nova.context'])
+ items = common.limited(items, req)
+ items = [_translate_keys(item) for item in items]
+ items = [_translate_status(item) for item in items]
+ return dict(images=items)
def show(self, req, id):
- """Return data about the given image id."""
+ """Return data about the given image id"""
return dict(image=self._service.show(req.environ['nova.context'], id))
def delete(self, req, id):
@@ -65,9 +127,11 @@ class Controller(wsgi.Controller):
raise faults.Fault(exc.HTTPNotFound())
def create(self, req):
- # Only public images are supported for now, so a request to
- # make a backup of a server cannot be supproted.
- raise faults.Fault(exc.HTTPNotFound())
+ context = req.environ['nova.context']
+ env = self._deserialize(req.body, req)
+ instance_id = env["image"]["serverId"]
+ name = env["image"]["name"]
+ return compute_api.ComputeAPI().snapshot(context, instance_id, name)
def update(self, req, id):
# Users may not modify public images, and that's all that
diff --git a/nova/api/openstack/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py
index 918caf055..81b83142f 100644
--- a/nova/api/openstack/ratelimiting/__init__.py
+++ b/nova/api/openstack/ratelimiting/__init__.py
@@ -1,3 +1,20 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 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
+
"""Rate limiting of arbitrary actions."""
import httplib
@@ -6,6 +23,8 @@ import urllib
import webob.dec
import webob.exc
+from nova import wsgi
+from nova.api.openstack import faults
# Convenience constants for the limits dictionary passed to Limiter().
PER_SECOND = 1
@@ -14,6 +33,83 @@ PER_HOUR = 60 * 60
PER_DAY = 60 * 60 * 24
+class RateLimitingMiddleware(wsgi.Middleware):
+ """Rate limit incoming requests according to the OpenStack rate limits."""
+
+ def __init__(self, application, service_host=None):
+ """Create a rate limiting middleware that wraps the given application.
+
+ By default, rate counters are stored in memory. If service_host is
+ specified, the middleware instead relies on the ratelimiting.WSGIApp
+ at the given host+port to keep rate counters.
+ """
+ if not service_host:
+ #TODO(gundlach): These limits were based on limitations of Cloud
+ #Servers. We should revisit them in Nova.
+ self.limiter = Limiter(limits={
+ 'DELETE': (100, PER_MINUTE),
+ 'PUT': (10, PER_MINUTE),
+ 'POST': (10, PER_MINUTE),
+ 'POST servers': (50, PER_DAY),
+ 'GET changes-since': (3, PER_MINUTE),
+ })
+ else:
+ self.limiter = WSGIAppProxy(service_host)
+ super(RateLimitingMiddleware, self).__init__(application)
+
+ @webob.dec.wsgify
+ def __call__(self, req):
+ """Rate limit the request.
+
+ If the request should be rate limited, return a 413 status with a
+ Retry-After header giving the time when the request would succeed.
+ """
+ return self.rate_limited_request(req, self.application)
+
+ def rate_limited_request(self, req, application):
+ """Rate limit the request.
+
+ If the request should be rate limited, return a 413 status with a
+ Retry-After header giving the time when the request would succeed.
+ """
+ action_name = self.get_action_name(req)
+ if not action_name:
+ # Not rate limited
+ return application
+ delay = self.get_delay(action_name,
+ req.environ['nova.context'].user_id)
+ if delay:
+ # TODO(gundlach): Get the retry-after format correct.
+ exc = webob.exc.HTTPRequestEntityTooLarge(
+ explanation=('Too many requests.'),
+ headers={'Retry-After': time.time() + delay})
+ raise faults.Fault(exc)
+ return application
+
+ def get_delay(self, action_name, username):
+ """Return the delay for the given action and username, or None if
+ the action would not be rate limited.
+ """
+ if action_name == 'POST servers':
+ # "POST servers" is a POST, so it counts against "POST" too.
+ # Attempt the "POST" first, lest we are rate limited by "POST" but
+ # use up a precious "POST servers" call.
+ delay = self.limiter.perform("POST", username=username)
+ if delay:
+ return delay
+ return self.limiter.perform(action_name, username=username)
+
+ def get_action_name(self, req):
+ """Return the action name for this request."""
+ if req.method == 'GET' and 'changes-since' in req.GET:
+ return 'GET changes-since'
+ if req.method == 'POST' and req.path_info.startswith('/servers'):
+ return 'POST servers'
+ if req.method in ['PUT', 'POST', 'DELETE']:
+ return req.method
+ return None
+
+
class Limiter(object):
"""Class providing rate limiting of arbitrary actions."""
@@ -123,3 +219,9 @@ class WSGIAppProxy(object):
# No delay
return None
return float(resp.getheader('X-Wait-Seconds'))
+
+
+def ratelimit_factory(global_conf, **local_conf):
+ def rl(app):
+ return RateLimitingMiddleware(app)
+ return rl
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 7704f48f1..c5cbe21ef 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -15,10 +15,14 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
+import traceback
+
from webob import exc
from nova import exception
from nova import wsgi
+from nova.api.openstack import common
from nova.api.openstack import faults
from nova.auth import manager as auth_manager
from nova.compute import api as compute_api
@@ -27,18 +31,20 @@ from nova.compute import power_state
import nova.api.openstack
-def _entity_list(entities):
- """ Coerces a list of servers into proper dictionary format """
- return dict(servers=entities)
+LOG = logging.getLogger('server')
+LOG.setLevel(logging.DEBUG)
-def _entity_detail(inst):
- """ Maps everything to Rackspace-like attributes for return"""
+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.PAUSED: 'suspended',
+ power_state.SUSPENDED: 'suspended',
+ power_state.PAUSED: 'error',
power_state.SHUTDOWN: 'active',
power_state.SHUTOFF: 'active',
power_state.CRASHED: 'error'}
@@ -58,8 +64,9 @@ def _entity_detail(inst):
return dict(server=inst_dict)
-def _entity_inst(inst):
- """ Filters all model attributes save for id and name """
+def _translate_keys(inst):
+ """ Coerces into dictionary format, excluding all model attributes
+ save for id and name """
return dict(server=dict(id=inst['internal_id'], name=inst['display_name']))
@@ -78,29 +85,29 @@ class Controller(wsgi.Controller):
def index(self, req):
""" Returns a list of server names and ids for a given user """
- return self._items(req, entity_maker=_entity_inst)
+ return self._items(req, entity_maker=_translate_keys)
def detail(self, req):
""" Returns a list of server details for a given user """
- return self._items(req, entity_maker=_entity_detail)
+ return self._items(req, entity_maker=_translate_detail_keys)
def _items(self, req, entity_maker):
"""Returns a list of servers for a given user.
- entity_maker - either _entity_detail or _entity_inst
+ entity_maker - either _translate_detail_keys or _translate_keys
"""
instance_list = self.compute_api.get_instances(
req.environ['nova.context'])
- limited_list = nova.api.openstack.limited(instance_list, req)
+ limited_list = common.limited(instance_list, req)
res = [entity_maker(inst)['server'] for inst in limited_list]
- return _entity_list(res)
+ return dict(servers=res)
def show(self, req, id):
""" Returns server details by server id """
try:
instance = self.compute_api.get_instance(
req.environ['nova.context'], int(id))
- return _entity_detail(instance)
+ return _translate_detail_keys(instance)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -129,7 +136,7 @@ class Controller(wsgi.Controller):
description=env['server']['name'],
key_name=key_pair['name'],
key_data=key_pair['public_key'])
- return _entity_inst(instances[0])
+ return _translate_keys(instances[0])
def update(self, req, id):
""" Updates the server name or password """
@@ -144,8 +151,9 @@ class Controller(wsgi.Controller):
update_dict['display_name'] = inst_dict['server']['name']
try:
- self.compute_api.update_instance(req.environ['nova.context'],
- instance['id'],
+ ctxt = req.environ['nova.context']
+ self.compute_api.update_instance(ctxt,
+ id,
**update_dict)
except exception.NotFound:
return faults.Fault(exc.HTTPNotFound())
@@ -166,3 +174,57 @@ class Controller(wsgi.Controller):
except:
return faults.Fault(exc.HTTPUnprocessableEntity())
return exc.HTTPAccepted()
+
+ def pause(self, req, id):
+ """ Permit Admins to Pause the server. """
+ ctxt = req.environ['nova.context']
+ try:
+ self.compute_api.pause(ctxt, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("Compute.api::pause %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def unpause(self, req, id):
+ """ Permit Admins to Unpause the server. """
+ ctxt = req.environ['nova.context']
+ try:
+ self.compute_api.unpause(ctxt, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("Compute.api::unpause %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def suspend(self, req, id):
+ """permit admins to suspend the server"""
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.suspend(context, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("compute.api::suspend %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def resume(self, req, id):
+ """permit admins to resume the server from suspend"""
+ context = req.environ['nova.context']
+ try:
+ self.compute_api.resume(context, id)
+ except:
+ readable = traceback.format_exc()
+ logging.error(_("compute.api::resume %s"), readable)
+ return faults.Fault(exc.HTTPUnprocessableEntity())
+ return exc.HTTPAccepted()
+
+ def diagnostics(self, req, id):
+ """Permit Admins to retrieve server diagnostics."""
+ ctxt = req.environ["nova.context"]
+ return self.compute_api.get_diagnostics(ctxt, id)
+
+ def actions(self, req, id):
+ """Permit Admins to retrieve server actions."""
+ ctxt = req.environ["nova.context"]
+ return self.compute_api.get_actions(ctxt, id)
diff --git a/nova/api/openstack/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py
index e805ca9f7..845f5bead 100644
--- a/nova/api/openstack/sharedipgroups.py
+++ b/nova/api/openstack/sharedipgroups.py
@@ -15,8 +15,51 @@
# License for the specific language governing permissions and limitations
# under the License.
+from webob import exc
+
from nova import wsgi
+from nova.api.openstack import faults
+
+
+def _translate_keys(inst):
+ """ Coerces a shared IP group instance into proper dictionary format """
+ return dict(sharedIpGroup=inst)
+
+
+def _translate_detail_keys(inst):
+ """ Coerces a shared IP group instance into proper dictionary format with
+ correctly mapped attributes """
+ return dict(sharedIpGroup=inst)
class Controller(wsgi.Controller):
- pass
+ """ The Shared IP Groups Controller for the Openstack API """
+
+ _serialization_metadata = {
+ 'application/xml': {
+ 'attributes': {
+ 'sharedIpGroup': []}}}
+
+ def index(self, req):
+ """ Returns a list of Shared IP Groups for the user """
+ return dict(sharedIpGroups=[])
+
+ def show(self, req, id):
+ """ Shows in-depth information on a specific Shared IP Group """
+ return _translate_keys({})
+
+ def update(self, req, id):
+ """ You can't update a Shared IP Group """
+ raise faults.Fault(exc.HTTPNotImplemented())
+
+ def delete(self, req, id):
+ """ Deletes a Shared IP Group """
+ raise faults.Fault(exc.HTTPNotFound())
+
+ def detail(self, req, id):
+ """ Returns a complete list of Shared IP Groups """
+ return _translate_detail_keys({})
+
+ def create(self, req):
+ """ Creates a new Shared IP group """
+ raise faults.Fault(exc.HTTPNotFound())