diff options
| author | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-10-12 19:02:04 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-10-12 19:02:04 -0700 |
| commit | c829e39c5436f2c9f075713ff4b80dbdfb1239ef (patch) | |
| tree | f651d48b531cbc1b0d39df7a956cc75675be617c /nova/api | |
| parent | 1e5724e7bb7e0aa3e441ec5e017430dd7c07315b (diff) | |
| parent | d0a55238fdf64a8da51ea1fe328a1a3dc3d17dc7 (diff) | |
merged trunk
Diffstat (limited to 'nova/api')
| -rw-r--r-- | nova/api/__init__.py | 30 | ||||
| -rw-r--r-- | nova/api/cloud.py | 2 | ||||
| -rw-r--r-- | nova/api/ec2/__init__.py | 2 | ||||
| -rw-r--r-- | nova/api/ec2/cloud.py | 293 | ||||
| -rw-r--r-- | nova/api/ec2/images.py | 3 | ||||
| -rw-r--r-- | nova/api/openstack/__init__.py (renamed from nova/api/rackspace/__init__.py) | 26 | ||||
| -rw-r--r-- | nova/api/openstack/_id_translator.py (renamed from nova/api/rackspace/_id_translator.py) | 0 | ||||
| -rw-r--r-- | nova/api/openstack/auth.py (renamed from nova/api/rackspace/auth.py) | 4 | ||||
| -rw-r--r-- | nova/api/openstack/backup_schedules.py (renamed from nova/api/rackspace/backup_schedules.py) | 3 | ||||
| -rw-r--r-- | nova/api/openstack/context.py (renamed from nova/api/rackspace/context.py) | 0 | ||||
| -rw-r--r-- | nova/api/openstack/faults.py (renamed from nova/api/rackspace/faults.py) | 0 | ||||
| -rw-r--r-- | nova/api/openstack/flavors.py (renamed from nova/api/rackspace/flavors.py) | 8 | ||||
| -rw-r--r-- | nova/api/openstack/images.py (renamed from nova/api/rackspace/images.py) | 23 | ||||
| -rw-r--r-- | nova/api/openstack/notes.txt (renamed from nova/api/rackspace/notes.txt) | 4 | ||||
| -rw-r--r-- | nova/api/openstack/ratelimiting/__init__.py (renamed from nova/api/rackspace/ratelimiting/__init__.py) | 0 | ||||
| -rw-r--r-- | nova/api/openstack/servers.py (renamed from nova/api/rackspace/servers.py) | 73 | ||||
| -rw-r--r-- | nova/api/openstack/sharedipgroups.py (renamed from nova/api/rackspace/sharedipgroups.py) | 0 | ||||
| -rw-r--r-- | nova/api/rackspace/ratelimiting/tests.py | 237 |
18 files changed, 348 insertions, 360 deletions
diff --git a/nova/api/__init__.py b/nova/api/__init__.py index 744abd621..8ec7094d7 100644 --- a/nova/api/__init__.py +++ b/nova/api/__init__.py @@ -27,16 +27,16 @@ from nova import flags from nova import wsgi from nova.api import cloudpipe from nova.api import ec2 -from nova.api import rackspace +from nova.api import openstack from nova.api.ec2 import metadatarequesthandler -flags.DEFINE_string('rsapi_subdomain', 'rs', - 'subdomain running the RS API') +flags.DEFINE_string('osapi_subdomain', 'api', + 'subdomain running the OpenStack API') flags.DEFINE_string('ec2api_subdomain', 'ec2', 'subdomain running the EC2 API') flags.DEFINE_string('FAKE_subdomain', None, - 'set to rs or ec2 to fake the subdomain of the host for testing') + 'set to api or ec2 to fake the subdomain of the host for testing') FLAGS = flags.FLAGS @@ -44,21 +44,21 @@ class API(wsgi.Router): """Routes top-level requests to the appropriate controller.""" def __init__(self): - rsdomain = {'sub_domain': [FLAGS.rsapi_subdomain]} + osapidomain = {'sub_domain': [FLAGS.osapi_subdomain]} ec2domain = {'sub_domain': [FLAGS.ec2api_subdomain]} - # If someone wants to pretend they're hitting the RS subdomain - # on their local box, they can set FAKE_subdomain to 'rs', which - # removes subdomain restrictions from the RS routes below. - if FLAGS.FAKE_subdomain == 'rs': - rsdomain = {} + # If someone wants to pretend they're hitting the OSAPI subdomain + # on their local box, they can set FAKE_subdomain to 'api', which + # removes subdomain restrictions from the OpenStack API routes below. + if FLAGS.FAKE_subdomain == 'api': + osapidomain = {} elif FLAGS.FAKE_subdomain == 'ec2': ec2domain = {} mapper = routes.Mapper() mapper.sub_domains = True - mapper.connect("/", controller=self.rsapi_versions, - conditions=rsdomain) - mapper.connect("/v1.0/{path_info:.*}", controller=rackspace.API(), - conditions=rsdomain) + mapper.connect("/", controller=self.osapi_versions, + conditions=osapidomain) + mapper.connect("/v1.0/{path_info:.*}", controller=openstack.API(), + conditions=osapidomain) mapper.connect("/", controller=self.ec2api_versions, conditions=ec2domain) @@ -81,7 +81,7 @@ class API(wsgi.Router): super(API, self).__init__(mapper) @webob.dec.wsgify - def rsapi_versions(self, req): + def osapi_versions(self, req): """Respond to a request for all OpenStack API versions.""" response = { "versions": [ diff --git a/nova/api/cloud.py b/nova/api/cloud.py index 345677d4f..57e94a17a 100644 --- a/nova/api/cloud.py +++ b/nova/api/cloud.py @@ -34,7 +34,7 @@ def reboot(instance_id, context=None): #TODO(gundlach) not actually sure what context is used for by ec2 here -- I think we can just remove it and use None all the time. """ - instance_ref = db.instance_get_by_ec2_id(None, instance_id) + instance_ref = db.instance_get_by_internal_id(None, instance_id) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "reboot_instance", diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index 6b538a7f1..6e771f064 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -142,6 +142,8 @@ class Authorizer(wsgi.Middleware): 'CreateKeyPair': ['all'], 'DeleteKeyPair': ['all'], 'DescribeSecurityGroups': ['all'], + 'AuthorizeSecurityGroupIngress': ['netadmin'], + 'RevokeSecurityGroupIngress': ['netadmin'], 'CreateSecurityGroup': ['netadmin'], 'DeleteSecurityGroup': ['netadmin'], 'GetConsoleOutput': ['projectmanager', 'sysadmin'], diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 79c95788b..7839dc92c 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -28,6 +28,8 @@ import logging import os import time +import IPy + from nova import crypto from nova import db from nova import exception @@ -43,6 +45,7 @@ from nova.api.ec2 import images FLAGS = flags.FLAGS flags.DECLARE('storage_availability_zone', 'nova.volume.manager') +InvalidInputException = exception.InvalidInputException class QuotaError(exception.ApiError): """Quota Exceeeded""" @@ -72,6 +75,20 @@ def _gen_key(context, user_id, key_name): return {'private_key': private_key, 'fingerprint': fingerprint} +def ec2_id_to_internal_id(ec2_id): + """Convert an ec2 ID (i-[base 36 number]) to an internal id (int)""" + return int(ec2_id[2:], 36) + + +def internal_id_to_ec2_id(internal_id): + """Convert an internal ID (int) to an ec2 ID (i-[base 36 number])""" + digits = [] + while internal_id != 0: + internal_id, remainder = divmod(internal_id, 36) + digits.append('0123456789abcdefghijklmnopqrstuvwxyz'[remainder]) + return "i-%s" % ''.join(reversed(digits)) + + class CloudController(object): """ CloudController provides the critical dispatch between inbound API calls through the endpoint and messages @@ -113,6 +130,15 @@ class CloudController(object): result[key] = [line] return result + def _trigger_refresh_security_group(self, security_group): + nodes = set([instance['host'] for instance in security_group.instances + if instance['host'] is not None]) + for node in nodes: + rpc.call('%s.%s' % (FLAGS.compute_topic, node), + { "method": "refresh_security_group", + "args": { "context": None, + "security_group_id": security_group.id}}) + def get_metadata(self, address): instance_ref = db.fixed_ip_get_instance(None, address) if instance_ref is None: @@ -144,7 +170,7 @@ class CloudController(object): }, 'hostname': hostname, 'instance-action': 'none', - 'instance-id': instance_ref['ec2_id'], + 'instance-id': internal_id_to_ec2_id(instance_ref['internal_id']), 'instance-type': instance_ref['instance_type'], 'local-hostname': hostname, 'local-ipv4': address, @@ -232,21 +258,200 @@ class CloudController(object): pass return True - def describe_security_groups(self, context, group_names, **kwargs): - groups = {'securityGroupSet': []} + def describe_security_groups(self, context, group_name=None, **kwargs): + self._ensure_default_security_group(context) + if context.user.is_admin(): + groups = db.security_group_get_all(context) + else: + groups = db.security_group_get_by_project(context, + context.project.id) + groups = [self._format_security_group(context, g) for g in groups] + if not group_name is None: + groups = [g for g in groups if g.name in group_name] + + return {'securityGroupInfo': groups } + + def _format_security_group(self, context, group): + g = {} + g['groupDescription'] = group.description + g['groupName'] = group.name + g['ownerId'] = group.project_id + g['ipPermissions'] = [] + for rule in group.rules: + r = {} + r['ipProtocol'] = rule.protocol + r['fromPort'] = rule.from_port + r['toPort'] = rule.to_port + r['groups'] = [] + r['ipRanges'] = [] + if rule.group_id: + source_group = db.security_group_get(context, rule.group_id) + r['groups'] += [{'groupName': source_group.name, + 'userId': source_group.project_id}] + else: + r['ipRanges'] += [{'cidrIp': rule.cidr}] + g['ipPermissions'] += [r] + return g + + + def _authorize_revoke_rule_args_to_dict(self, context, + to_port=None, from_port=None, + ip_protocol=None, cidr_ip=None, + user_id=None, + source_security_group_name=None, + source_security_group_owner_id=None): + + values = {} + + if source_security_group_name: + source_project_id = self._get_source_project_id(context, + source_security_group_owner_id) + + source_security_group = \ + db.security_group_get_by_name(context, + source_project_id, + source_security_group_name) + values['group_id'] = source_security_group['id'] + elif cidr_ip: + # If this fails, it throws an exception. This is what we want. + IPy.IP(cidr_ip) + values['cidr'] = cidr_ip + else: + values['cidr'] = '0.0.0.0/0' + + if ip_protocol and from_port and to_port: + from_port = int(from_port) + to_port = int(to_port) + ip_protocol = str(ip_protocol) + + if ip_protocol.upper() not in ['TCP','UDP','ICMP']: + 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') + + values['protocol'] = ip_protocol + values['from_port'] = from_port + values['to_port'] = to_port + else: + # If cidr based filtering, protocol and ports are mandatory + if 'cidr' in values: + return None + + return values - # Stubbed for now to unblock other things. - return groups - def create_security_group(self, context, group_name, **kwargs): + def _security_group_rule_exists(self, security_group, values): + """Indicates whether the specified rule values are already + defined in the given security group. + """ + for rule in security_group.rules: + if 'group_id' in values: + if rule['group_id'] == values['group_id']: + return True + else: + is_duplicate = True + for key in ('cidr', 'from_port', 'to_port', 'protocol'): + if rule[key] != values[key]: + is_duplicate = False + break + if is_duplicate: + return True + return False + + + def revoke_security_group_ingress(self, context, group_name, **kwargs): + self._ensure_default_security_group(context) + security_group = db.security_group_get_by_name(context, + context.project.id, + group_name) + + criteria = self._authorize_revoke_rule_args_to_dict(context, **kwargs) + if criteria == None: + raise exception.ApiError("No rule for the specified parameters.") + + for rule in security_group.rules: + match = True + for (k,v) in criteria.iteritems(): + if getattr(rule, k, False) != v: + match = False + if match: + db.security_group_rule_destroy(context, rule['id']) + self._trigger_refresh_security_group(security_group) + return True + 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 + # for these operations, so support for newer API versions + # is sketchy. + def authorize_security_group_ingress(self, context, group_name, **kwargs): + self._ensure_default_security_group(context) + security_group = db.security_group_get_by_name(context, + context.project.id, + group_name) + + values = self._authorize_revoke_rule_args_to_dict(context, **kwargs) + 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) + + security_group_rule = db.security_group_rule_create(context, values) + + self._trigger_refresh_security_group(security_group) + return True + + def _get_source_project_id(self, context, source_security_group_owner_id): + if source_security_group_owner_id: + # Parse user:project for source group. + source_parts = source_security_group_owner_id.split(':') + + # If no project name specified, assume it's same as user name. + # Since we're looking up by project name, the user name is not + # used here. It's only read for EC2 API compatibility. + if len(source_parts) == 2: + source_project_id = source_parts[1] + else: + source_project_id = source_parts[0] + else: + source_project_id = context.project.id + + return source_project_id + + + def create_security_group(self, context, group_name, group_description): + self._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) + + group = {'user_id' : context.user.id, + 'project_id': context.project.id, + 'name': group_name, + 'description': group_description} + group_ref = db.security_group_create(context, group) + + return {'securityGroupSet': [self._format_security_group(context, + group_ref)]} + + def delete_security_group(self, context, group_name, **kwargs): + security_group = db.security_group_get_by_name(context, + context.project.id, + group_name) + db.security_group_destroy(context, security_group.id) return True + def get_console_output(self, context, instance_id, **kwargs): # instance_id is passed in as a list of instances - instance_ref = db.instance_get_by_ec2_id(context, instance_id[0]) + ec2_id = instance_id[0] + internal_id = ec2_id_to_internal_id(ec2_id) + instance_ref = db.instance_get_by_internal_id(context, internal_id) return rpc.call('%s.%s' % (FLAGS.compute_topic, instance_ref['host']), {"method": "get_console_output", @@ -326,7 +531,8 @@ class CloudController(object): raise exception.ApiError("Volume status must be available") if volume_ref['attach_status'] == "attached": raise exception.ApiError("Volume is already attached") - instance_ref = db.instance_get_by_ec2_id(context, instance_id) + internal_id = ec2_id_to_internal_id(instance_id) + instance_ref = db.instance_get_by_internal_id(context, internal_id) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "attach_volume", @@ -360,9 +566,11 @@ class CloudController(object): # If the instance doesn't exist anymore, # then we need to call detach blind db.volume_detached(context) + internal_id = instance_ref['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) return {'attachTime': volume_ref['attach_time'], 'device': volume_ref['mountpoint'], - 'instanceId': instance_ref['ec2_id'], + 'instanceId': internal_id, 'requestId': context.request_id, 'status': volume_ref['attach_status'], 'volumeId': volume_ref['id']} @@ -411,7 +619,9 @@ class CloudController(object): if instance['image_id'] == FLAGS.vpn_image_id: continue i = {} - i['instanceId'] = instance['ec2_id'] + internal_id = instance['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) + i['instanceId'] = ec2_id i['imageId'] = instance['image_id'] i['instanceState'] = { 'code': instance['state'], @@ -464,9 +674,10 @@ class CloudController(object): instance_id = None if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): - instance_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] + internal_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] + ec2_id = internal_id_to_ec2_id(internal_id) address_rv = {'public_ip': address, - 'instance_id': instance_id} + 'instance_id': ec2_id} if context.user.is_admin(): details = "%s (%s)" % (address_rv['instance_id'], floating_ip_ref['project_id']) @@ -498,8 +709,9 @@ class CloudController(object): "floating_address": floating_ip_ref['address']}}) return {'releaseResponse': ["Address released."]} - def associate_address(self, context, instance_id, public_ip, **kwargs): - instance_ref = db.instance_get_by_ec2_id(context, instance_id) + def associate_address(self, context, ec2_id, public_ip, **kwargs): + internal_id = ec2_id_to_internal_id(ec2_id) + instance_ref = db.instance_get_by_internal_id(context, internal_id) fixed_address = db.instance_get_fixed_address(context, instance_ref['id']) floating_ip_ref = db.floating_ip_get_by_address(context, public_ip) @@ -531,6 +743,18 @@ class CloudController(object): "project_id": context.project.id}}) return db.queue_get_for(context, FLAGS.network_topic, host) + def _ensure_default_security_group(self, context): + try: + db.security_group_get_by_name(context, + context.project.id, + 'default') + except exception.NotFound: + values = { 'name' : 'default', + 'description' : 'default', + 'user_id' : context.user.id, + 'project_id' : context.project.id } + group = db.security_group_create(context, values) + def run_instances(self, context, **kwargs): instance_type = kwargs.get('instance_type', 'm1.small') if instance_type not in INSTANCE_TYPES: @@ -578,8 +802,17 @@ class CloudController(object): kwargs['key_name']) key_data = key_pair_ref['public_key'] - # TODO: Get the real security group of launch in here - security_group = "default" + security_group_arg = kwargs.get('security_group', ["default"]) + if not type(security_group_arg) is list: + security_group_arg = [security_group_arg] + + security_groups = [] + self._ensure_default_security_group(context) + for security_group_name in security_group_arg: + group = db.security_group_get_by_name(context, + context.project.id, + security_group_name) + security_groups.append(group['id']) reservation_id = utils.generate_uid('r') base_options = {} @@ -593,12 +826,12 @@ class CloudController(object): base_options['user_id'] = context.user.id base_options['project_id'] = context.project.id base_options['user_data'] = kwargs.get('user_data', '') - base_options['security_group'] = security_group - base_options['instance_type'] = instance_type + base_options['display_name'] = kwargs.get('display_name') base_options['display_description'] = kwargs.get('display_description') type_data = INSTANCE_TYPES[instance_type] + base_options['instance_type'] = instance_type base_options['memory_mb'] = type_data['memory_mb'] base_options['vcpus'] = type_data['vcpus'] base_options['local_gb'] = type_data['local_gb'] @@ -607,10 +840,16 @@ class CloudController(object): instance_ref = db.instance_create(context, base_options) inst_id = instance_ref['id'] + for security_group_id in security_groups: + db.instance_add_security_group(context, inst_id, + security_group_id) + inst = {} inst['mac_address'] = utils.generate_mac() inst['launch_index'] = num - inst['hostname'] = instance_ref['ec2_id'] + internal_id = instance_ref['internal_id'] + ec2_id = internal_id_to_ec2_id(internal_id) + inst['hostname'] = ec2_id db.instance_update(context, inst_id, inst) address = self.network_manager.allocate_fixed_ip(context, inst_id, @@ -635,11 +874,18 @@ class CloudController(object): def terminate_instances(self, context, instance_id, **kwargs): + """Terminate each instance in instance_id, which is a list of ec2 ids. + + instance_id is a kwarg so its name cannot be modified. + """ + ec2_id_list = instance_id logging.debug("Going to start terminating instances") - for id_str in instance_id: + for id_str in ec2_id_list: + internal_id = ec2_id_to_internal_id(id_str) logging.debug("Going to try and terminate %s" % id_str) try: - instance_ref = db.instance_get_by_ec2_id(context, id_str) + instance_ref = db.instance_get_by_internal_id(context, + internal_id) except exception.NotFound: logging.warning("Instance %s was not found during terminate" % id_str) @@ -688,7 +934,7 @@ class CloudController(object): cloud.reboot(id_str, context=context) return True - def update_instance(self, context, instance_id, **kwargs): + def update_instance(self, context, ec2_id, **kwargs): updatable_fields = ['display_name', 'display_description'] changes = {} for field in updatable_fields: @@ -696,7 +942,8 @@ class CloudController(object): changes[field] = kwargs[field] if changes: db_context = {} - inst = db.instance_get_by_ec2_id(db_context, instance_id) + internal_id = ec2_id_to_internal_id(ec2_id) + inst = db.instance_get_by_internal_id(db_context, internal_id) db.instance_update(db_context, inst['id'], kwargs) return True diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index cb54cdda2..f0a43dad6 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -69,6 +69,9 @@ def list(context, filter_list=[]): optionally filtered by a list of image_id """ + if FLAGS.connection_type == 'fake': + return [{ 'imageId' : 'bar'}] + # FIXME: send along the list of only_images to check for response = conn(context).make_request( method='GET', diff --git a/nova/api/rackspace/__init__.py b/nova/api/openstack/__init__.py index 89a4693ad..5e81ba2bd 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/openstack/__init__.py @@ -17,7 +17,7 @@ # under the License. """ -WSGI middleware for Rackspace API controllers. +WSGI middleware for OpenStack API controllers. """ import json @@ -31,30 +31,30 @@ import webob from nova import flags from nova import utils from nova import wsgi -from nova.api.rackspace import faults -from nova.api.rackspace import backup_schedules -from nova.api.rackspace import flavors -from nova.api.rackspace import images -from nova.api.rackspace import ratelimiting -from nova.api.rackspace import servers -from nova.api.rackspace import sharedipgroups +from nova.api.openstack import faults +from nova.api.openstack import backup_schedules +from nova.api.openstack import flavors +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.rackspace.auth.BasicApiAuthManager', - 'The auth mechanism to use for the Rackspace API implemenation') + 'nova.api.openstack.auth.BasicApiAuthManager', + 'The auth mechanism to use for the OpenStack API implemenation') class API(wsgi.Middleware): - """WSGI entry point for all Rackspace API requests.""" + """WSGI entry point for all OpenStack API requests.""" def __init__(self): app = AuthMiddleware(RateLimitingMiddleware(APIRouter())) super(API, self).__init__(app) class AuthMiddleware(wsgi.Middleware): - """Authorize the rackspace API request or return an HTTP Forbidden.""" + """Authorize the openstack API request or return an HTTP Forbidden.""" def __init__(self, application): self.auth_driver = utils.import_class(FLAGS.nova_api_auth)() @@ -145,7 +145,7 @@ class RateLimitingMiddleware(wsgi.Middleware): class APIRouter(wsgi.Router): """ - Routes requests on the Rackspace API to the appropriate controller + Routes requests on the OpenStack API to the appropriate controller and method. """ diff --git a/nova/api/rackspace/_id_translator.py b/nova/api/openstack/_id_translator.py index 333aa8434..333aa8434 100644 --- a/nova/api/rackspace/_id_translator.py +++ b/nova/api/openstack/_id_translator.py diff --git a/nova/api/rackspace/auth.py b/nova/api/openstack/auth.py index c45156ebd..4c909293e 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/openstack/auth.py @@ -11,7 +11,7 @@ from nova import db from nova import flags from nova import manager from nova import utils -from nova.api.rackspace import faults +from nova.api.openstack import faults FLAGS = flags.FLAGS @@ -19,7 +19,7 @@ class Context(object): pass class BasicApiAuthManager(object): - """ Implements a somewhat rudimentary version of Rackspace Auth""" + """ Implements a somewhat rudimentary version of OpenStack Auth""" def __init__(self, host=None, db_driver=None): if not host: diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/openstack/backup_schedules.py index cb83023bc..76ad6ef87 100644 --- a/nova/api/rackspace/backup_schedules.py +++ b/nova/api/openstack/backup_schedules.py @@ -19,8 +19,7 @@ import time from webob import exc from nova import wsgi -from nova.api.rackspace import _id_translator -from nova.api.rackspace import faults +from nova.api.openstack import faults import nova.image.service class Controller(wsgi.Controller): diff --git a/nova/api/rackspace/context.py b/nova/api/openstack/context.py index 77394615b..77394615b 100644 --- a/nova/api/rackspace/context.py +++ b/nova/api/openstack/context.py diff --git a/nova/api/rackspace/faults.py b/nova/api/openstack/faults.py index 32e5c866f..32e5c866f 100644 --- a/nova/api/rackspace/faults.py +++ b/nova/api/openstack/faults.py diff --git a/nova/api/rackspace/flavors.py b/nova/api/openstack/flavors.py index 916449854..793984a5d 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/openstack/flavors.py @@ -17,13 +17,13 @@ from webob import exc -from nova.api.rackspace import faults +from nova.api.openstack import faults from nova.compute import instance_types from nova import wsgi -import nova.api.rackspace +import nova.api.openstack class Controller(wsgi.Controller): - """Flavor controller for the Rackspace API.""" + """Flavor controller for the OpenStack API.""" _serialization_metadata = { 'application/xml': { @@ -41,7 +41,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.rackspace.limited(items, req) + items = nova.api.openstack.limited(items, req) return dict(flavors=items) def show(self, req, id): diff --git a/nova/api/rackspace/images.py b/nova/api/openstack/images.py index 4a7dd489c..aa438739c 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/openstack/images.py @@ -17,11 +17,15 @@ from webob import exc +from nova import flags +from nova import utils from nova import wsgi -from nova.api.rackspace import _id_translator -import nova.api.rackspace +import nova.api.openstack import nova.image.service -from nova.api.rackspace import faults +from nova.api.openstack import faults + + +FLAGS = flags.FLAGS class Controller(wsgi.Controller): @@ -35,9 +39,7 @@ class Controller(wsgi.Controller): } def __init__(self): - self._service = nova.image.service.ImageService.load() - self._id_translator = _id_translator.RackspaceAPIIdTranslator( - "image", self._service.__class__.__name__) + self._service = utils.import_object(FLAGS.image_service) def index(self, req): """Return all public images in brief.""" @@ -47,17 +49,12 @@ class Controller(wsgi.Controller): def detail(self, req): """Return all public images in detail.""" data = self._service.index() - data = nova.api.rackspace.limited(data, req) - for img in data: - img['id'] = self._id_translator.to_rs_id(img['id']) + data = nova.api.openstack.limited(data, req) return dict(images=data) def show(self, req, id): """Return data about the given image id.""" - opaque_id = self._id_translator.from_rs_id(id) - img = self._service.show(opaque_id) - img['id'] = id - return dict(image=img) + return dict(image=self._service.show(id)) def delete(self, req, id): # Only public images are supported for now. diff --git a/nova/api/rackspace/notes.txt b/nova/api/openstack/notes.txt index e133bf5ea..2330f1002 100644 --- a/nova/api/rackspace/notes.txt +++ b/nova/api/openstack/notes.txt @@ -10,11 +10,11 @@ image ids are URIs. LocalImageService(ImageService): image ids are random strings. -RackspaceAPITranslationStore: +OpenstackAPITranslationStore: translates RS server/images/flavor/etc ids into formats required by a given ImageService strategy. -api.rackspace.images.Controller: +api.openstack.images.Controller: uses an ImageService strategy behind the scenes to do its fetching; it just converts int image id into a strategy-specific image id. diff --git a/nova/api/rackspace/ratelimiting/__init__.py b/nova/api/openstack/ratelimiting/__init__.py index f843bac0f..f843bac0f 100644 --- a/nova/api/rackspace/ratelimiting/__init__.py +++ b/nova/api/openstack/ratelimiting/__init__.py diff --git a/nova/api/rackspace/servers.py b/nova/api/openstack/servers.py index 11efd8aef..5d1ed9822 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/openstack/servers.py @@ -25,30 +25,15 @@ from nova import rpc from nova import utils from nova import wsgi from nova.api import cloud -from nova.api.rackspace import _id_translator -from nova.api.rackspace import context -from nova.api.rackspace import faults +from nova.api.openstack import context +from nova.api.openstack import faults from nova.compute import instance_types from nova.compute import power_state -import nova.api.rackspace +import nova.api.openstack import nova.image.service FLAGS = flags.FLAGS -flags.DEFINE_string('rs_network_manager', 'nova.network.manager.FlatManager', - 'Networking for rackspace') - -def _instance_id_translator(): - """ Helper method for initializing an id translator for Rackspace instance - ids """ - return _id_translator.RackspaceAPIIdTranslator( "instance", 'nova') - -def _image_service(): - """ Helper method for initializing the image id translator """ - service = nova.image.service.ImageService.load() - return (service, _id_translator.RackspaceAPIIdTranslator( - "image", service.__class__.__name__)) - def _filter_params(inst_dict): """ Extracts all updatable parameters for a server update request """ keys = dict(name='name', admin_pass='adminPass') @@ -63,7 +48,7 @@ def _entity_list(entities): return dict(servers=entities) def _entity_detail(inst): - """ Maps everything to Rackspace-like attributes for return""" + """ Maps everything to valid attributes for return""" power_mapping = { power_state.NOSTATE: 'build', power_state.RUNNING: 'active', @@ -93,7 +78,7 @@ def _entity_inst(inst): return dict(server=dict(id=inst['id'], name=inst['server_name'])) class Controller(wsgi.Controller): - """ The Server API controller for the Openstack API """ + """ The Server API controller for the OpenStack API """ _serialization_metadata = { 'application/xml': { @@ -125,17 +110,14 @@ class Controller(wsgi.Controller): """ user_id = req.environ['nova.context']['user']['id'] instance_list = self.db_driver.instance_get_all_by_user(None, user_id) - limited_list = nova.api.rackspace.limited(instance_list, req) + limited_list = nova.api.openstack.limited(instance_list, req) res = [entity_maker(inst)['server'] for inst in limited_list] return _entity_list(res) def show(self, req, id): """ Returns server details by server id """ - inst_id_trans = _instance_id_translator() - inst_id = inst_id_trans.from_rs_id(id) - user_id = req.environ['nova.context']['user']['id'] - inst = self.db_driver.instance_get_by_ec2_id(None, inst_id) + inst = self.db_driver.instance_get_by_internal_id(None, int(id)) if inst: if inst.user_id == user_id: return _entity_detail(inst) @@ -143,11 +125,8 @@ class Controller(wsgi.Controller): def delete(self, req, id): """ Destroys a server """ - inst_id_trans = _instance_id_translator() - inst_id = inst_id_trans.from_rs_id(id) - user_id = req.environ['nova.context']['user']['id'] - instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) if instance and instance['user_id'] == user_id: self.db_driver.instance_destroy(None, id) return faults.Fault(exc.HTTPAccepted()) @@ -160,10 +139,10 @@ class Controller(wsgi.Controller): if not env: return faults.Fault(exc.HTTPUnprocessableEntity()) - try: - inst = self._build_server_instance(req, env) - except Exception, e: - return faults.Fault(exc.HTTPUnprocessableEntity()) + #try: + inst = self._build_server_instance(req, env) + #except Exception, e: + # return faults.Fault(exc.HTTPUnprocessableEntity()) rpc.cast( FLAGS.compute_topic, { @@ -173,8 +152,6 @@ class Controller(wsgi.Controller): def update(self, req, id): """ Updates the server name or password """ - inst_id_trans = _instance_id_translator() - inst_id = inst_id_trans.from_rs_id(id) user_id = req.environ['nova.context']['user']['id'] inst_dict = self._deserialize(req.body, req) @@ -182,32 +159,33 @@ class Controller(wsgi.Controller): if not inst_dict: return faults.Fault(exc.HTTPUnprocessableEntity()) - instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) + instance = self.db_driver.instance_get_by_internal_id(None, int(id)) if not instance or instance.user_id != user_id: return faults.Fault(exc.HTTPNotFound()) - self.db_driver.instance_update(None, id, + self.db_driver.instance_update(None, int(id), _filter_params(inst_dict['server'])) return faults.Fault(exc.HTTPNoContent()) def action(self, req, id): """ multi-purpose method used to reboot, rebuild, and resize a server """ + user_id = req.environ['nova.context']['user']['id'] input_dict = self._deserialize(req.body, req) try: reboot_type = input_dict['reboot']['type'] except Exception: raise faults.Fault(webob.exc.HTTPNotImplemented()) - opaque_id = _instance_id_translator().from_rs_id(id) - cloud.reboot(opaque_id) + inst_ref = self.db.instance_get_by_internal_id(None, int(id)) + if not inst_ref or (inst_ref and not inst_ref.user_id == user_id): + return faults.Fault(exc.HTTPUnprocessableEntity()) + cloud.reboot(id) def _build_server_instance(self, req, env): """Build instance data structure and save it to the data store.""" ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} - inst_id_trans = _instance_id_translator() - user_id = req.environ['nova.context']['user']['id'] flavor_id = env['server']['flavorId'] @@ -218,16 +196,15 @@ class Controller(wsgi.Controller): image_id = env['server']['imageId'] - img_service, image_id_trans = _image_service() + img_service = utils.import_object(FLAGS.image_service) - opaque_image_id = image_id_trans.to_rs_id(image_id) - image = img_service.show(opaque_image_id) + image = img_service.show(image_id) if not image: raise Exception, "Image not found" inst['server_name'] = env['server']['name'] - inst['image_id'] = opaque_image_id + inst['image_id'] = image_id inst['user_id'] = user_id inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() @@ -258,7 +235,7 @@ class Controller(wsgi.Controller): inst['local_gb'] = flavor['local_gb'] ref = self.db_driver.instance_create(None, inst) - inst['id'] = inst_id_trans.to_rs_id(ref.ec2_id) + inst['id'] = ref.internal_id # TODO(dietz): this isn't explicitly necessary, but the networking # calls depend on an object with a project_id property, and therefore @@ -270,10 +247,10 @@ class Controller(wsgi.Controller): #TODO(dietz) is this necessary? inst['launch_index'] = 0 - inst['hostname'] = ref.ec2_id + inst['hostname'] = str(ref.internal_id) self.db_driver.instance_update(None, inst['id'], inst) - network_manager = utils.import_object(FLAGS.rs_network_manager) + network_manager = utils.import_object(FLAGS.network_manager) address = network_manager.allocate_fixed_ip(api_context, inst['id']) diff --git a/nova/api/rackspace/sharedipgroups.py b/nova/api/openstack/sharedipgroups.py index 4d2d0ede1..4d2d0ede1 100644 --- a/nova/api/rackspace/sharedipgroups.py +++ b/nova/api/openstack/sharedipgroups.py diff --git a/nova/api/rackspace/ratelimiting/tests.py b/nova/api/rackspace/ratelimiting/tests.py deleted file mode 100644 index 4c9510917..000000000 --- a/nova/api/rackspace/ratelimiting/tests.py +++ /dev/null @@ -1,237 +0,0 @@ -import httplib -import StringIO -import time -import unittest -import webob - -import nova.api.rackspace.ratelimiting as ratelimiting - -class LimiterTest(unittest.TestCase): - - def setUp(self): - self.limits = { - 'a': (5, ratelimiting.PER_SECOND), - 'b': (5, ratelimiting.PER_MINUTE), - 'c': (5, ratelimiting.PER_HOUR), - 'd': (1, ratelimiting.PER_SECOND), - 'e': (100, ratelimiting.PER_SECOND)} - self.rl = ratelimiting.Limiter(self.limits) - - def exhaust(self, action, times_until_exhausted, **kwargs): - for i in range(times_until_exhausted): - when = self.rl.perform(action, **kwargs) - self.assertEqual(when, None) - num, period = self.limits[action] - delay = period * 1.0 / num - # Verify that we are now thoroughly delayed - for i in range(10): - when = self.rl.perform(action, **kwargs) - self.assertAlmostEqual(when, delay, 2) - - def test_second(self): - self.exhaust('a', 5) - time.sleep(0.2) - self.exhaust('a', 1) - time.sleep(1) - self.exhaust('a', 5) - - def test_minute(self): - self.exhaust('b', 5) - - def test_one_per_period(self): - def allow_once_and_deny_once(): - when = self.rl.perform('d') - self.assertEqual(when, None) - when = self.rl.perform('d') - self.assertAlmostEqual(when, 1, 2) - return when - time.sleep(allow_once_and_deny_once()) - time.sleep(allow_once_and_deny_once()) - allow_once_and_deny_once() - - def test_we_can_go_indefinitely_if_we_spread_out_requests(self): - for i in range(200): - when = self.rl.perform('e') - self.assertEqual(when, None) - time.sleep(0.01) - - def test_users_get_separate_buckets(self): - self.exhaust('c', 5, username='alice') - self.exhaust('c', 5, username='bob') - self.exhaust('c', 5, username='chuck') - self.exhaust('c', 0, username='chuck') - self.exhaust('c', 0, username='bob') - self.exhaust('c', 0, username='alice') - - -class FakeLimiter(object): - """Fake Limiter class that you can tell how to behave.""" - def __init__(self, test): - self._action = self._username = self._delay = None - self.test = test - def mock(self, action, username, delay): - self._action = action - self._username = username - self._delay = delay - def perform(self, action, username): - self.test.assertEqual(action, self._action) - self.test.assertEqual(username, self._username) - return self._delay - - -class WSGIAppTest(unittest.TestCase): - - def setUp(self): - self.limiter = FakeLimiter(self) - self.app = ratelimiting.WSGIApp(self.limiter) - - def test_invalid_methods(self): - requests = [] - for method in ['GET', 'PUT', 'DELETE']: - req = webob.Request.blank('/limits/michael/breakdance', - dict(REQUEST_METHOD=method)) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 405) - - def test_invalid_urls(self): - requests = [] - for prefix in ['limit', '', 'limiter2', 'limiter/limits', 'limiter/1']: - req = webob.Request.blank('/%s/michael/breakdance' % prefix, - dict(REQUEST_METHOD='POST')) - requests.append(req) - for req in requests: - self.assertEqual(req.get_response(self.app).status_int, 404) - - def verify(self, url, username, action, delay=None): - """Make sure that POSTing to the given url causes the given username - to perform the given action. Make the internal rate limiter return - delay and make sure that the WSGI app returns the correct response. - """ - req = webob.Request.blank(url, dict(REQUEST_METHOD='POST')) - self.limiter.mock(action, username, delay) - resp = req.get_response(self.app) - if not delay: - self.assertEqual(resp.status_int, 200) - else: - self.assertEqual(resp.status_int, 403) - self.assertEqual(resp.headers['X-Wait-Seconds'], "%.2f" % delay) - - def test_good_urls(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot') - - def test_escaping(self): - self.verify('/limiter/michael/jump%20up', 'michael', 'jump up') - - def test_response_to_delays(self): - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1.56) - self.verify('/limiter/michael/hoot', 'michael', 'hoot', 1000) - - -class FakeHttplibSocket(object): - """a fake socket implementation for httplib.HTTPResponse, trivial""" - - def __init__(self, response_string): - self._buffer = StringIO.StringIO(response_string) - - def makefile(self, _mode, _other): - """Returns the socket's internal buffer""" - return self._buffer - - -class FakeHttplibConnection(object): - """A fake httplib.HTTPConnection - - Requests made via this connection actually get translated and routed into - our WSGI app, we then wait for the response and turn it back into - an httplib.HTTPResponse. - """ - def __init__(self, app, host, is_secure=False): - self.app = app - self.host = host - - def request(self, method, path, data='', headers={}): - req = webob.Request.blank(path) - req.method = method - req.body = data - req.headers = headers - req.host = self.host - # Call the WSGI app, get the HTTP response - resp = str(req.get_response(self.app)) - # For some reason, the response doesn't have "HTTP/1.0 " prepended; I - # guess that's a function the web server usually provides. - resp = "HTTP/1.0 %s" % resp - sock = FakeHttplibSocket(resp) - self.http_response = httplib.HTTPResponse(sock) - self.http_response.begin() - - def getresponse(self): - return self.http_response - - -def wire_HTTPConnection_to_WSGI(host, app): - """Monkeypatches HTTPConnection so that if you try to connect to host, you - are instead routed straight to the given WSGI app. - - After calling this method, when any code calls - - httplib.HTTPConnection(host) - - the connection object will be a fake. Its requests will be sent directly - to the given WSGI app rather than through a socket. - - Code connecting to hosts other than host will not be affected. - - This method may be called multiple times to map different hosts to - different apps. - """ - class HTTPConnectionDecorator(object): - """Wraps the real HTTPConnection class so that when you instantiate - the class you might instead get a fake instance.""" - def __init__(self, wrapped): - self.wrapped = wrapped - def __call__(self, connection_host, *args, **kwargs): - if connection_host == host: - return FakeHttplibConnection(app, host) - else: - return self.wrapped(connection_host, *args, **kwargs) - httplib.HTTPConnection = HTTPConnectionDecorator(httplib.HTTPConnection) - - -class WSGIAppProxyTest(unittest.TestCase): - - def setUp(self): - """Our WSGIAppProxy is going to call across an HTTPConnection to a - WSGIApp running a limiter. The proxy will send input, and the proxy - should receive that same input, pass it to the limiter who gives a - result, and send the expected result back. - - The HTTPConnection isn't real -- it's monkeypatched to point straight - at the WSGIApp. And the limiter isn't real -- it's a fake that - behaves the way we tell it to. - """ - self.limiter = FakeLimiter(self) - app = ratelimiting.WSGIApp(self.limiter) - wire_HTTPConnection_to_WSGI('100.100.100.100:80', app) - self.proxy = ratelimiting.WSGIAppProxy('100.100.100.100:80') - - def test_200(self): - self.limiter.mock('conquer', 'caesar', None) - when = self.proxy.perform('conquer', 'caesar') - self.assertEqual(when, None) - - def test_403(self): - self.limiter.mock('grumble', 'proletariat', 1.5) - when = self.proxy.perform('grumble', 'proletariat') - self.assertEqual(when, 1.5) - - def test_failure(self): - def shouldRaise(): - self.limiter.mock('murder', 'brutus', None) - self.proxy.perform('stab', 'brutus') - self.assertRaises(AssertionError, shouldRaise) - - -if __name__ == '__main__': - unittest.main() |
