summaryrefslogtreecommitdiffstats
path: root/nova/api
diff options
context:
space:
mode:
authorVishvananda Ishaya <vishvananda@gmail.com>2010-12-22 21:50:58 +0000
committerVishvananda Ishaya <vishvananda@gmail.com>2010-12-22 21:50:58 +0000
commit440978ce046a73acede887117e9812f1a6a026fa (patch)
treeb0afd90999bf88f4efa8a42f5496cacd1561278a /nova/api
parentca1017988f98a246aa82c16f471791c7ea3eceec (diff)
parenteb64fe72160ca7c68809eaf0af91768f4eb5d8e8 (diff)
downloadnova-440978ce046a73acede887117e9812f1a6a026fa.tar.gz
nova-440978ce046a73acede887117e9812f1a6a026fa.tar.xz
nova-440978ce046a73acede887117e9812f1a6a026fa.zip
merged trunk and fixed conflicts
Diffstat (limited to 'nova/api')
-rw-r--r--nova/api/__init__.py4
-rw-r--r--nova/api/cloudpipe/__init__.py69
-rw-r--r--nova/api/ec2/__init__.py73
-rw-r--r--nova/api/ec2/admin.py1
-rw-r--r--nova/api/ec2/apirequest.py4
-rw-r--r--nova/api/ec2/cloud.py71
-rw-r--r--nova/api/ec2/metadatarequesthandler.py2
-rw-r--r--nova/api/openstack/__init__.py17
-rw-r--r--nova/api/openstack/backup_schedules.py1
-rw-r--r--nova/api/openstack/servers.py29
10 files changed, 153 insertions, 118 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py
index 80f9f2109..803470570 100644
--- a/nova/api/__init__.py
+++ b/nova/api/__init__.py
@@ -29,9 +29,7 @@ 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 +39,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 +79,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 c455144a9..51d33bcc6 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
@@ -40,16 +40,79 @@ 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):
@@ -80,7 +143,7 @@ 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!
@@ -126,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
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 ebb13aedc..e1a21f122 100644
--- a/nova/api/ec2/cloud.py
+++ b/nova/api/ec2/cloud.py
@@ -114,7 +114,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):
@@ -196,15 +196,19 @@ class CloudController(object):
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,
@@ -318,11 +322,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
@@ -360,7 +364,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
@@ -371,7 +376,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
@@ -387,8 +392,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)
@@ -416,7 +421,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,
@@ -529,13 +534,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']
@@ -557,10 +562,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,
@@ -689,10 +694,11 @@ 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")
+ raise quota.QuotaError(_("Address quota exceeded. You cannot "
+ "allocate any more addresses"))
network_topic = self._get_network_topic(context)
public_ip = rpc.call(context,
network_topic,
@@ -751,11 +757,12 @@ 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'),
generate_hostname=internal_id_to_ec2_id)
return self._format_run_instances(context,
@@ -805,7 +812,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})
@@ -836,11 +843,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'})
@@ -850,13 +858,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 66439a0df..f832863a9 100644
--- a/nova/api/ec2/metadatarequesthandler.py
+++ b/nova/api/ec2/metadatarequesthandler.py
@@ -72,7 +72,7 @@ class MetadataRequestHandler(object):
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' %
+ logging.error(_('Failed to get metadata for ip: %s') %
remote_address)
raise webob.exc.HTTPNotFound()
data = self.lookup(req.path_info, meta_data)
diff --git a/nova/api/openstack/__init__.py b/nova/api/openstack/__init__.py
index b9ecbd9b8..de95ee548 100644
--- a/nova/api/openstack/__init__.py
+++ b/nova/api/openstack/__init__.py
@@ -66,7 +66,7 @@ class API(wsgi.Middleware):
try:
return req.get_response(self.application)
except Exception as ex:
- logging.warn("Caught error: %s" % str(ex))
+ logging.warn(_("Caught error: %s") % str(ex))
logging.debug(traceback.format_exc())
exc = webob.exc.HTTPInternalServerError(explanation=str(ex))
return faults.Fault(exc)
@@ -133,7 +133,7 @@ class RateLimitingMiddleware(wsgi.Middleware):
if delay:
# TODO(gundlach): Get the retry-after format correct.
exc = webob.exc.HTTPRequestEntityTooLarge(
- explanation='Too many requests.',
+ explanation=_('Too many requests.'),
headers={'Retry-After': time.time() + delay})
raise faults.Fault(exc)
return self.application
@@ -170,9 +170,16 @@ 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'
+
mapper.resource("server", "servers", controller=servers.Controller(),
collection={'detail': 'GET'},
- member={'action': 'POST'})
+ member=server_members)
mapper.resource("backup_schedule", "backup_schedules",
controller=backup_schedules.Controller(),
@@ -186,10 +193,6 @@ 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)
diff --git a/nova/api/openstack/backup_schedules.py b/nova/api/openstack/backup_schedules.py
index 3ed691d7b..fc70b5c6c 100644
--- a/nova/api/openstack/backup_schedules.py
+++ b/nova/api/openstack/backup_schedules.py
@@ -24,6 +24,7 @@ import nova.image.service
class Controller(wsgi.Controller):
+
def __init__(self):
pass
diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py
index 7704f48f1..5c3322f7c 100644
--- a/nova/api/openstack/servers.py
+++ b/nova/api/openstack/servers.py
@@ -15,6 +15,9 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
+import traceback
+
from webob import exc
from nova import exception
@@ -27,6 +30,10 @@ from nova.compute import power_state
import nova.api.openstack
+LOG = logging.getLogger('server')
+LOG.setLevel(logging.DEBUG)
+
+
def _entity_list(entities):
""" Coerces a list of servers into proper dictionary format """
return dict(servers=entities)
@@ -166,3 +173,25 @@ 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()