diff options
| author | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-09-30 18:33:23 -0700 |
|---|---|---|
| committer | Vishvananda Ishaya <vishvananda@yahoo.com> | 2010-09-30 18:33:23 -0700 |
| commit | e0da95213631b3dc383ce69d6dff044c5841f143 (patch) | |
| tree | a3429d496c2b310eb08fbe445cde83f2c0b9d767 | |
| parent | e16031ddd09d668972c38372a28ec7f898f571f6 (diff) | |
| parent | c9cb22f87561fad4ba57865d8a614ca024393f13 (diff) | |
| download | nova-e0da95213631b3dc383ce69d6dff044c5841f143.tar.gz nova-e0da95213631b3dc383ce69d6dff044c5841f143.tar.xz nova-e0da95213631b3dc383ce69d6dff044c5841f143.zip | |
merged trunk, removed extra quotas
54 files changed, 2123 insertions, 529 deletions
diff --git a/bin/nova-manage b/bin/nova-manage index baa1cb4db..bf3c67612 100755 --- a/bin/nova-manage +++ b/bin/nova-manage @@ -135,15 +135,48 @@ class VpnCommands(object): class ShellCommands(object): - def run(self): - "Runs a Python interactive interpreter. Tries to use IPython, if it's available." - try: - import IPython - # Explicitly pass an empty list as arguments, because otherwise IPython - # would use sys.argv from this script. - shell = IPython.Shell.IPShell(argv=[]) - shell.mainloop() - except ImportError: + def bpython(self): + """Runs a bpython shell. + + Falls back to Ipython/python shell if unavailable""" + self.run('bpython') + + def ipython(self): + """Runs an Ipython shell. + + Falls back to Python shell if unavailable""" + self.run('ipython') + + def python(self): + """Runs a python shell. + + Falls back to Python shell if unavailable""" + self.run('python') + + def run(self, shell=None): + """Runs a Python interactive interpreter. + + args: [shell=bpython]""" + if not shell: + shell = 'bpython' + + if shell == 'bpython': + try: + import bpython + bpython.embed() + except ImportError: + shell = 'ipython' + if shell == 'ipython': + try: + import IPython + # Explicitly pass an empty list as arguments, because otherwise IPython + # would use sys.argv from this script. + shell = IPython.Shell.IPShell(argv=[]) + shell.mainloop() + except ImportError: + shell = 'python' + + if shell == 'python': import code try: # Try activating rlcompleter, because it's handy. import readline @@ -156,6 +189,11 @@ class ShellCommands(object): readline.parse_and_bind("tab:complete") code.interact() + def script(self, path): + """Runs the script from the specifed path with flags set properly. + arguments: path""" + exec(compile(open(path).read(), path, 'exec'), locals(), globals()) + class RoleCommands(object): """Class for managing roles.""" @@ -228,6 +266,18 @@ class UserCommands(object): for user in self.manager.get_users(): print user.name + def modify(self, name, access_key, secret_key, is_admin): + """update a users keys & admin flag + arguments: accesskey secretkey admin + leave any field blank to ignore it, admin should be 'T', 'F', or blank + """ + if not is_admin: + is_admin = None + elif is_admin.upper()[0] == 'T': + is_admin = True + else: + is_admin = False + self.manager.modify_user(name, access_key, secret_key, is_admin) class ProjectCommands(object): """Class for managing projects.""" @@ -253,7 +303,7 @@ class ProjectCommands(object): def environment(self, project_id, user_id, filename='novarc'): """Exports environment variables to an sourcable file arguments: project_id user_id [filename='novarc]""" - rc = self.manager.get_environment_rc(project_id, user_id) + rc = self.manager.get_environment_rc(user_id, project_id) with open(filename, 'w') as f: f.write(rc) @@ -316,7 +366,7 @@ class FloatingIpCommands(object): for floating_ip in floating_ips: instance = None if floating_ip['fixed_ip']: - instance = floating_ip['fixed_ip']['instance']['str_id'] + instance = floating_ip['fixed_ip']['instance']['ec2_id'] print "%s\t%s\t%s" % (floating_ip['host'], floating_ip['address'], instance) diff --git a/nova/adminclient.py b/nova/adminclient.py index 0ca32b1e5..fc9fcfde0 100644 --- a/nova/adminclient.py +++ b/nova/adminclient.py @@ -20,11 +20,17 @@ Nova User API client library. """ import base64 - import boto +import httplib from boto.ec2.regioninfo import RegionInfo +DEFAULT_CLC_URL='http://127.0.0.1:8773' +DEFAULT_REGION='nova' +DEFAULT_ACCESS_KEY='admin' +DEFAULT_SECRET_KEY='admin' + + class UserInfo(object): """ Information about a Nova user, as parsed through SAX @@ -68,13 +74,13 @@ class UserRole(object): def __init__(self, connection=None): self.connection = connection self.role = None - + def __repr__(self): return 'UserRole:%s' % self.role def startElement(self, name, attrs, connection): return None - + def endElement(self, name, value, connection): if name == 'role': self.role = value @@ -128,20 +134,20 @@ class ProjectMember(object): def __init__(self, connection=None): self.connection = connection self.memberId = None - + def __repr__(self): return 'ProjectMember:%s' % self.memberId def startElement(self, name, attrs, connection): return None - + def endElement(self, name, value, connection): if name == 'member': self.memberId = value else: setattr(self, name, str(value)) - + class HostInfo(object): """ Information about a Nova Host, as parsed through SAX: @@ -171,35 +177,56 @@ class HostInfo(object): class NovaAdminClient(object): - def __init__(self, clc_ip='127.0.0.1', region='nova', access_key='admin', - secret_key='admin', **kwargs): - self.clc_ip = clc_ip + def __init__(self, clc_url=DEFAULT_CLC_URL, region=DEFAULT_REGION, + access_key=DEFAULT_ACCESS_KEY, secret_key=DEFAULT_SECRET_KEY, + **kwargs): + parts = self.split_clc_url(clc_url) + + self.clc_url = clc_url self.region = region self.access = access_key self.secret = secret_key self.apiconn = boto.connect_ec2(aws_access_key_id=access_key, aws_secret_access_key=secret_key, - is_secure=False, - region=RegionInfo(None, region, clc_ip), - port=8773, + is_secure=parts['is_secure'], + region=RegionInfo(None, + region, + parts['ip']), + port=parts['port'], path='/services/Admin', **kwargs) self.apiconn.APIVersion = 'nova' - def connection_for(self, username, project, **kwargs): + def connection_for(self, username, project, clc_url=None, region=None, + **kwargs): """ Returns a boto ec2 connection for the given username. """ + if not clc_url: + clc_url = self.clc_url + if not region: + region = self.region + parts = self.split_clc_url(clc_url) user = self.get_user(username) access_key = '%s:%s' % (user.accesskey, project) - return boto.connect_ec2( - aws_access_key_id=access_key, - aws_secret_access_key=user.secretkey, - is_secure=False, - region=RegionInfo(None, self.region, self.clc_ip), - port=8773, - path='/services/Cloud', - **kwargs) + return boto.connect_ec2(aws_access_key_id=access_key, + aws_secret_access_key=user.secretkey, + is_secure=parts['is_secure'], + region=RegionInfo(None, + self.region, + parts['ip']), + port=parts['port'], + path='/services/Cloud', + **kwargs) + + def split_clc_url(self, clc_url): + """ + Splits a cloud controller endpoint url. + """ + parts = httplib.urlsplit(clc_url) + is_secure = parts.scheme == 'https' + ip, port = parts.netloc.split(':') + return {'ip': ip, 'port': int(port), 'is_secure': is_secure} def get_users(self): """ grabs the list of all users """ @@ -289,7 +316,7 @@ class NovaAdminClient(object): if project.projectname != None: return project - + def create_project(self, projectname, manager_user, description=None, member_users=None): """ @@ -322,7 +349,7 @@ class NovaAdminClient(object): Adds a user to a project. """ return self.modify_project_member(user, project, operation='add') - + def remove_project_member(self, user, project): """ Removes a user from a project. diff --git a/nova/api/cloud.py b/nova/api/cloud.py new file mode 100644 index 000000000..345677d4f --- /dev/null +++ b/nova/api/cloud.py @@ -0,0 +1,42 @@ +# 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. + +""" +Methods for API calls to control instances via AMQP. +""" + + +from nova import db +from nova import flags +from nova import rpc + +FLAGS = flags.FLAGS + + +def reboot(instance_id, context=None): + """Reboot the given instance. + + #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) + host = instance_ref['host'] + rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "reboot_instance", + "args": {"context": None, + "instance_id": instance_ref['id']}}) diff --git a/nova/api/ec2/__init__.py b/nova/api/ec2/__init__.py index f0aa57ee4..7a958f841 100644 --- a/nova/api/ec2/__init__.py +++ b/nova/api/ec2/__init__.py @@ -158,12 +158,14 @@ class Authorizer(wsgi.Middleware): 'RunInstances': ['projectmanager', 'sysadmin'], 'TerminateInstances': ['projectmanager', 'sysadmin'], 'RebootInstances': ['projectmanager', 'sysadmin'], + 'UpdateInstance': ['projectmanager', 'sysadmin'], 'DeleteVolume': ['projectmanager', 'sysadmin'], 'DescribeImages': ['all'], 'DeregisterImage': ['projectmanager', 'sysadmin'], 'RegisterImage': ['projectmanager', 'sysadmin'], 'DescribeImageAttribute': ['all'], 'ModifyImageAttribute': ['projectmanager', 'sysadmin'], + 'UpdateImage': ['projectmanager', 'sysadmin'], }, 'AdminController': { # All actions have the same permission: ['none'] (the default) diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 6a921c1fe..36646ad08 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -36,6 +36,7 @@ from nova import quota from nova import rpc from nova import utils from nova.compute.instance_types import INSTANCE_TYPES +from nova.api import cloud from nova.api.ec2 import images @@ -101,9 +102,9 @@ class CloudController(object): def _get_mpi_data(self, project_id): result = {} - for instance in db.instance_get_by_project(None, project_id): + for instance in db.instance_get_all_by_project(None, project_id): if instance['fixed_ip']: - line = '%s slots=%d' % (instance['fixed_ip']['str_id'], + line = '%s slots=%d' % (instance['fixed_ip']['address'], INSTANCE_TYPES[instance['instance_type']]['vcpus']) key = str(instance['key_name']) if key in result: @@ -143,7 +144,7 @@ class CloudController(object): }, 'hostname': hostname, 'instance-action': 'none', - 'instance-id': instance_ref['str_id'], + 'instance-id': instance_ref['ec2_id'], 'instance-type': instance_ref['instance_type'], 'local-hostname': hostname, 'local-ipv4': address, @@ -245,7 +246,7 @@ class CloudController(object): 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_str(context, instance_id[0]) + instance_ref = db.instance_get_by_ec2_id(context, instance_id[0]) return rpc.call('%s.%s' % (FLAGS.compute_topic, instance_ref['host']), {"method": "get_console_output", @@ -256,7 +257,7 @@ class CloudController(object): if context.user.is_admin(): volumes = db.volume_get_all(context) else: - volumes = db.volume_get_by_project(context, context.project.id) + volumes = db.volume_get_all_by_project(context, context.project.id) volumes = [self._format_volume(context, v) for v in volumes] @@ -264,7 +265,7 @@ class CloudController(object): def _format_volume(self, context, volume): v = {} - v['volumeId'] = volume['str_id'] + v['volumeId'] = volume['ec2_id'] v['status'] = volume['status'] v['size'] = volume['size'] v['availabilityZone'] = volume['availability_zone'] @@ -282,9 +283,12 @@ class CloudController(object): 'device': volume['mountpoint'], 'instanceId': volume['instance_id'], 'status': 'attached', - 'volume_id': volume['str_id']}] + 'volume_id': volume['ec2_id']}] else: v['attachmentSet'] = [{}] + + v['display_name'] = volume['display_name'] + v['display_description'] = volume['display_description'] return v def create_volume(self, context, size, **kwargs): @@ -302,6 +306,8 @@ class CloudController(object): vol['availability_zone'] = FLAGS.storage_availability_zone vol['status'] = "creating" vol['attach_status'] = "detached" + vol['display_name'] = kwargs.get('display_name') + vol['display_description'] = kwargs.get('display_description') volume_ref = db.volume_create(context, vol) rpc.cast(FLAGS.scheduler_topic, @@ -314,13 +320,13 @@ class CloudController(object): def attach_volume(self, context, volume_id, instance_id, device, **kwargs): - volume_ref = db.volume_get_by_str(context, volume_id) + volume_ref = db.volume_get_by_ec2_id(context, volume_id) # TODO(vish): abstract status checking? if volume_ref['status'] != "available": 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_str(context, instance_id) + instance_ref = db.instance_get_by_ec2_id(context, instance_id) host = instance_ref['host'] rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), {"method": "attach_volume", @@ -336,7 +342,7 @@ class CloudController(object): 'volumeId': volume_ref['id']} def detach_volume(self, context, volume_id, **kwargs): - volume_ref = db.volume_get_by_str(context, volume_id) + volume_ref = db.volume_get_by_ec2_id(context, volume_id) instance_ref = db.volume_get_instance(context, volume_ref['id']) if not instance_ref: raise exception.ApiError("Volume isn't attached to anything!") @@ -356,7 +362,7 @@ class CloudController(object): db.volume_detached(context) return {'attachTime': volume_ref['attach_time'], 'device': volume_ref['mountpoint'], - 'instanceId': instance_ref['str_id'], + 'instanceId': instance_ref['ec2_id'], 'requestId': context.request_id, 'status': volume_ref['attach_status'], 'volumeId': volume_ref['id']} @@ -368,6 +374,16 @@ class CloudController(object): lst = [lst] return [{label: x} for x in lst] + def update_volume(self, context, volume_id, **kwargs): + updatable_fields = ['display_name', 'display_description'] + changes = {} + for field in updatable_fields: + if field in kwargs: + changes[field] = kwargs[field] + if changes: + db.volume_update(context, volume_id, kwargs) + return True + def describe_instances(self, context, **kwargs): return self._format_describe_instances(context) @@ -382,20 +398,20 @@ class CloudController(object): def _format_instances(self, context, reservation_id=None): reservations = {} if reservation_id: - instances = db.instance_get_by_reservation(context, - reservation_id) + instances = db.instance_get_all_by_reservation(context, + reservation_id) else: if context.user.is_admin(): instances = db.instance_get_all(context) else: - instances = db.instance_get_by_project(context, - context.project.id) + instances = db.instance_get_all_by_project(context, + context.project.id) for instance in instances: if not context.user.is_admin(): if instance['image_id'] == FLAGS.vpn_image_id: continue i = {} - i['instanceId'] = instance['str_id'] + i['instanceId'] = instance['ec2_id'] i['imageId'] = instance['image_id'] i['instanceState'] = { 'code': instance['state'], @@ -404,10 +420,10 @@ class CloudController(object): fixed_addr = None floating_addr = None if instance['fixed_ip']: - fixed_addr = instance['fixed_ip']['str_id'] + fixed_addr = instance['fixed_ip']['address'] if instance['fixed_ip']['floating_ips']: fixed = instance['fixed_ip'] - floating_addr = fixed['floating_ips'][0]['str_id'] + floating_addr = fixed['floating_ips'][0]['address'] i['privateDnsName'] = fixed_addr i['publicDnsName'] = floating_addr i['dnsName'] = i['publicDnsName'] or i['privateDnsName'] @@ -420,6 +436,8 @@ class CloudController(object): i['instanceType'] = instance['instance_type'] i['launchTime'] = instance['created_at'] i['amiLaunchIndex'] = instance['launch_index'] + i['displayName'] = instance['display_name'] + i['displayDescription'] = instance['display_description'] if not reservations.has_key(instance['reservation_id']): r = {} r['reservationId'] = instance['reservation_id'] @@ -439,14 +457,14 @@ class CloudController(object): if context.user.is_admin(): iterator = db.floating_ip_get_all(context) else: - iterator = db.floating_ip_get_by_project(context, - context.project.id) + iterator = db.floating_ip_get_all_by_project(context, + context.project.id) for floating_ip_ref in iterator: - address = floating_ip_ref['str_id'] + address = floating_ip_ref['address'] instance_id = None if (floating_ip_ref['fixed_ip'] and floating_ip_ref['fixed_ip']['instance']): - instance_id = floating_ip_ref['fixed_ip']['instance']['str_id'] + instance_id = floating_ip_ref['fixed_ip']['instance']['ec2_id'] address_rv = {'public_ip': address, 'instance_id': instance_id} if context.user.is_admin(): @@ -477,19 +495,20 @@ class CloudController(object): rpc.cast(network_topic, {"method": "deallocate_floating_ip", "args": {"context": None, - "floating_address": floating_ip_ref['str_id']}}) + "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_str(context, instance_id) - fixed_ip_ref = db.fixed_ip_get_by_instance(context, instance_ref['id']) + instance_ref = db.instance_get_by_ec2_id(context, instance_id) + 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) rpc.cast(network_topic, {"method": "associate_floating_ip", "args": {"context": None, - "floating_address": floating_ip_ref['str_id'], - "fixed_address": fixed_ip_ref['str_id']}}) + "floating_address": floating_ip_ref['address'], + "fixed_address": fixed_address}}) return {'associateResponse': ["Address associated."]} def disassociate_address(self, context, public_ip, **kwargs): @@ -498,7 +517,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "disassociate_floating_ip", "args": {"context": None, - "floating_address": floating_ip_ref['str_id']}}) + "floating_address": floating_ip_ref['address']}}) return {'disassociateResponse': ["Address disassociated."]} def _get_network_topic(self, context): @@ -576,6 +595,8 @@ class CloudController(object): 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['memory_mb'] = type_data['memory_mb'] @@ -589,7 +610,7 @@ class CloudController(object): inst = {} inst['mac_address'] = utils.generate_mac() inst['launch_index'] = num - inst['hostname'] = instance_ref['str_id'] + inst['hostname'] = instance_ref['ec2_id'] db.instance_update(context, inst_id, inst) address = self.network_manager.allocate_fixed_ip(context, inst_id, @@ -618,7 +639,7 @@ class CloudController(object): for id_str in instance_id: logging.debug("Going to try and terminate %s" % id_str) try: - instance_ref = db.instance_get_by_str(context, id_str) + instance_ref = db.instance_get_by_ec2_id(context, id_str) except exception.NotFound: logging.warning("Instance %s was not found during terminate", id_str) @@ -646,7 +667,7 @@ class CloudController(object): rpc.cast(network_topic, {"method": "disassociate_floating_ip", "args": {"context": None, - "address": address}}) + "floating_address": address}}) address = db.instance_get_fixed_address(context, instance_ref['id']) @@ -670,17 +691,24 @@ class CloudController(object): def reboot_instances(self, context, instance_id, **kwargs): """instance_id is a list of instance ids""" for id_str in instance_id: - instance_ref = db.instance_get_by_str(context, id_str) - host = instance_ref['host'] - rpc.cast(db.queue_get_for(context, FLAGS.compute_topic, host), - {"method": "reboot_instance", - "args": {"context": None, - "instance_id": instance_ref['id']}}) + cloud.reboot(id_str, context=context) + return True + + def update_instance(self, context, instance_id, **kwargs): + updatable_fields = ['display_name', 'display_description'] + changes = {} + for field in updatable_fields: + if field in kwargs: + changes[field] = kwargs[field] + if changes: + db_context = {} + inst = db.instance_get_by_ec2_id(db_context, instance_id) + db.instance_update(db_context, inst['id'], kwargs) return True def delete_volume(self, context, volume_id, **kwargs): # TODO: return error if not authorized - volume_ref = db.volume_get_by_str(context, volume_id) + volume_ref = db.volume_get_by_ec2_id(context, volume_id) if volume_ref['status'] != "available": raise exception.ApiError("Volume status must be available") now = datetime.datetime.utcnow() @@ -734,3 +762,7 @@ class CloudController(object): if not operation_type in ['add', 'remove']: raise exception.ApiError('operation_type must be add or remove') return images.modify(context, image_id, operation_type) + + def update_image(self, context, image_id, **kwargs): + result = images.update(context, image_id, dict(kwargs)) + return result diff --git a/nova/api/ec2/images.py b/nova/api/ec2/images.py index 4579cd81a..cb54cdda2 100644 --- a/nova/api/ec2/images.py +++ b/nova/api/ec2/images.py @@ -43,6 +43,14 @@ def modify(context, image_id, operation): return True +def update(context, image_id, attributes): + """update an image's attributes / info.json""" + attributes.update({"image_id": image_id}) + conn(context).make_request( + method='POST', + bucket='_images', + query_args=qs(attributes)) + return True def register(context, image_location): """ rpc call to register a new image based from a manifest """ diff --git a/nova/api/rackspace/__init__.py b/nova/api/rackspace/__init__.py index c24d08585..89a4693ad 100644 --- a/nova/api/rackspace/__init__.py +++ b/nova/api/rackspace/__init__.py @@ -31,6 +31,8 @@ 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 @@ -66,9 +68,11 @@ class AuthMiddleware(wsgi.Middleware): user = self.auth_driver.authorize_token(req.headers["X-Auth-Token"]) if not user: - return webob.exc.HTTPUnauthorized() - context = {'user': user} - req.environ['nova.context'] = context + return faults.Fault(webob.exc.HTTPUnauthorized()) + + if not req.environ.has_key('nova.context'): + req.environ['nova.context'] = {} + req.environ['nova.context']['user'] = user return self.application class RateLimitingMiddleware(wsgi.Middleware): @@ -109,8 +113,10 @@ class RateLimitingMiddleware(wsgi.Middleware): delay = self.get_delay(action_name, username) if delay: # TODO(gundlach): Get the retry-after format correct. - raise webob.exc.HTTPRequestEntityTooLarge(headers={ - 'Retry-After': time.time() + delay}) + 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): @@ -145,11 +151,40 @@ class APIRouter(wsgi.Router): def __init__(self): mapper = routes.Mapper() - mapper.resource("server", "servers", controller=servers.Controller()) + mapper.resource("server", "servers", controller=servers.Controller(), + collection={ 'detail': 'GET'}, + member={'action':'POST'}) + + mapper.resource("backup_schedule", "backup_schedules", + controller=backup_schedules.Controller(), + parent_resource=dict(member_name='server', + collection_name = 'servers')) + mapper.resource("image", "images", controller=images.Controller(), collection={'detail': 'GET'}) mapper.resource("flavor", "flavors", controller=flavors.Controller(), collection={'detail': 'GET'}) mapper.resource("sharedipgroup", "sharedipgroups", controller=sharedipgroups.Controller()) + super(APIRouter, self).__init__(mapper) + + +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/rackspace/_id_translator.py b/nova/api/rackspace/_id_translator.py index aec5fb6a5..333aa8434 100644 --- a/nova/api/rackspace/_id_translator.py +++ b/nova/api/rackspace/_id_translator.py @@ -37,6 +37,6 @@ class RackspaceAPIIdTranslator(object): # every int id be used.) return int(self._store.hget(self._fwd_key, str(opaque_id))) - def from_rs_id(self, strategy_name, rs_id): + def from_rs_id(self, rs_id): """Convert a Rackspace id to a strategy-specific one.""" return self._store.hget(self._rev_key, rs_id) diff --git a/nova/api/rackspace/auth.py b/nova/api/rackspace/auth.py index ce5a967eb..c45156ebd 100644 --- a/nova/api/rackspace/auth.py +++ b/nova/api/rackspace/auth.py @@ -1,14 +1,17 @@ import datetime +import hashlib import json import time + import webob.exc import webob.dec -import hashlib -from nova import flags + from nova import auth -from nova import manager from nova import db +from nova import flags +from nova import manager from nova import utils +from nova.api.rackspace import faults FLAGS = flags.FLAGS @@ -34,13 +37,13 @@ class BasicApiAuthManager(object): # honor it path_info = req.path_info if len(path_info) > 1: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) try: username, key = req.headers['X-Auth-User'], \ req.headers['X-Auth-Key'] except KeyError: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) username, key = req.headers['X-Auth-User'], req.headers['X-Auth-Key'] token, user = self._authorize_user(username, key) @@ -55,7 +58,7 @@ class BasicApiAuthManager(object): res.status = '204' return res else: - return webob.exc.HTTPUnauthorized() + return faults.Fault(webob.exc.HTTPUnauthorized()) def authorize_token(self, token_hash): """ retrieves user information from the datastore given a token diff --git a/nova/api/rackspace/backup_schedules.py b/nova/api/rackspace/backup_schedules.py new file mode 100644 index 000000000..cb83023bc --- /dev/null +++ b/nova/api/rackspace/backup_schedules.py @@ -0,0 +1,39 @@ +# 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 time +from webob import exc + +from nova import wsgi +from nova.api.rackspace import _id_translator +from nova.api.rackspace import faults +import nova.image.service + +class Controller(wsgi.Controller): + def __init__(self): + pass + + def index(self, req, server_id): + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req, server_id): + """ No actual update method required, since the existing API allows + both create and update through a POST """ + return faults.Fault(exc.HTTPNotFound()) + + def delete(self, req, server_id): + return faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/base.py b/nova/api/rackspace/context.py index dd2c6543c..77394615b 100644 --- a/nova/api/rackspace/base.py +++ b/nova/api/rackspace/context.py @@ -15,16 +15,19 @@ # License for the specific language governing permissions and limitations # under the License. -from nova import wsgi +""" +APIRequestContext +""" +import random -class Controller(wsgi.Controller): - """TODO(eday): Base controller for all rackspace controllers. What is this - for? Is this just Rackspace specific? """ - - @classmethod - def render(cls, instance): - if isinstance(instance, list): - return {cls.entity_name: cls.render(instance)} - else: - return {"TODO": "TODO"} +class Project(object): + def __init__(self, user_id): + self.id = user_id + +class APIRequestContext(object): + """ This is an adapter class to get around all of the assumptions made in + the FlatNetworking """ + def __init__(self, user_id): + self.user_id = user_id + self.project = Project(user_id) diff --git a/nova/api/rackspace/faults.py b/nova/api/rackspace/faults.py new file mode 100644 index 000000000..32e5c866f --- /dev/null +++ b/nova/api/rackspace/faults.py @@ -0,0 +1,62 @@ +# 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 webob.dec +import webob.exc + +from nova import wsgi + + +class Fault(webob.exc.HTTPException): + + """An RS API fault response.""" + + _fault_names = { + 400: "badRequest", + 401: "unauthorized", + 403: "resizeNotAllowed", + 404: "itemNotFound", + 405: "badMethod", + 409: "inProgress", + 413: "overLimit", + 415: "badMediaType", + 501: "notImplemented", + 503: "serviceUnavailable"} + + def __init__(self, exception): + """Create a Fault for the given webob.exc.exception.""" + self.wrapped_exc = exception + + @webob.dec.wsgify + def __call__(self, req): + """Generate a WSGI response based on the exception passed to ctor.""" + # Replace the body with fault details. + code = self.wrapped_exc.status_int + fault_name = self._fault_names.get(code, "cloudServersFault") + fault_data = { + fault_name: { + 'code': code, + 'message': self.wrapped_exc.explanation}} + if code == 413: + retry = self.wrapped_exc.headers['Retry-After'] + fault_data[fault_name]['retryAfter'] = retry + # 'code' is an attribute on the fault tag itself + metadata = {'application/xml': {'attributes': {fault_name: 'code'}}} + serializer = wsgi.Serializer(req.environ, metadata) + self.wrapped_exc.body = serializer.to_content_type(fault_data) + return self.wrapped_exc diff --git a/nova/api/rackspace/flavors.py b/nova/api/rackspace/flavors.py index 60b35c939..916449854 100644 --- a/nova/api/rackspace/flavors.py +++ b/nova/api/rackspace/flavors.py @@ -15,11 +15,14 @@ # License for the specific language governing permissions and limitations # under the License. -from nova.api.rackspace import base -from nova.compute import instance_types from webob import exc -class Controller(base.Controller): +from nova.api.rackspace import faults +from nova.compute import instance_types +from nova import wsgi +import nova.api.rackspace + +class Controller(wsgi.Controller): """Flavor controller for the Rackspace API.""" _serialization_metadata = { @@ -38,6 +41,7 @@ class Controller(base.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) return dict(flavors=items) def show(self, req, id): @@ -47,7 +51,7 @@ class Controller(base.Controller): item = dict(ram=val['memory_mb'], disk=val['local_gb'], id=val['flavorid'], name=name) return dict(flavor=item) - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) def _all_ids(self): """Return the list of all flavorids.""" diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py index 2f3e928b9..4a7dd489c 100644 --- a/nova/api/rackspace/images.py +++ b/nova/api/rackspace/images.py @@ -15,12 +15,15 @@ # License for the specific language governing permissions and limitations # under the License. -import nova.image.service -from nova.api.rackspace import base -from nova.api.rackspace import _id_translator from webob import exc -class Controller(base.Controller): +from nova import wsgi +from nova.api.rackspace import _id_translator +import nova.api.rackspace +import nova.image.service +from nova.api.rackspace import faults + +class Controller(wsgi.Controller): _serialization_metadata = { 'application/xml': { @@ -44,6 +47,7 @@ class Controller(base.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']) return dict(images=data) @@ -57,14 +61,14 @@ class Controller(base.Controller): def delete(self, req, id): # Only public images are supported for now. - raise exc.HTTPNotFound() + 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 exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) def update(self, req, id): # Users may not modify public images, and that's all that # we support for now. - raise exc.HTTPNotFound() + raise faults.Fault(exc.HTTPNotFound()) diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py index 1815f7523..11efd8aef 100644 --- a/nova/api/rackspace/servers.py +++ b/nova/api/rackspace/servers.py @@ -14,67 +14,286 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + import time -from nova import db +import webob +from webob import exc + from nova import flags from nova import rpc from nova import utils -from nova.api.rackspace import base +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.compute import instance_types +from nova.compute import power_state +import nova.api.rackspace +import nova.image.service FLAGS = flags.FLAGS -class Controller(base.Controller): - entity_name = 'servers' +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') + new_attrs = {} + for k, v in keys.items(): + if inst_dict.has_key(v): + new_attrs[k] = inst_dict[v] + return new_attrs + +def _entity_list(entities): + """ Coerces a list of servers into proper dictionary format """ + return dict(servers=entities) + +def _entity_detail(inst): + """ Maps everything to Rackspace-like attributes for return""" + power_mapping = { + power_state.NOSTATE: 'build', + power_state.RUNNING: 'active', + power_state.BLOCKED: 'active', + power_state.PAUSED: 'suspended', + power_state.SHUTDOWN: 'active', + power_state.SHUTOFF: 'active', + power_state.CRASHED: 'error' + } + inst_dict = {} + + mapped_keys = dict(status='state', imageId='image_id', + flavorId='instance_type', name='server_name', id='id') + + for k, v in mapped_keys.iteritems(): + inst_dict[k] = inst[v] + + inst_dict['status'] = power_mapping[inst_dict['status']] + inst_dict['addresses'] = dict(public=[], private=[]) + inst_dict['metadata'] = {} + inst_dict['hostId'] = '' + + return dict(server=inst_dict) + +def _entity_inst(inst): + """ Filters all model attributes save for id and name """ + return dict(server=dict(id=inst['id'], name=inst['server_name'])) - def index(self, **kwargs): - instances = [] - for inst in db.instance_get_all(None): - instances.append(instance_details(inst)) +class Controller(wsgi.Controller): + """ The Server API controller for the Openstack API """ - def show(self, **kwargs): - instance_id = kwargs['id'] - return db.instance_get(None, instance_id) + _serialization_metadata = { + 'application/xml': { + "attributes": { + "server": [ "id", "imageId", "name", "flavorId", "hostId", + "status", "progress", "progress" ] + } + } + } - def delete(self, **kwargs): - instance_id = kwargs['id'] - instance = db.instance_get(None, instance_id) - if not instance: - raise ServerNotFound("The requested server was not found") - instance.destroy() - return True + def __init__(self, db_driver=None): + if not db_driver: + db_driver = FLAGS.db_driver + self.db_driver = utils.import_object(db_driver) + super(Controller, self).__init__() + + def index(self, req): + """ Returns a list of server names and ids for a given user """ + return self._items(req, entity_maker=_entity_inst) + + def detail(self, req): + """ Returns a list of server details for a given user """ + return self._items(req, entity_maker=_entity_detail) + + def _items(self, req, entity_maker): + """Returns a list of servers for a given user. + + entity_maker - either _entity_detail or _entity_inst + """ + 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) + 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) + if inst: + if inst.user_id == user_id: + return _entity_detail(inst) + raise faults.Fault(exc.HTTPNotFound()) + + 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) + if instance and instance['user_id'] == user_id: + self.db_driver.instance_destroy(None, id) + return faults.Fault(exc.HTTPAccepted()) + return faults.Fault(exc.HTTPNotFound()) + + def create(self, req): + """ Creates a new server for a given user """ + + env = self._deserialize(req.body, req) + if not env: + return faults.Fault(exc.HTTPUnprocessableEntity()) + + try: + inst = self._build_server_instance(req, env) + except Exception, e: + return faults.Fault(exc.HTTPUnprocessableEntity()) - def create(self, **kwargs): - inst = self.build_server_instance(kwargs['server']) rpc.cast( FLAGS.compute_topic, { "method": "run_instance", "args": {"instance_id": inst['id']}}) + return _entity_inst(inst) + + 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'] - def update(self, **kwargs): - instance_id = kwargs['id'] - instance = db.instance_get(None, instance_id) - if not instance: - raise ServerNotFound("The requested server was not found") - instance.update(kwargs['server']) - instance.save() + inst_dict = self._deserialize(req.body, req) + + if not inst_dict: + return faults.Fault(exc.HTTPUnprocessableEntity()) - def build_server_instance(self, env): + instance = self.db_driver.instance_get_by_ec2_id(None, inst_id) + if not instance or instance.user_id != user_id: + return faults.Fault(exc.HTTPNotFound()) + + self.db_driver.instance_update(None, 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 """ + 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) + + def _build_server_instance(self, req, env): """Build instance data structure and save it to the data store.""" - reservation = utils.generate_uid('r') ltime = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()) inst = {} - inst['name'] = env['server']['name'] - inst['image_id'] = env['server']['imageId'] - inst['instance_type'] = env['server']['flavorId'] - inst['user_id'] = env['user']['id'] - inst['project_id'] = env['project']['id'] - inst['reservation_id'] = reservation + + inst_id_trans = _instance_id_translator() + + user_id = req.environ['nova.context']['user']['id'] + + flavor_id = env['server']['flavorId'] + + instance_type, flavor = [(k, v) for k, v in + instance_types.INSTANCE_TYPES.iteritems() + if v['flavorid'] == flavor_id][0] + + image_id = env['server']['imageId'] + + img_service, image_id_trans = _image_service() + + opaque_image_id = image_id_trans.to_rs_id(image_id) + image = img_service.show(opaque_image_id) + + if not image: + raise Exception, "Image not found" + + inst['server_name'] = env['server']['name'] + inst['image_id'] = opaque_image_id + inst['user_id'] = user_id inst['launch_time'] = ltime inst['mac_address'] = utils.generate_mac() - inst_id = db.instance_create(None, inst)['id'] - address = self.network_manager.allocate_fixed_ip(None, inst_id) - # key_data, key_name, ami_launch_index - # TODO(todd): key data or root password - inst.save() + inst['project_id'] = user_id + + inst['state_description'] = 'scheduling' + inst['kernel_id'] = image.get('kernelId', FLAGS.default_kernel) + inst['ramdisk_id'] = image.get('ramdiskId', FLAGS.default_ramdisk) + inst['reservation_id'] = utils.generate_uid('r') + + inst['display_name'] = env['server']['name'] + inst['display_description'] = env['server']['name'] + + #TODO(dietz) this may be ill advised + key_pair_ref = self.db_driver.key_pair_get_all_by_user( + None, user_id)[0] + + inst['key_data'] = key_pair_ref['public_key'] + inst['key_name'] = key_pair_ref['name'] + + #TODO(dietz) stolen from ec2 api, see TODO there + inst['security_group'] = 'default' + + # Flavor related attributes + inst['instance_type'] = instance_type + inst['memory_mb'] = flavor['memory_mb'] + inst['vcpus'] = flavor['vcpus'] + 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) + + # TODO(dietz): this isn't explicitly necessary, but the networking + # calls depend on an object with a project_id property, and therefore + # should be cleaned up later + api_context = context.APIRequestContext(user_id) + + inst['mac_address'] = utils.generate_mac() + + #TODO(dietz) is this necessary? + inst['launch_index'] = 0 + + inst['hostname'] = ref.ec2_id + self.db_driver.instance_update(None, inst['id'], inst) + + network_manager = utils.import_object(FLAGS.rs_network_manager) + address = network_manager.allocate_fixed_ip(api_context, + inst['id']) + + # TODO(vish): This probably should be done in the scheduler + # network is setup when host is assigned + network_topic = self._get_network_topic(user_id) + rpc.call(network_topic, + {"method": "setup_fixed_ip", + "args": {"context": None, + "address": address}}) return inst + + def _get_network_topic(self, user_id): + """Retrieves the network host for a project""" + network_ref = self.db_driver.project_get_network(None, + user_id) + host = network_ref['host'] + if not host: + host = rpc.call(FLAGS.network_topic, + {"method": "set_network_host", + "args": {"context": None, + "project_id": user_id}}) + return self.db_driver.queue_get_for(None, FLAGS.network_topic, host) diff --git a/nova/api/rackspace/sharedipgroups.py b/nova/api/rackspace/sharedipgroups.py index 986f11434..4d2d0ede1 100644 --- a/nova/api/rackspace/sharedipgroups.py +++ b/nova/api/rackspace/sharedipgroups.py @@ -15,4 +15,6 @@ # License for the specific language governing permissions and limitations # under the License. -class Controller(object): pass +from nova import wsgi + +class Controller(wsgi.Controller): pass diff --git a/nova/auth/ldapdriver.py b/nova/auth/ldapdriver.py index 021851ebf..640ea169e 100644 --- a/nova/auth/ldapdriver.py +++ b/nova/auth/ldapdriver.py @@ -256,8 +256,7 @@ class LdapDriver(object): if not self.__user_exists(uid): raise exception.NotFound("User %s doesn't exist" % uid) self.__remove_from_all(uid) - self.conn.delete_s('uid=%s,%s' % (uid, - FLAGS.ldap_user_subtree)) + self.conn.delete_s(self.__uid_to_dn(uid)) def delete_project(self, project_id): """Delete a project""" @@ -265,6 +264,19 @@ class LdapDriver(object): self.__delete_roles(project_dn) self.__delete_group(project_dn) + def modify_user(self, uid, access_key=None, secret_key=None, admin=None): + """Modify an existing project""" + if not access_key and not secret_key and admin is None: + return + attr = [] + if access_key: + attr.append((self.ldap.MOD_REPLACE, 'accessKey', access_key)) + if secret_key: + attr.append((self.ldap.MOD_REPLACE, 'secretKey', secret_key)) + if admin is not None: + attr.append((self.ldap.MOD_REPLACE, 'isAdmin', str(admin).upper())) + self.conn.modify_s(self.__uid_to_dn(uid), attr) + def __user_exists(self, uid): """Check if user exists""" return self.get_user(uid) != None diff --git a/nova/auth/manager.py b/nova/auth/manager.py index 55fbf42aa..0bc12c80f 100644 --- a/nova/auth/manager.py +++ b/nova/auth/manager.py @@ -630,6 +630,12 @@ class AuthManager(object): with self.driver() as drv: drv.delete_user(uid) + def modify_user(self, user, access_key=None, secret_key=None, admin=None): + """Modify credentials for a user""" + uid = User.safe_id(user) + with self.driver() as drv: + drv.modify_user(uid, access_key, secret_key, admin) + def get_credentials(self, user, project=None): """Get credential zip for user in project""" if not isinstance(user, User): diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 1ed70dc3f..70730cff6 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -67,7 +67,7 @@ class ComputeManager(manager.Manager): def run_instance(self, context, instance_id, **_kwargs): """Launch a new instance with specified options.""" instance_ref = self.db.instance_get(context, instance_id) - if instance_ref['str_id'] in self.driver.list_instances(): + if instance_ref['ec2_id'] in self.driver.list_instances(): raise exception.Error("Instance has already been created") logging.debug("instance %s: starting...", instance_id) project_id = instance_ref['project_id'] @@ -125,7 +125,7 @@ class ComputeManager(manager.Manager): raise exception.Error( 'trying to reboot a non-running' 'instance: %s (state: %s excepted: %s)' % - (instance_ref['str_id'], + (instance_ref['ec2_id'], instance_ref['state'], power_state.RUNNING)) @@ -147,7 +147,7 @@ class ComputeManager(manager.Manager): if FLAGS.connection_type == 'libvirt': fname = os.path.abspath(os.path.join(FLAGS.instances_path, - instance_ref['str_id'], + instance_ref['ec2_id'], 'console.log')) with open(fname, 'r') as f: output = f.read() @@ -170,7 +170,7 @@ class ComputeManager(manager.Manager): instance_ref = self.db.instance_get(context, instance_id) dev_path = yield self.volume_manager.setup_compute_volume(context, volume_id) - yield self.driver.attach_volume(instance_ref['str_id'], + yield self.driver.attach_volume(instance_ref['ec2_id'], dev_path, mountpoint) self.db.volume_attached(context, volume_id, instance_id, mountpoint) @@ -185,7 +185,7 @@ class ComputeManager(manager.Manager): volume_id) instance_ref = self.db.instance_get(context, instance_id) volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.detach_volume(instance_ref['str_id'], + yield self.driver.detach_volume(instance_ref['ec2_id'], volume_ref['mountpoint']) self.db.volume_detached(context, volume_id) defer.returnValue(True) diff --git a/nova/db/api.py b/nova/db/api.py index 848ec2682..a39d5665e 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -161,10 +161,15 @@ def floating_ip_get_all(context): def floating_ip_get_all_by_host(context, host): - """Get all floating ips.""" + """Get all floating ips by host.""" return IMPL.floating_ip_get_all_by_host(context, host) +def floating_ip_get_all_by_project(context, project_id): + """Get all floating ips by project.""" + return IMPL.floating_ip_get_all_by_project(context, project_id) + + def floating_ip_get_by_address(context, address): """Get a floating ip by address or raise if it doesn't exist.""" return IMPL.floating_ip_get_by_address(context, address) @@ -204,6 +209,11 @@ def fixed_ip_disassociate(context, address): return IMPL.fixed_ip_disassociate(context, address) +def fixed_ip_disassociate_all_by_timeout(context, host, time): + """Disassociate old fixed ips from host""" + return IMPL.fixed_ip_disassociate_all_by_timeout(context, host, time) + + def fixed_ip_get_by_address(context, address): """Get a fixed ip by address or raise if it does not exist.""" return IMPL.fixed_ip_get_by_address(context, address) @@ -251,15 +261,18 @@ def instance_get_all(context): """Get all instances.""" return IMPL.instance_get_all(context) +def instance_get_all_by_user(context, user_id): + """Get all instances.""" + return IMPL.instance_get_all(context, user_id) -def instance_get_by_project(context, project_id): +def instance_get_all_by_project(context, project_id): """Get all instance belonging to a project.""" - return IMPL.instance_get_by_project(context, project_id) + return IMPL.instance_get_all_by_project(context, project_id) -def instance_get_by_reservation(context, reservation_id): +def instance_get_all_by_reservation(context, reservation_id): """Get all instance belonging to a reservation.""" - return IMPL.instance_get_by_reservation(context, reservation_id) + return IMPL.instance_get_all_by_reservation(context, reservation_id) def instance_get_fixed_address(context, instance_id): @@ -272,9 +285,9 @@ def instance_get_floating_address(context, instance_id): return IMPL.instance_get_floating_address(context, instance_id) -def instance_get_by_str(context, str_id): - """Get an instance by string id.""" - return IMPL.instance_get_by_str(context, str_id) +def instance_get_by_ec2_id(context, ec2_id): + """Get an instance by ec2 id.""" + return IMPL.instance_get_by_ec2_id(context, ec2_id) def instance_is_vpn(context, instance_id): @@ -393,9 +406,12 @@ def network_index_count(context): return IMPL.network_index_count(context) -def network_index_create(context, values): - """Create a network index from the values dict""" - return IMPL.network_index_create(context, values) +def network_index_create_safe(context, values): + """Create a network index from the values dict + + The index is not returned. If the create violates the unique + constraints because the index already exists, no exception is raised.""" + return IMPL.network_index_create_safe(context, values) def network_set_cidr(context, network_id, cidr): @@ -475,6 +491,7 @@ def quota_destroy(context, project_id): ################### + def auth_destroy_token(context, token): """Destroy an auth token""" return IMPL.auth_destroy_token(context, token) @@ -491,29 +508,6 @@ def auth_create_token(context, token): ################### -def quota_create(context, values): - """Create a quota from the values dictionary.""" - return IMPL.quota_create(context, values) - - -def quota_get(context, project_id): - """Retrieve a quota or raise if it does not exist.""" - return IMPL.quota_get(context, project_id) - - -def quota_update(context, project_id, values): - """Update a quota from the values dictionary.""" - return IMPL.quota_update(context, project_id, values) - - -def quota_destroy(context, project_id): - """Destroy the quota or raise if it does not exist.""" - return IMPL.quota_destroy(context, project_id) - - -################### - - def volume_allocate_shelf_and_blade(context, volume_id): """Atomically allocate a free shelf and blade from the pool.""" return IMPL.volume_allocate_shelf_and_blade(context, volume_id) @@ -559,14 +553,14 @@ def volume_get_instance(context, volume_id): return IMPL.volume_get_instance(context, volume_id) -def volume_get_by_project(context, project_id): +def volume_get_all_by_project(context, project_id): """Get all volumes belonging to a project.""" - return IMPL.volume_get_by_project(context, project_id) + return IMPL.volume_get_all_by_project(context, project_id) -def volume_get_by_str(context, str_id): - """Get a volume by string id.""" - return IMPL.volume_get_by_str(context, str_id) +def volume_get_by_ec2_id(context, ec2_id): + """Get a volume by ec2 id.""" + return IMPL.volume_get_by_ec2_id(context, ec2_id) def volume_get_shelf_and_blade(context, volume_id): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e1dad129c..568d7ea61 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -22,12 +22,13 @@ Implementation of SQLAlchemy backend from nova import db from nova import exception from nova import flags +from nova import utils from nova.db.sqlalchemy import models from nova.db.sqlalchemy.session import get_session from sqlalchemy import or_ from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import joinedload_all -from sqlalchemy.sql import func +from sqlalchemy.sql import exists, func FLAGS = flags.FLAGS @@ -62,6 +63,7 @@ def service_get_all_by_topic(context, topic): session = get_session() return session.query(models.Service ).filter_by(deleted=False + ).filter_by(disabled=False ).filter_by(topic=topic ).all() @@ -71,6 +73,7 @@ def _service_get_all_topic_subquery(_context, session, topic, subq, label): return session.query(models.Service, func.coalesce(sort_value, 0) ).filter_by(topic=topic ).filter_by(deleted=False + ).filter_by(disabled=False ).outerjoin((subq, models.Service.host == subq.c.host) ).order_by(sort_value ).all() @@ -164,6 +167,7 @@ def floating_ip_allocate_address(_context, host, project_id): floating_ip_ref = session.query(models.FloatingIp ).filter_by(host=host ).filter_by(fixed_ip_id=None + ).filter_by(project_id=None ).filter_by(deleted=False ).with_lockmode('update' ).first() @@ -251,6 +255,14 @@ def floating_ip_get_all_by_host(_context, host): ).filter_by(deleted=False ).all() +def floating_ip_get_all_by_project(_context, project_id): + session = get_session() + return session.query(models.FloatingIp + ).options(joinedload_all('fixed_ip.instance') + ).filter_by(project_id=project_id + ).filter_by(deleted=False + ).all() + def floating_ip_get_by_address(_context, address): return models.FloatingIp.find_by_str(address) @@ -325,8 +337,32 @@ def fixed_ip_disassociate(_context, address): fixed_ip_ref.save(session=session) +def fixed_ip_disassociate_all_by_timeout(_context, host, time): + session = get_session() + # NOTE(vish): The nested select is because sqlite doesn't support + # JOINs in UPDATEs. + result = session.execute('UPDATE fixed_ips SET instance_id = NULL, ' + 'leased = 0 ' + 'WHERE network_id IN (SELECT id FROM networks ' + 'WHERE host = :host) ' + 'AND updated_at < :time ' + 'AND instance_id IS NOT NULL ' + 'AND allocated = 0', + {'host': host, + 'time': time.isoformat()}) + return result.rowcount + + def fixed_ip_get_by_address(_context, address): - return models.FixedIp.find_by_str(address) + session = get_session() + result = session.query(models.FixedIp + ).options(joinedload_all('instance') + ).filter_by(address=address + ).filter_by(deleted=False + ).first() + if not result: + raise exception.NotFound("No model for address %s" % address) + return result def fixed_ip_get_instance(_context, address): @@ -357,9 +393,15 @@ def instance_create(_context, values): instance_ref = models.Instance() for (key, value) in values.iteritems(): instance_ref[key] = value - instance_ref.save() - return instance_ref + session = get_session() + with session.begin(): + while instance_ref.ec2_id == None: + ec2_id = utils.generate_uid(instance_ref.__prefix__) + if not instance_ec2_id_exists(_context, ec2_id, session=session): + instance_ref.ec2_id = ec2_id + instance_ref.save(session=session) + return instance_ref def instance_data_get_for_project(_context, project_id): session = get_session() @@ -390,8 +432,15 @@ def instance_get_all(context): ).filter_by(deleted=_deleted(context) ).all() +def instance_get_all_by_user(context, user_id): + session = get_session() + return session.query(models.Instance + ).options(joinedload_all('fixed_ip.floating_ips') + ).filter_by(deleted=_deleted(context) + ).filter_by(user_id=user_id + ).all() -def instance_get_by_project(context, project_id): +def instance_get_all_by_project(context, project_id): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -400,7 +449,7 @@ def instance_get_by_project(context, project_id): ).all() -def instance_get_by_reservation(_context, reservation_id): +def instance_get_all_by_reservation(_context, reservation_id): session = get_session() return session.query(models.Instance ).options(joinedload_all('fixed_ip.floating_ips') @@ -409,8 +458,22 @@ def instance_get_by_reservation(_context, reservation_id): ).all() -def instance_get_by_str(context, str_id): - return models.Instance.find_by_str(str_id, deleted=_deleted(context)) +def instance_get_by_ec2_id(context, ec2_id): + session = get_session() + instance_ref = session.query(models.Instance + ).filter_by(ec2_id=ec2_id + ).filter_by(deleted=_deleted(context) + ).first() + if not instance_ref: + raise exception.NotFound('Instance %s not found' % (ec2_id)) + + return instance_ref + + +def instance_ec2_id_exists(context, ec2_id, session=None): + if not session: + session = get_session() + return session.query(exists().where(models.Instance.id==ec2_id)).one()[0] def instance_get_fixed_address(_context, instance_id): @@ -571,6 +634,7 @@ def network_get(_context, network_id): def network_get_associated_fixed_ips(_context, network_id): session = get_session() return session.query(models.FixedIp + ).options(joinedload_all('instance') ).filter_by(network_id=network_id ).filter(models.FixedIp.instance_id != None ).filter_by(deleted=False @@ -608,11 +672,14 @@ def network_index_count(_context): return models.NetworkIndex.count() -def network_index_create(_context, values): +def network_index_create_safe(_context, values): network_index_ref = models.NetworkIndex() for (key, value) in values.iteritems(): network_index_ref[key] = value - network_index_ref.save() + try: + network_index_ref.save() + except IntegrityError: + pass def network_set_host(_context, network_id, host_id): @@ -685,37 +752,6 @@ def export_device_create_safe(_context, values): ################### -def quota_create(_context, values): - quota_ref = models.Quota() - for (key, value) in values.iteritems(): - quota_ref[key] = value - quota_ref.save() - return quota_ref - - -def quota_get(_context, project_id): - return models.Quota.find_by_str(project_id) - - -def quota_update(_context, project_id, values): - session = get_session() - with session.begin(): - quota_ref = models.Quota.find_by_str(project_id, session=session) - for (key, value) in values.iteritems(): - quota_ref[key] = value - quota_ref.save(session=session) - - -def quota_destroy(_context, project_id): - session = get_session() - with session.begin(): - quota_ref = models.Quota.find_by_str(project_id, session=session) - quota_ref.delete(session=session) - - -################### - - def auth_destroy_token(_context, token): session = get_session() session.delete(token) @@ -734,7 +770,7 @@ def auth_create_token(_context, token): tk[k] = v tk.save() return tk - + ################### @@ -803,7 +839,14 @@ def volume_create(_context, values): volume_ref = models.Volume() for (key, value) in values.iteritems(): volume_ref[key] = value - volume_ref.save() + + session = get_session() + with session.begin(): + while volume_ref.ec2_id == None: + ec2_id = utils.generate_uid(volume_ref.__prefix__) + if not volume_ec2_id_exists(_context, ec2_id, session=session): + volume_ref.ec2_id = ec2_id + volume_ref.save(session=session) return volume_ref @@ -848,7 +891,7 @@ def volume_get_all(context): return models.Volume.all(deleted=_deleted(context)) -def volume_get_by_project(context, project_id): +def volume_get_all_by_project(context, project_id): session = get_session() return session.query(models.Volume ).filter_by(project_id=project_id @@ -856,8 +899,22 @@ def volume_get_by_project(context, project_id): ).all() -def volume_get_by_str(context, str_id): - return models.Volume.find_by_str(str_id, deleted=_deleted(context)) +def volume_get_by_ec2_id(context, ec2_id): + session = get_session() + volume_ref = session.query(models.Volume + ).filter_by(ec2_id=ec2_id + ).filter_by(deleted=_deleted(context) + ).first() + if not volume_ref: + raise exception.NotFound('Volume %s not found' % (ec2_id)) + + return volume_ref + + +def volume_ec2_id_exists(context, ec2_id, session=None): + if not session: + session = get_session() + return session.query(exists().where(models.Volume.id==ec2_id)).one()[0] def volume_get_instance(_context, volume_id): diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index a03a1d2d5..5ef6c3271 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -126,6 +126,7 @@ class NovaBase(object): # __tablename__ = 'images' # __prefix__ = 'ami' # id = Column(Integer, primary_key=True) +# ec2_id = Column(String(12), unique=True) # user_id = Column(String(255)) # project_id = Column(String(255)) # image_type = Column(String(255)) @@ -173,6 +174,7 @@ class Service(BASE, NovaBase): binary = Column(String(255)) topic = Column(String(255)) report_count = Column(Integer, nullable=False, default=0) + disabled = Column(Boolean, default=False) @classmethod def find_by_args(cls, host, binary, session=None, deleted=False): @@ -195,6 +197,9 @@ class Instance(BASE, NovaBase): __tablename__ = 'instances' __prefix__ = 'i' id = Column(Integer, primary_key=True) + ec2_id = Column(String(10), unique=True) + + admin_pass = Column(String(255)) user_id = Column(String(255)) project_id = Column(String(255)) @@ -209,12 +214,14 @@ class Instance(BASE, NovaBase): @property def name(self): - return self.str_id + return self.ec2_id image_id = Column(String(255)) kernel_id = Column(String(255)) ramdisk_id = Column(String(255)) + server_name = Column(String(255)) + # image_id = Column(Integer, ForeignKey('images.id'), nullable=True) # kernel_id = Column(Integer, ForeignKey('images.id'), nullable=True) # ramdisk_id = Column(Integer, ForeignKey('images.id'), nullable=True) @@ -234,7 +241,6 @@ class Instance(BASE, NovaBase): vcpus = Column(Integer) local_gb = Column(Integer) - hostname = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) @@ -248,6 +254,10 @@ class Instance(BASE, NovaBase): scheduled_at = Column(DateTime) launched_at = Column(DateTime) terminated_at = Column(DateTime) + + display_name = Column(String(255)) + display_description = Column(String(255)) + # TODO(vish): see Ewan's email about state improvements, probably # should be in a driver base class or some such # vmstate_state = running, halted, suspended, paused @@ -265,6 +275,7 @@ class Volume(BASE, NovaBase): __tablename__ = 'volumes' __prefix__ = 'vol' id = Column(Integer, primary_key=True) + ec2_id = Column(String(12), unique=True) user_id = Column(String(255)) project_id = Column(String(255)) @@ -283,6 +294,10 @@ class Volume(BASE, NovaBase): launched_at = Column(DateTime) terminated_at = Column(DateTime) + display_name = Column(String(255)) + display_description = Column(String(255)) + + class Quota(BASE, NovaBase): """Represents quota overrides for a project""" __tablename__ = 'quotas' @@ -393,14 +408,14 @@ class NetworkIndex(BASE, NovaBase): """ __tablename__ = 'network_indexes' id = Column(Integer, primary_key=True) - index = Column(Integer) + index = Column(Integer, unique=True) network_id = Column(Integer, ForeignKey('networks.id'), nullable=True) network = relationship(Network, backref=backref('network_index', uselist=False)) class AuthToken(BASE, NovaBase): - """Represents an authorization token for all API transactions. Fields - are a string representing the actual token and a user id for mapping + """Represents an authorization token for all API transactions. Fields + are a string representing the actual token and a user id for mapping to the actual user""" __tablename__ = 'auth_tokens' token_hash = Column(String(255), primary_key=True) @@ -455,10 +470,6 @@ class FloatingIp(BASE, NovaBase): project_id = Column(String(255)) host = Column(String(255)) # , ForeignKey('hosts.id')) - @property - def str_id(self): - return self.address - @classmethod def find_by_str(cls, str_id, session=None, deleted=False): if not session: diff --git a/nova/flags.py b/nova/flags.py index 6a1c14490..c32cdd7a4 100644 --- a/nova/flags.py +++ b/nova/flags.py @@ -188,6 +188,8 @@ DEFINE_string('rabbit_userid', 'guest', 'rabbit userid') DEFINE_string('rabbit_password', 'guest', 'rabbit password') DEFINE_string('rabbit_virtual_host', '/', 'rabbit virtual host') DEFINE_string('control_exchange', 'nova', 'the main exchange to connect to') +DEFINE_string('cc_host', '127.0.0.1', 'ip of api server') +DEFINE_integer('cc_port', 8773, 'cloud controller port') DEFINE_string('ec2_url', 'http://127.0.0.1:8773/services/Cloud', 'Url to ec2 api server') diff --git a/nova/manager.py b/nova/manager.py index e9aa50c56..56ba7d3f6 100644 --- a/nova/manager.py +++ b/nova/manager.py @@ -22,6 +22,7 @@ Base class for managers of different parts of the system from nova import utils from nova import flags +from twisted.internet import defer FLAGS = flags.FLAGS flags.DEFINE_string('db_driver', 'nova.db.api', @@ -37,3 +38,15 @@ class Manager(object): if not db_driver: db_driver = FLAGS.db_driver self.db = utils.import_object(db_driver) # pylint: disable-msg=C0103 + + @defer.inlineCallbacks + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval""" + yield + + def init_host(self): + """Do any initialization that needs to be run if this is a standalone service. + + Child classes should override this method. + """ + pass diff --git a/nova/network/linux_net.py b/nova/network/linux_net.py index 41aeb5da7..709195ba4 100644 --- a/nova/network/linux_net.py +++ b/nova/network/linux_net.py @@ -36,13 +36,34 @@ flags.DEFINE_string('dhcpbridge_flagfile', flags.DEFINE_string('networks_path', utils.abspath('../networks'), 'Location to keep network config files') flags.DEFINE_string('public_interface', 'vlan1', - 'Interface for public IP addresses') + 'Interface for public IP addresses') flags.DEFINE_string('bridge_dev', 'eth0', - 'network device for bridges') - + 'network device for bridges') +flags.DEFINE_string('routing_source_ip', '127.0.0.1', + 'Public IP of network host') +flags.DEFINE_bool('use_nova_chains', False, + 'use the nova_ routing chains instead of default') DEFAULT_PORTS = [("tcp", 80), ("tcp", 22), ("udp", 1194), ("tcp", 443)] +def init_host(): + """Basic networking setup goes here""" + # NOTE(devcamcar): Cloud public DNAT entries, CloudPipe port + # forwarding entries and a default DNAT entry. + _confirm_rule("PREROUTING", "-t nat -s 0.0.0.0/0 " + "-d 169.254.169.254/32 -p tcp -m tcp --dport 80 -j DNAT " + "--to-destination %s:%s" % (FLAGS.cc_host, FLAGS.cc_port)) + + # NOTE(devcamcar): Cloud public SNAT entries and the default + # SNAT rule for outbound traffic. + _confirm_rule("POSTROUTING", "-t nat -s %s " + "-j SNAT --to-source %s" + % (FLAGS.private_range, FLAGS.routing_source_ip)) + + _confirm_rule("POSTROUTING", "-t nat -s %s -j MASQUERADE" % + FLAGS.private_range) + _confirm_rule("POSTROUTING", "-t nat -s %(range)s -d %(range)s -j ACCEPT" % + {'range': FLAGS.private_range}) def bind_floating_ip(floating_ip): """Bind ip to public interface""" @@ -58,37 +79,37 @@ def unbind_floating_ip(floating_ip): def ensure_vlan_forward(public_ip, port, private_ip): """Sets up forwarding rules for vlan""" - _confirm_rule("FORWARD -d %s -p udp --dport 1194 -j ACCEPT" % private_ip) - _confirm_rule( - "PREROUTING -t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" + _confirm_rule("FORWARD", "-d %s -p udp --dport 1194 -j ACCEPT" % + private_ip) + _confirm_rule("PREROUTING", + "-t nat -d %s -p udp --dport %s -j DNAT --to %s:1194" % (public_ip, port, private_ip)) def ensure_floating_forward(floating_ip, fixed_ip): """Ensure floating ip forwarding rule""" - _confirm_rule("PREROUTING -t nat -d %s -j DNAT --to %s" + _confirm_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s" % (floating_ip, fixed_ip)) - _confirm_rule("POSTROUTING -t nat -s %s -j SNAT --to %s" + _confirm_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s" % (fixed_ip, floating_ip)) # TODO(joshua): Get these from the secgroup datastore entries - _confirm_rule("FORWARD -d %s -p icmp -j ACCEPT" + _confirm_rule("FORWARD", "-d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: - _confirm_rule( - "FORWARD -d %s -p %s --dport %s -j ACCEPT" + _confirm_rule("FORWARD","-d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) def remove_floating_forward(floating_ip, fixed_ip): """Remove forwarding for floating ip""" - _remove_rule("PREROUTING -t nat -d %s -j DNAT --to %s" + _remove_rule("PREROUTING", "-t nat -d %s -j DNAT --to %s" % (floating_ip, fixed_ip)) - _remove_rule("POSTROUTING -t nat -s %s -j SNAT --to %s" + _remove_rule("POSTROUTING", "-t nat -s %s -j SNAT --to %s" % (fixed_ip, floating_ip)) - _remove_rule("FORWARD -d %s -p icmp -j ACCEPT" + _remove_rule("FORWARD", "-d %s -p icmp -j ACCEPT" % (fixed_ip)) for (protocol, port) in DEFAULT_PORTS: - _remove_rule("FORWARD -d %s -p %s --dport %s -j ACCEPT" + _remove_rule("FORWARD", "-d %s -p %s --dport %s -j ACCEPT" % (fixed_ip, protocol, port)) @@ -124,16 +145,18 @@ def ensure_bridge(bridge, interface, net_attrs=None): net_attrs['gateway'], net_attrs['broadcast'], net_attrs['netmask'])) - _confirm_rule("FORWARD --in-interface %s -j ACCEPT" % bridge) else: _execute("sudo ifconfig %s up" % bridge) + _confirm_rule("FORWARD", "--in-interface %s -j ACCEPT" % bridge) + _confirm_rule("FORWARD", "--out-interface %s -j ACCEPT" % bridge) def get_dhcp_hosts(context, network_id): """Get a string containing a network's hosts config in dnsmasq format""" hosts = [] - for fixed_ip in db.network_get_associated_fixed_ips(context, network_id): - hosts.append(_host_dhcp(fixed_ip['str_id'])) + for fixed_ip_ref in db.network_get_associated_fixed_ips(context, + network_id): + hosts.append(_host_dhcp(fixed_ip_ref)) return '\n'.join(hosts) @@ -171,12 +194,12 @@ def update_dhcp(context, network_id): _execute(command, addl_env=env) -def _host_dhcp(address): +def _host_dhcp(fixed_ip_ref): """Return a host string for an address""" - instance_ref = db.fixed_ip_get_instance(None, address) + instance_ref = fixed_ip_ref['instance'] return "%s,%s.novalocal,%s" % (instance_ref['mac_address'], instance_ref['hostname'], - address) + fixed_ip_ref['address']) def _execute(cmd, *args, **kwargs): @@ -194,15 +217,19 @@ def _device_exists(device): return not err -def _confirm_rule(cmd): +def _confirm_rule(chain, cmd): """Delete and re-add iptables rule""" - _execute("sudo iptables --delete %s" % (cmd), check_exit_code=False) - _execute("sudo iptables -I %s" % (cmd)) + if FLAGS.use_nova_chains: + chain = "nova_%s" % chain.lower() + _execute("sudo iptables --delete %s %s" % (chain, cmd), check_exit_code=False) + _execute("sudo iptables -I %s %s" % (chain, cmd)) -def _remove_rule(cmd): +def _remove_rule(chain, cmd): """Remove iptables rule""" - _execute("sudo iptables --delete %s" % (cmd)) + if FLAGS.use_nova_chains: + chain = "%S" % chain.lower() + _execute("sudo iptables --delete %s %s" % (chain, cmd)) def _dnsmasq_cmd(net): diff --git a/nova/network/manager.py b/nova/network/manager.py index 191c1d364..1325c300b 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -20,10 +20,12 @@ Network Hosts are responsible for allocating ips and setting up network """ +import datetime import logging import math import IPy +from twisted.internet import defer from nova import db from nova import exception @@ -62,7 +64,9 @@ flags.DEFINE_integer('cnt_vpn_clients', 5, flags.DEFINE_string('network_driver', 'nova.network.linux_net', 'Driver to use for network creation') flags.DEFINE_bool('update_dhcp_on_disassociate', False, - 'Whether to update dhcp when fixed_ip is disassocated') + 'Whether to update dhcp when fixed_ip is disassociated') +flags.DEFINE_integer('fixed_ip_disassociate_timeout', 600, + 'Seconds after which a deallocated ip is disassociated') class AddressAlreadyAllocated(exception.Error): @@ -218,6 +222,26 @@ class FlatManager(NetworkManager): class VlanManager(NetworkManager): """Vlan network with dhcp""" + + @defer.inlineCallbacks + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval""" + yield super(VlanManager, self).periodic_tasks(context) + now = datetime.datetime.utcnow() + timeout = FLAGS.fixed_ip_disassociate_timeout + time = now - datetime.timedelta(seconds=timeout) + num = self.db.fixed_ip_disassociate_all_by_timeout(self, + self.host, + time) + if num: + logging.debug("Dissassociated %s stale fixed ip(s)", num) + + def init_host(self): + """Do any initialization that needs to be run if this is a + standalone service. + """ + self.driver.init_host() + def allocate_fixed_ip(self, context, instance_id, *args, **kwargs): """Gets a fixed ip from the pool""" network_ref = self.db.project_get_network(context, context.project.id) @@ -235,14 +259,6 @@ class VlanManager(NetworkManager): """Returns a fixed ip to the pool""" self.db.fixed_ip_update(context, address, {'allocated': False}) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - if not fixed_ip_ref['leased']: - self.db.fixed_ip_disassociate(context, address) - # NOTE(vish): dhcp server isn't updated until next setup, this - # means there will stale entries in the conf file - # the code below will update the file if necessary - if FLAGS.update_dhcp_on_disassociate: - network_ref = self.db.fixed_ip_get_network(context, address) - self.driver.update_dhcp(context, network_ref['id']) def setup_fixed_ip(self, context, address): @@ -259,10 +275,7 @@ class VlanManager(NetworkManager): """Called by dhcp-bridge when ip is leased""" logging.debug("Leasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - if not fixed_ip_ref['allocated']: - logging.warn("IP %s leased that was already deallocated", address) - return - instance_ref = self.db.fixed_ip_get_instance(context, address) + instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error("IP %s leased that isn't associated" % address) @@ -270,24 +283,27 @@ class VlanManager(NetworkManager): raise exception.Error("IP %s leased to bad mac %s vs %s" % (address, instance_ref['mac_address'], mac)) self.db.fixed_ip_update(context, - fixed_ip_ref['str_id'], + fixed_ip_ref['address'], {'leased': True}) + if not fixed_ip_ref['allocated']: + logging.warn("IP %s leased that was already deallocated", address) def release_fixed_ip(self, context, mac, address): """Called by dhcp-bridge when ip is released""" logging.debug("Releasing IP %s", address) fixed_ip_ref = self.db.fixed_ip_get_by_address(context, address) - if not fixed_ip_ref['leased']: - logging.warn("IP %s released that was not leased", address) - return - instance_ref = self.db.fixed_ip_get_instance(context, address) + instance_ref = fixed_ip_ref['instance'] if not instance_ref: raise exception.Error("IP %s released that isn't associated" % address) if instance_ref['mac_address'] != mac: raise exception.Error("IP %s released from bad mac %s vs %s" % (address, instance_ref['mac_address'], mac)) - self.db.fixed_ip_update(context, address, {'leased': False}) + if not fixed_ip_ref['leased']: + logging.warn("IP %s released that was not leased", address) + self.db.fixed_ip_update(context, + fixed_ip_ref['str_id'], + {'leased': False}) if not fixed_ip_ref['allocated']: self.db.fixed_ip_disassociate(context, address) # NOTE(vish): dhcp server isn't updated until next setup, this @@ -343,7 +359,7 @@ class VlanManager(NetworkManager): This could use a manage command instead of keying off of a flag""" if not self.db.network_index_count(context): for index in range(FLAGS.num_networks): - self.db.network_index_create(context, {'index': index}) + self.db.network_index_create_safe(context, {'index': index}) def _on_set_network_host(self, context, network_id): """Called when this host becomes the host for a project""" diff --git a/nova/objectstore/__init__.py b/nova/objectstore/__init__.py index b8890ac03..ecad9be7c 100644 --- a/nova/objectstore/__init__.py +++ b/nova/objectstore/__init__.py @@ -22,7 +22,7 @@ .. automodule:: nova.objectstore :platform: Unix - :synopsis: Currently a trivial file-based system, getting extended w/ mongo. + :synopsis: Currently a trivial file-based system, getting extended w/ swift. .. moduleauthor:: Jesse Andrews <jesse@ansolabs.com> .. moduleauthor:: Devin Carlen <devin.carlen@gmail.com> .. moduleauthor:: Vishvananda Ishaya <vishvananda@yahoo.com> diff --git a/nova/objectstore/handler.py b/nova/objectstore/handler.py index aabf6831f..dfee64aca 100644 --- a/nova/objectstore/handler.py +++ b/nova/objectstore/handler.py @@ -352,6 +352,8 @@ class ImagesResource(resource.Resource): m[u'imageType'] = m['type'] elif 'imageType' in m: m[u'type'] = m['imageType'] + if 'displayName' not in m: + m[u'displayName'] = u'' return m request.write(json.dumps([decorate(i.metadata) for i in images])) @@ -382,16 +384,25 @@ class ImagesResource(resource.Resource): def render_POST(self, request): # pylint: disable-msg=R0201 """Update image attributes: public/private""" + # image_id required for all requests image_id = get_argument(request, 'image_id', u'') - operation = get_argument(request, 'operation', u'') - image_object = image.Image(image_id) - if not image_object.is_authorized(request.context): + logging.debug("not authorized for render_POST in images") raise exception.NotAuthorized - image_object.set_public(operation=='add') - + operation = get_argument(request, 'operation', u'') + if operation: + # operation implies publicity toggle + logging.debug("handling publicity toggle") + image_object.set_public(operation=='add') + else: + # other attributes imply update + logging.debug("update user fields") + clean_args = {} + for arg in request.args.keys(): + clean_args[arg] = request.args[arg][0] + image_object.update_user_editable_fields(clean_args) return '' def render_DELETE(self, request): # pylint: disable-msg=R0201 diff --git a/nova/objectstore/image.py b/nova/objectstore/image.py index f3c02a425..def1b8167 100644 --- a/nova/objectstore/image.py +++ b/nova/objectstore/image.py @@ -82,6 +82,16 @@ class Image(object): with open(os.path.join(self.path, 'info.json'), 'w') as f: json.dump(md, f) + def update_user_editable_fields(self, args): + """args is from the request parameters, so requires extra cleaning""" + fields = {'display_name': 'displayName', 'description': 'description'} + info = self.metadata + for field in fields.keys(): + if field in args: + info[fields[field]] = args[field] + with open(os.path.join(self.path, 'info.json'), 'w') as f: + json.dump(info, f) + @staticmethod def all(): images = [] diff --git a/nova/rpc.py b/nova/rpc.py index 6363335ea..fe52ad35f 100644 --- a/nova/rpc.py +++ b/nova/rpc.py @@ -84,19 +84,6 @@ class Consumer(messaging.Consumer): self.failed_connection = False super(Consumer, self).__init__(*args, **kwargs) - # TODO(termie): it would be nice to give these some way of automatically - # cleaning up after themselves - def attach_to_tornado(self, io_inst=None): - """Attach a callback to tornado that fires 10 times a second""" - from tornado import ioloop - if io_inst is None: - io_inst = ioloop.IOLoop.instance() - - injected = ioloop.PeriodicCallback( - lambda: self.fetch(enable_callbacks=True), 100, io_loop=io_inst) - injected.start() - return injected - def fetch(self, no_ack=None, auto_ack=None, enable_callbacks=False): """Wraps the parent fetch with some logic for failed connections""" # TODO(vish): the logic for failed connections and logging should be @@ -124,6 +111,7 @@ class Consumer(messaging.Consumer): """Attach a callback to twisted that fires 10 times a second""" loop = task.LoopingCall(self.fetch, enable_callbacks=True) loop.start(interval=0.1) + return loop class Publisher(messaging.Publisher): @@ -294,6 +282,37 @@ def call(topic, msg): return wait_msg.result +def call_twisted(topic, msg): + """Sends a message on a topic and wait for a response""" + LOG.debug("Making asynchronous call...") + msg_id = uuid.uuid4().hex + msg.update({'_msg_id': msg_id}) + LOG.debug("MSG_ID is %s" % (msg_id)) + + conn = Connection.instance() + d = defer.Deferred() + consumer = DirectConsumer(connection=conn, msg_id=msg_id) + + def deferred_receive(data, message): + """Acks message and callbacks or errbacks""" + message.ack() + if data['failure']: + return d.errback(RemoteError(*data['failure'])) + else: + return d.callback(data['result']) + + consumer.register_callback(deferred_receive) + injected = consumer.attach_to_twisted() + + # clean up after the injected listened and return x + d.addCallback(lambda x: injected.stop() and x or x) + + publisher = TopicPublisher(connection=conn, topic=topic) + publisher.send(msg) + publisher.close() + return d + + def cast(topic, msg): """Sends a message on a topic without waiting for a response""" LOG.debug("Making asynchronous cast...") diff --git a/nova/service.py b/nova/service.py index 870dd6ceb..a6c186896 100644 --- a/nova/service.py +++ b/nova/service.py @@ -37,7 +37,11 @@ from nova import utils FLAGS = flags.FLAGS flags.DEFINE_integer('report_interval', 10, - 'seconds between nodes reporting state to cloud', + 'seconds between nodes reporting state to datastore', + lower_bound=1) + +flags.DEFINE_integer('periodic_interval', 60, + 'seconds between running periodic tasks', lower_bound=1) @@ -50,6 +54,7 @@ class Service(object, service.Service): self.topic = topic manager_class = utils.import_class(manager) self.manager = manager_class(host=host, *args, **kwargs) + self.manager.init_host() self.model_disconnected = False super(Service, self).__init__(*args, **kwargs) try: @@ -80,7 +85,8 @@ class Service(object, service.Service): binary=None, topic=None, manager=None, - report_interval=None): + report_interval=None, + periodic_interval=None): """Instantiates class and passes back application object. Args: @@ -89,6 +95,7 @@ class Service(object, service.Service): topic, defaults to bin_name - "nova-" part manager, defaults to FLAGS.<topic>_manager report_interval, defaults to FLAGS.report_interval + periodic_interval, defaults to FLAGS.periodic_interval """ if not host: host = FLAGS.host @@ -100,6 +107,8 @@ class Service(object, service.Service): manager = FLAGS.get('%s_manager' % topic, None) if not report_interval: report_interval = FLAGS.report_interval + if not periodic_interval: + periodic_interval = FLAGS.periodic_interval logging.warn("Starting %s node", topic) service_obj = cls(host, binary, topic, manager) conn = rpc.Connection.instance() @@ -112,11 +121,14 @@ class Service(object, service.Service): topic='%s.%s' % (topic, host), proxy=service_obj) + consumer_all.attach_to_twisted() + consumer_node.attach_to_twisted() + pulse = task.LoopingCall(service_obj.report_state) pulse.start(interval=report_interval, now=False) - consumer_all.attach_to_twisted() - consumer_node.attach_to_twisted() + pulse = task.LoopingCall(service_obj.periodic_tasks) + pulse.start(interval=periodic_interval, now=False) # This is the parent service that twistd will be looking for when it # parses this file, return it so that we can get it into globals. @@ -132,6 +144,11 @@ class Service(object, service.Service): logging.warn("Service killed that has no database entry") @defer.inlineCallbacks + def periodic_tasks(self, context=None): + """Tasks to be run at a periodic interval""" + yield self.manager.periodic_tasks(context) + + @defer.inlineCallbacks def report_state(self, context=None): """Update the state of this service in the datastore.""" try: diff --git a/nova/test.py b/nova/test.py index c392c8a84..1f4b33272 100644 --- a/nova/test.py +++ b/nova/test.py @@ -33,6 +33,7 @@ from twisted.trial import unittest from nova import fakerabbit from nova import flags +from nova import rpc FLAGS = flags.FLAGS @@ -62,19 +63,29 @@ class TrialTestCase(unittest.TestCase): self.mox = mox.Mox() self.stubs = stubout.StubOutForTesting() self.flag_overrides = {} + self.injected = [] + self._monkeyPatchAttach() def tearDown(self): # pylint: disable-msg=C0103 """Runs after each test method to finalize/tear down test environment""" - super(TrialTestCase, self).tearDown() self.reset_flags() self.mox.UnsetStubs() self.stubs.UnsetAll() self.stubs.SmartUnsetAll() self.mox.VerifyAll() + + rpc.Consumer.attach_to_twisted = self.originalAttach + for x in self.injected: + try: + x.stop() + except AssertionError: + pass if FLAGS.fake_rabbit: fakerabbit.reset_all() + super(TrialTestCase, self).tearDown() + def flags(self, **kw): """Override flag variables for a test""" for k, v in kw.iteritems(): @@ -90,16 +101,51 @@ class TrialTestCase(unittest.TestCase): for k, v in self.flag_overrides.iteritems(): setattr(FLAGS, k, v) + def run(self, result=None): + test_method = getattr(self, self._testMethodName) + setattr(self, + self._testMethodName, + self._maybeInlineCallbacks(test_method, result)) + rv = super(TrialTestCase, self).run(result) + setattr(self, self._testMethodName, test_method) + return rv + + def _maybeInlineCallbacks(self, func, result): + def _wrapped(): + g = func() + if isinstance(g, defer.Deferred): + return g + if not hasattr(g, 'send'): + return defer.succeed(g) + + inlined = defer.inlineCallbacks(func) + d = inlined() + return d + _wrapped.func_name = func.func_name + return _wrapped + + def _monkeyPatchAttach(self): + self.originalAttach = rpc.Consumer.attach_to_twisted + def _wrapped(innerSelf): + rv = self.originalAttach(innerSelf) + self.injected.append(rv) + return rv + + _wrapped.func_name = self.originalAttach.func_name + rpc.Consumer.attach_to_twisted = _wrapped + class BaseTestCase(TrialTestCase): # TODO(jaypipes): Can this be moved into the TrialTestCase class? - """Base test case class for all unit tests.""" + """Base test case class for all unit tests. + + DEPRECATED: This is being removed once Tornado is gone, use TrialTestCase. + """ def setUp(self): # pylint: disable-msg=C0103 """Run before each test method to initialize test environment""" super(BaseTestCase, self).setUp() # TODO(termie): we could possibly keep a more global registry of # the injected listeners... this is fine for now though - self.injected = [] self.ioloop = ioloop.IOLoop.instance() self._waiting = None @@ -109,8 +155,6 @@ class BaseTestCase(TrialTestCase): def tearDown(self):# pylint: disable-msg=C0103 """Runs after each test method to finalize/tear down test environment""" super(BaseTestCase, self).tearDown() - for x in self.injected: - x.stop() if FLAGS.fake_rabbit: fakerabbit.reset_all() diff --git a/nova/tests/access_unittest.py b/nova/tests/access_unittest.py index c8a49d2ca..4b40ffd0a 100644 --- a/nova/tests/access_unittest.py +++ b/nova/tests/access_unittest.py @@ -31,7 +31,7 @@ FLAGS = flags.FLAGS class Context(object): pass -class AccessTestCase(test.BaseTestCase): +class AccessTestCase(test.TrialTestCase): def setUp(self): super(AccessTestCase, self).setUp() um = manager.AuthManager() diff --git a/nova/tests/api/rackspace/__init__.py b/nova/tests/api/rackspace/__init__.py index 622cb4335..bfd0f87a7 100644 --- a/nova/tests/api/rackspace/__init__.py +++ b/nova/tests/api/rackspace/__init__.py @@ -17,6 +17,7 @@ import unittest +from nova.api.rackspace import limited from nova.api.rackspace import RateLimitingMiddleware from nova.tests.api.test_helper import * from webob import Request @@ -77,3 +78,31 @@ class RateLimitingMiddlewareTest(unittest.TestCase): self.assertEqual(middleware.limiter.__class__.__name__, "Limiter") middleware = RateLimitingMiddleware(APIStub(), service_host='foobar') self.assertEqual(middleware.limiter.__class__.__name__, "WSGIAppProxy") + + +class LimiterTest(unittest.TestCase): + + def testLimiter(self): + items = range(2000) + req = Request.blank('/') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=3') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=2005') + self.assertEqual(limited(items, req), []) + req = Request.blank('/?limit=10') + self.assertEqual(limited(items, req), items[ :10]) + req = Request.blank('/?limit=0') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?limit=3000') + self.assertEqual(limited(items, req), items[ :1000]) + req = Request.blank('/?offset=1&limit=3') + self.assertEqual(limited(items, req), items[1:4]) + req = Request.blank('/?offset=3&limit=0') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3&limit=1500') + self.assertEqual(limited(items, req), items[3:1003]) + req = Request.blank('/?offset=3000&limit=10') + self.assertEqual(limited(items, req), []) diff --git a/nova/tests/api/rackspace/auth.py b/nova/tests/api/rackspace/auth.py index 429c22ad2..56677c2f4 100644 --- a/nova/tests/api/rackspace/auth.py +++ b/nova/tests/api/rackspace/auth.py @@ -1,12 +1,14 @@ -import webob -import webob.dec +import datetime import unittest + import stubout +import webob +import webob.dec + import nova.api import nova.api.rackspace.auth from nova import auth from nova.tests.api.rackspace import test_helper -import datetime class Test(unittest.TestCase): def setUp(self): @@ -15,8 +17,8 @@ class Test(unittest.TestCase): '__init__', test_helper.fake_auth_init) test_helper.FakeAuthManager.auth_data = {} test_helper.FakeAuthDatabase.data = {} - self.stubs.Set(nova.api.rackspace, 'RateLimitingMiddleware', - test_helper.FakeRateLimiter) + test_helper.stub_out_rate_limiting(self.stubs) + test_helper.stub_for_testing(self.stubs) def tearDown(self): self.stubs.UnsetAll() diff --git a/nova/tests/api/rackspace/flavors.py b/nova/tests/api/rackspace/flavors.py index fb8ba94a5..d25a2e2be 100644 --- a/nova/tests/api/rackspace/flavors.py +++ b/nova/tests/api/rackspace/flavors.py @@ -16,19 +16,31 @@ # under the License. import unittest +import stubout +import nova.api from nova.api.rackspace import flavors +from nova.tests.api.rackspace import test_helper from nova.tests.api.test_helper import * class FlavorsTest(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} + test_helper.stub_for_testing(self.stubs) + test_helper.stub_out_rate_limiting(self.stubs) + test_helper.stub_out_auth(self.stubs) def tearDown(self): self.stubs.UnsetAll() def test_get_flavor_list(self): - pass + req = webob.Request.blank('/v1.0/flavors') + res = req.get_response(nova.api.API()) def test_get_flavor_by_id(self): pass + +if __name__ == '__main__': + unittest.main() diff --git a/nova/tests/api/rackspace/images.py b/nova/tests/api/rackspace/images.py index 560d8c898..4c9987e8b 100644 --- a/nova/tests/api/rackspace/images.py +++ b/nova/tests/api/rackspace/images.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import stubout import unittest from nova.api.rackspace import images diff --git a/nova/tests/api/rackspace/servers.py b/nova/tests/api/rackspace/servers.py index 6d628e78a..69ad2c1d3 100644 --- a/nova/tests/api/rackspace/servers.py +++ b/nova/tests/api/rackspace/servers.py @@ -15,44 +15,231 @@ # License for the specific language governing permissions and limitations # under the License. +import json import unittest +import stubout + +from nova import db +from nova import flags +import nova.api.rackspace from nova.api.rackspace import servers +import nova.db.api +from nova.db.sqlalchemy.models import Instance +import nova.rpc from nova.tests.api.test_helper import * +from nova.tests.api.rackspace import test_helper + +FLAGS = flags.FLAGS + +def return_server(context, id): + return stub_instance(id) + +def return_servers(context, user_id=1): + return [stub_instance(i, user_id) for i in xrange(5)] + + +def stub_instance(id, user_id=1): + return Instance( + id=id, state=0, image_id=10, server_name='server%s'%id, + user_id=user_id + ) class ServersTest(unittest.TestCase): def setUp(self): self.stubs = stubout.StubOutForTesting() + test_helper.FakeAuthManager.auth_data = {} + test_helper.FakeAuthDatabase.data = {} + test_helper.stub_for_testing(self.stubs) + test_helper.stub_out_rate_limiting(self.stubs) + test_helper.stub_out_auth(self.stubs) + test_helper.stub_out_id_translator(self.stubs) + test_helper.stub_out_key_pair_funcs(self.stubs) + test_helper.stub_out_image_service(self.stubs) + self.stubs.Set(nova.db.api, 'instance_get_all', return_servers) + self.stubs.Set(nova.db.api, 'instance_get_by_ec2_id', return_server) + self.stubs.Set(nova.db.api, 'instance_get_all_by_user', + return_servers) def tearDown(self): self.stubs.UnsetAll() + def test_get_server_by_id(self): + req = webob.Request.blank('/v1.0/servers/1') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], '1') + self.assertEqual(res_dict['server']['name'], 'server1') + def test_get_server_list(self): - pass + req = webob.Request.blank('/v1.0/servers') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s.get('imageId', None), None) + i += 1 def test_create_instance(self): - pass + def server_update(context, id, params): + pass - def test_get_server_by_id(self): - pass + def instance_create(context, inst): + class Foo(object): + ec2_id = 1 + return Foo() + + def fake_method(*args, **kwargs): + pass + + def project_get_network(context, user_id): + return dict(id='1', host='localhost') + + def queue_get_for(context, *args): + return 'network_topic' + + self.stubs.Set(nova.db.api, 'project_get_network', project_get_network) + self.stubs.Set(nova.db.api, 'instance_create', instance_create) + self.stubs.Set(nova.rpc, 'cast', fake_method) + self.stubs.Set(nova.rpc, 'call', fake_method) + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + self.stubs.Set(nova.db.api, 'queue_get_for', queue_get_for) + self.stubs.Set(nova.network.manager.FlatManager, 'allocate_fixed_ip', + fake_method) + + test_helper.stub_out_id_translator(self.stubs) + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers') + req.method = 'POST' + req.body = json.dumps(body) + + res = req.get_response(nova.api.API()) + + self.assertEqual(res.status_int, 200) - def test_get_backup_schedule(self): - pass + def test_update_no_body(self): + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status_int, 422) - def test_get_server_details(self): - pass + def test_update_bad_params(self): + """ Confirm that update is filtering params """ + inst_dict = dict(cat='leopard', name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) - def test_get_server_ips(self): - pass + def server_update(context, id, params): + self.update_called = True + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) + + def test_update_server(self): + inst_dict = dict(name='server_test', adminPass='bacon') + self.body = json.dumps(dict(server=inst_dict)) + + def server_update(context, id, params): + filtered_dict = dict(name='server_test', admin_pass='bacon') + self.assertEqual(params, filtered_dict) + + self.stubs.Set(nova.db.api, 'instance_update', + server_update) + + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'PUT' + req.body = self.body + req.get_response(nova.api.API()) + + def test_create_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'POST' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_delete_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + req.method = 'DELETE' + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_server_backup_schedules(self): + req = webob.Request.blank('/v1.0/servers/1/backup_schedules') + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '404 Not Found') + + def test_get_all_server_details(self): + req = webob.Request.blank('/v1.0/servers/detail') + res = req.get_response(nova.api.API()) + res_dict = json.loads(res.body) + + i = 0 + for s in res_dict['servers']: + self.assertEqual(s['id'], i) + self.assertEqual(s['name'], 'server%d'%i) + self.assertEqual(s['imageId'], 10) + i += 1 def test_server_reboot(self): - pass + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) def test_server_rebuild(self): - pass + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) def test_server_resize(self): - pass + body = dict(server=dict( + name='server_test', imageId=2, flavorId=2, metadata={}, + personality = {} + )) + req = webob.Request.blank('/v1.0/servers/1/action') + req.method = 'POST' + req.content_type= 'application/json' + req.body = json.dumps(body) + res = req.get_response(nova.api.API()) def test_delete_server_instance(self): - pass + req = webob.Request.blank('/v1.0/servers/1') + req.method = 'DELETE' + + self.server_delete_called = False + def instance_destroy_mock(context, id): + self.server_delete_called = True + + self.stubs.Set(nova.db.api, 'instance_destroy', + instance_destroy_mock) + + res = req.get_response(nova.api.API()) + self.assertEqual(res.status, '202 Accepted') + self.assertEqual(self.server_delete_called, True) + +if __name__ == "__main__": + unittest.main() diff --git a/nova/tests/api/rackspace/sharedipgroups.py b/nova/tests/api/rackspace/sharedipgroups.py index b4b281db7..1906b54f5 100644 --- a/nova/tests/api/rackspace/sharedipgroups.py +++ b/nova/tests/api/rackspace/sharedipgroups.py @@ -15,6 +15,7 @@ # License for the specific language governing permissions and limitations # under the License. +import stubout import unittest from nova.api.rackspace import sharedipgroups diff --git a/nova/tests/api/rackspace/test_helper.py b/nova/tests/api/rackspace/test_helper.py index be14e2de8..2cf154f63 100644 --- a/nova/tests/api/rackspace/test_helper.py +++ b/nova/tests/api/rackspace/test_helper.py @@ -1,8 +1,18 @@ +import datetime +import json + import webob import webob.dec -import datetime -from nova.wsgi import Router + from nova import auth +from nova import utils +from nova import flags +import nova.api.rackspace.auth +import nova.api.rackspace._id_translator +from nova.image import service +from nova.wsgi import Router + +FLAGS = flags.FLAGS class Context(object): pass @@ -24,6 +34,65 @@ def fake_auth_init(self): self.auth = FakeAuthManager() self.host = 'foo' +@webob.dec.wsgify +def fake_wsgi(self, req): + req.environ['nova.context'] = dict(user=dict(id=1)) + if req.body: + req.environ['inst_dict'] = json.loads(req.body) + return self.application + +def stub_out_key_pair_funcs(stubs): + def key_pair(context, user_id): + return [dict(name='key', public_key='public_key')] + stubs.Set(nova.db.api, 'key_pair_get_all_by_user', + key_pair) + +def stub_out_image_service(stubs): + def fake_image_show(meh, id): + return dict(kernelId=1, ramdiskId=1) + + stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show) + +def stub_out_id_translator(stubs): + class FakeTranslator(object): + def __init__(self, id_type, service_name): + pass + + def to_rs_id(self, id): + return id + + def from_rs_id(self, id): + return id + + stubs.Set(nova.api.rackspace._id_translator, + 'RackspaceAPIIdTranslator', FakeTranslator) + +def stub_out_auth(stubs): + def fake_auth_init(self, app): + self.application = app + + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__init__', fake_auth_init) + stubs.Set(nova.api.rackspace.AuthMiddleware, + '__call__', fake_wsgi) + +def stub_out_rate_limiting(stubs): + def fake_rate_init(self, app): + super(nova.api.rackspace.RateLimitingMiddleware, self).__init__(app) + self.application = app + + stubs.Set(nova.api.rackspace.RateLimitingMiddleware, + '__init__', fake_rate_init) + + stubs.Set(nova.api.rackspace.RateLimitingMiddleware, + '__call__', fake_wsgi) + +def stub_for_testing(stubs): + def get_my_ip(): + return '127.0.0.1' + stubs.Set(nova.utils, 'get_my_ip', get_my_ip) + FLAGS.FAKE_subdomain = 'rs' + class FakeAuthDatabase(object): data = {} diff --git a/nova/tests/api/rackspace/testfaults.py b/nova/tests/api/rackspace/testfaults.py new file mode 100644 index 000000000..b2931bc98 --- /dev/null +++ b/nova/tests/api/rackspace/testfaults.py @@ -0,0 +1,40 @@ +import unittest +import webob +import webob.dec +import webob.exc + +from nova.api.rackspace import faults + +class TestFaults(unittest.TestCase): + + def test_fault_parts(self): + req = webob.Request.blank('/.xml') + f = faults.Fault(webob.exc.HTTPBadRequest(explanation='scram')) + resp = req.get_response(f) + + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['<badRequest', 'code="400">']) + body_without_spaces = ''.join(resp.body.split()) + self.assertTrue('<message>scram</message>' in body_without_spaces) + + def test_retry_header(self): + req = webob.Request.blank('/.xml') + exc = webob.exc.HTTPRequestEntityTooLarge(explanation='sorry', + headers={'Retry-After': 4}) + f = faults.Fault(exc) + resp = req.get_response(f) + first_two_words = resp.body.strip().split()[:2] + self.assertEqual(first_two_words, ['<overLimit', 'code="413">']) + body_sans_spaces = ''.join(resp.body.split()) + self.assertTrue('<message>sorry</message>' in body_sans_spaces) + self.assertTrue('<retryAfter>4</retryAfter>' in body_sans_spaces) + self.assertEqual(resp.headers['Retry-After'], 4) + + def test_raise(self): + @webob.dec.wsgify + def raiser(req): + raise faults.Fault(webob.exc.HTTPNotFound(explanation='whut?')) + req = webob.Request.blank('/.xml') + resp = req.get_response(raiser) + self.assertEqual(resp.status_int, 404) + self.assertTrue('whut?' in resp.body) diff --git a/nova/tests/api/test_helper.py b/nova/tests/api/test_helper.py index 8151a4af6..d0a2cc027 100644 --- a/nova/tests/api/test_helper.py +++ b/nova/tests/api/test_helper.py @@ -1,4 +1,5 @@ import webob.dec +from nova import wsgi class APIStub(object): """Class to verify request and mark it was called.""" diff --git a/nova/tests/api/wsgi_test.py b/nova/tests/api/wsgi_test.py index 786dc1bce..9425b01d0 100644 --- a/nova/tests/api/wsgi_test.py +++ b/nova/tests/api/wsgi_test.py @@ -91,6 +91,57 @@ class Test(unittest.TestCase): result = webob.Request.blank('/test/123').get_response(Router()) self.assertNotEqual(result.body, "123") - def test_serializer(self): - # TODO(eday): Placeholder for serializer testing. - pass + +class SerializerTest(unittest.TestCase): + + def match(self, url, accept, expect): + input_dict = dict(servers=dict(a=(2,3))) + expected_xml = '<servers><a>(2,3)</a></servers>' + expected_json = '{"servers":{"a":[2,3]}}' + req = webob.Request.blank(url, headers=dict(Accept=accept)) + result = wsgi.Serializer(req.environ).to_content_type(input_dict) + result = result.replace('\n', '').replace(' ', '') + if expect == 'xml': + self.assertEqual(result, expected_xml) + elif expect == 'json': + self.assertEqual(result, expected_json) + else: + raise "Bad expect value" + + def test_basic(self): + self.match('/servers/4.json', None, expect='json') + self.match('/servers/4', 'application/json', expect='json') + self.match('/servers/4', 'application/xml', expect='xml') + self.match('/servers/4.xml', None, expect='xml') + + def test_defaults_to_json(self): + self.match('/servers/4', None, expect='json') + self.match('/servers/4', 'text/html', expect='json') + + def test_suffix_takes_precedence_over_accept_header(self): + self.match('/servers/4.xml', 'application/json', expect='xml') + self.match('/servers/4.xml.', 'application/json', expect='json') + + def test_deserialize(self): + xml = """ + <a a1="1" a2="2"> + <bs><b>1</b><b>2</b><b>3</b><b><c c1="1"/></b></bs> + <d><e>1</e></d> + <f>1</f> + </a> + """.strip() + as_dict = dict(a={ + 'a1': '1', + 'a2': '2', + 'bs': ['1', '2', '3', {'c': dict(c1='1')}], + 'd': {'e': '1'}, + 'f': '1'}) + metadata = {'application/xml': dict(plurals={'bs': 'b', 'ts': 't'})} + serializer = wsgi.Serializer({}, metadata) + self.assertEqual(serializer.deserialize(xml), as_dict) + + def test_deserialize_empty_xml(self): + xml = """<a></a>""" + as_dict = {"a": {}} + serializer = wsgi.Serializer({}) + self.assertEqual(serializer.deserialize(xml), as_dict) diff --git a/nova/tests/auth_unittest.py b/nova/tests/auth_unittest.py index 3235dea39..1955bb417 100644 --- a/nova/tests/auth_unittest.py +++ b/nova/tests/auth_unittest.py @@ -28,25 +28,71 @@ from nova.api.ec2 import cloud FLAGS = flags.FLAGS - -class AuthTestCase(test.BaseTestCase): +class user_generator(object): + def __init__(self, manager, **user_state): + if 'name' not in user_state: + user_state['name'] = 'test1' + self.manager = manager + self.user = manager.create_user(**user_state) + + def __enter__(self): + return self.user + + def __exit__(self, value, type, trace): + self.manager.delete_user(self.user) + +class project_generator(object): + def __init__(self, manager, **project_state): + if 'name' not in project_state: + project_state['name'] = 'testproj' + if 'manager_user' not in project_state: + project_state['manager_user'] = 'test1' + self.manager = manager + self.project = manager.create_project(**project_state) + + def __enter__(self): + return self.project + + def __exit__(self, value, type, trace): + self.manager.delete_project(self.project) + +class user_and_project_generator(object): + def __init__(self, manager, user_state={}, project_state={}): + self.manager = manager + if 'name' not in user_state: + user_state['name'] = 'test1' + if 'name' not in project_state: + project_state['name'] = 'testproj' + if 'manager_user' not in project_state: + project_state['manager_user'] = 'test1' + self.user = manager.create_user(**user_state) + self.project = manager.create_project(**project_state) + + def __enter__(self): + return (self.user, self.project) + + def __exit__(self, value, type, trace): + self.manager.delete_user(self.user) + self.manager.delete_project(self.project) + +class AuthManagerTestCase(test.TrialTestCase): def setUp(self): - super(AuthTestCase, self).setUp() + super(AuthManagerTestCase, self).setUp() self.flags(connection_type='fake') self.manager = manager.AuthManager() - def test_001_can_create_users(self): - self.manager.create_user('test1', 'access', 'secret') - self.manager.create_user('test2') - - def test_002_can_get_user(self): - user = self.manager.get_user('test1') + def test_create_and_find_user(self): + with user_generator(self.manager): + self.assert_(self.manager.get_user('test1')) - def test_003_can_retreive_properties(self): - user = self.manager.get_user('test1') - self.assertEqual('test1', user.id) - self.assertEqual('access', user.access) - self.assertEqual('secret', user.secret) + def test_create_and_find_with_properties(self): + with user_generator(self.manager, name="herbert", secret="classified", + access="private-party"): + u = self.manager.get_user('herbert') + self.assertEqual('herbert', u.id) + self.assertEqual('herbert', u.name) + self.assertEqual('classified', u.secret) + self.assertEqual('private-party', u.access) def test_004_signature_is_valid(self): #self.assertTrue(self.manager.authenticate( **boto.generate_url ... ? ? ? )) @@ -63,133 +109,216 @@ class AuthTestCase(test.BaseTestCase): 'export S3_URL="http://127.0.0.1:3333/"\n' + 'export EC2_USER_ID="test1"\n') - def test_010_can_list_users(self): - users = self.manager.get_users() - logging.warn(users) - self.assertTrue(filter(lambda u: u.id == 'test1', users)) - - def test_101_can_add_user_role(self): - self.assertFalse(self.manager.has_role('test1', 'itsec')) - self.manager.add_role('test1', 'itsec') - self.assertTrue(self.manager.has_role('test1', 'itsec')) - - def test_199_can_remove_user_role(self): - self.assertTrue(self.manager.has_role('test1', 'itsec')) - self.manager.remove_role('test1', 'itsec') - self.assertFalse(self.manager.has_role('test1', 'itsec')) - - def test_201_can_create_project(self): - project = self.manager.create_project('testproj', 'test1', 'A test project', ['test1']) - self.assertTrue(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) - self.assertEqual(project.name, 'testproj') - self.assertEqual(project.description, 'A test project') - self.assertEqual(project.project_manager_id, 'test1') - self.assertTrue(project.has_member('test1')) - - def test_202_user1_is_project_member(self): - self.assertTrue(self.manager.get_user('test1').is_project_member('testproj')) - - def test_203_user2_is_not_project_member(self): - self.assertFalse(self.manager.get_user('test2').is_project_member('testproj')) - - def test_204_user1_is_project_manager(self): - self.assertTrue(self.manager.get_user('test1').is_project_manager('testproj')) - - def test_205_user2_is_not_project_manager(self): - self.assertFalse(self.manager.get_user('test2').is_project_manager('testproj')) - - def test_206_can_add_user_to_project(self): - self.manager.add_to_project('test2', 'testproj') - self.assertTrue(self.manager.get_project('testproj').has_member('test2')) - - def test_207_can_remove_user_from_project(self): - self.manager.remove_from_project('test2', 'testproj') - self.assertFalse(self.manager.get_project('testproj').has_member('test2')) - - def test_208_can_remove_add_user_with_role(self): - self.manager.add_to_project('test2', 'testproj') - self.manager.add_role('test2', 'developer', 'testproj') - self.manager.remove_from_project('test2', 'testproj') - self.assertFalse(self.manager.has_role('test2', 'developer', 'testproj')) - self.manager.add_to_project('test2', 'testproj') - self.manager.remove_from_project('test2', 'testproj') - - def test_209_can_generate_x509(self): - # MUST HAVE RUN CLOUD SETUP BY NOW - self.cloud = cloud.CloudController() - self.cloud.setup() - _key, cert_str = self.manager._generate_x509_cert('test1', 'testproj') - logging.debug(cert_str) - - # Need to verify that it's signed by the right intermediate CA - full_chain = crypto.fetch_ca(project_id='testproj', chain=True) - int_cert = crypto.fetch_ca(project_id='testproj', chain=False) - cloud_cert = crypto.fetch_ca() - logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain) - signed_cert = X509.load_cert_string(cert_str) - chain_cert = X509.load_cert_string(full_chain) - int_cert = X509.load_cert_string(int_cert) - cloud_cert = X509.load_cert_string(cloud_cert) - self.assertTrue(signed_cert.verify(chain_cert.get_pubkey())) - self.assertTrue(signed_cert.verify(int_cert.get_pubkey())) - - if not FLAGS.use_intermediate_ca: - self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey())) - else: - self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey())) - - def test_210_can_add_project_role(self): - project = self.manager.get_project('testproj') - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.manager.add_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - project.add_role('test1', 'sysadmin') - self.assertTrue(project.has_role('test1', 'sysadmin')) - - def test_211_can_list_project_roles(self): - project = self.manager.get_project('testproj') - user = self.manager.get_user('test1') - self.manager.add_role(user, 'netadmin', project) - roles = self.manager.get_user_roles(user) - self.assertTrue('sysadmin' in roles) - self.assertFalse('netadmin' in roles) - project_roles = self.manager.get_user_roles(user, project) - self.assertTrue('sysadmin' in project_roles) - self.assertTrue('netadmin' in project_roles) - # has role should be false because global role is missing - self.assertFalse(self.manager.has_role(user, 'netadmin', project)) - - - def test_212_can_remove_project_role(self): - project = self.manager.get_project('testproj') - self.assertTrue(project.has_role('test1', 'sysadmin')) - project.remove_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - self.manager.remove_role('test1', 'sysadmin') - self.assertFalse(project.has_role('test1', 'sysadmin')) - - def test_214_can_retrieve_project_by_user(self): - project = self.manager.create_project('testproj2', 'test2', 'Another test project', ['test2']) - self.assert_(len(self.manager.get_projects()) > 1) - self.assertEqual(len(self.manager.get_projects('test2')), 1) - - def test_220_can_modify_project(self): - self.manager.modify_project('testproj', 'test2', 'new description') - project = self.manager.get_project('testproj') - self.assertEqual(project.project_manager_id, 'test2') - self.assertEqual(project.description, 'new description') - - def test_299_can_delete_project(self): - self.manager.delete_project('testproj') - self.assertFalse(filter(lambda p: p.name == 'testproj', self.manager.get_projects())) - self.manager.delete_project('testproj2') - - def test_999_can_delete_users(self): + def test_can_list_users(self): + with user_generator(self.manager): + with user_generator(self.manager, name="test2"): + users = self.manager.get_users() + self.assert_(filter(lambda u: u.id == 'test1', users)) + self.assert_(filter(lambda u: u.id == 'test2', users)) + self.assert_(not filter(lambda u: u.id == 'test3', users)) + + def test_can_add_and_remove_user_role(self): + with user_generator(self.manager): + self.assertFalse(self.manager.has_role('test1', 'itsec')) + self.manager.add_role('test1', 'itsec') + self.assertTrue(self.manager.has_role('test1', 'itsec')) + self.manager.remove_role('test1', 'itsec') + self.assertFalse(self.manager.has_role('test1', 'itsec')) + + def test_can_create_and_get_project(self): + with user_and_project_generator(self.manager) as (u,p): + self.assert_(self.manager.get_user('test1')) + self.assert_(self.manager.get_user('test1')) + self.assert_(self.manager.get_project('testproj')) + + def test_can_list_projects(self): + with user_and_project_generator(self.manager): + with project_generator(self.manager, name="testproj2"): + projects = self.manager.get_projects() + self.assert_(filter(lambda p: p.name == 'testproj', projects)) + self.assert_(filter(lambda p: p.name == 'testproj2', projects)) + self.assert_(not filter(lambda p: p.name == 'testproj3', + projects)) + + def test_can_create_and_get_project_with_attributes(self): + with user_generator(self.manager): + with project_generator(self.manager, description='A test project'): + project = self.manager.get_project('testproj') + self.assertEqual('A test project', project.description) + + def test_can_create_project_with_manager(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertEqual('test1', project.project_manager_id) + self.assertTrue(self.manager.is_project_manager(user, project)) + + def test_create_project_assigns_manager_to_members(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertTrue(self.manager.is_project_member(user, project)) + + def test_no_extra_project_members(self): + with user_generator(self.manager, name='test2') as baduser: + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(self.manager.is_project_member(baduser, + project)) + + def test_no_extra_project_managers(self): + with user_generator(self.manager, name='test2') as baduser: + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(self.manager.is_project_manager(baduser, + project)) + + def test_can_add_user_to_project(self): + with user_generator(self.manager, name='test2') as user: + with user_and_project_generator(self.manager) as (_user, project): + self.manager.add_to_project(user, project) + project = self.manager.get_project('testproj') + self.assertTrue(self.manager.is_project_member(user, project)) + + def test_can_remove_user_from_project(self): + with user_generator(self.manager, name='test2') as user: + with user_and_project_generator(self.manager) as (_user, project): + self.manager.add_to_project(user, project) + project = self.manager.get_project('testproj') + self.assertTrue(self.manager.is_project_member(user, project)) + self.manager.remove_from_project(user, project) + project = self.manager.get_project('testproj') + self.assertFalse(self.manager.is_project_member(user, project)) + + def test_can_add_remove_user_with_role(self): + with user_generator(self.manager, name='test2') as user: + with user_and_project_generator(self.manager) as (_user, project): + # NOTE(todd): after modifying users you must reload project + self.manager.add_to_project(user, project) + project = self.manager.get_project('testproj') + self.manager.add_role(user, 'developer', project) + self.assertTrue(self.manager.is_project_member(user, project)) + self.manager.remove_from_project(user, project) + project = self.manager.get_project('testproj') + self.assertFalse(self.manager.has_role(user, 'developer', + project)) + self.assertFalse(self.manager.is_project_member(user, project)) + + def test_can_generate_x509(self): + # NOTE(todd): this doesn't assert against the auth manager + # so it probably belongs in crypto_unittest + # but I'm leaving it where I found it. + with user_and_project_generator(self.manager) as (user, project): + # NOTE(todd): Should mention why we must setup controller first + # (somebody please clue me in) + cloud_controller = cloud.CloudController() + cloud_controller.setup() + _key, cert_str = self.manager._generate_x509_cert('test1', + 'testproj') + logging.debug(cert_str) + + # Need to verify that it's signed by the right intermediate CA + full_chain = crypto.fetch_ca(project_id='testproj', chain=True) + int_cert = crypto.fetch_ca(project_id='testproj', chain=False) + cloud_cert = crypto.fetch_ca() + logging.debug("CA chain:\n\n =====\n%s\n\n=====" % full_chain) + signed_cert = X509.load_cert_string(cert_str) + chain_cert = X509.load_cert_string(full_chain) + int_cert = X509.load_cert_string(int_cert) + cloud_cert = X509.load_cert_string(cloud_cert) + self.assertTrue(signed_cert.verify(chain_cert.get_pubkey())) + self.assertTrue(signed_cert.verify(int_cert.get_pubkey())) + if not FLAGS.use_intermediate_ca: + self.assertTrue(signed_cert.verify(cloud_cert.get_pubkey())) + else: + self.assertFalse(signed_cert.verify(cloud_cert.get_pubkey())) + + def test_adding_role_to_project_is_ignored_unless_added_to_user(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.manager.add_role(user, 'sysadmin', project) + # NOTE(todd): it will still show up in get_user_roles(u, project) + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.manager.add_role(user, 'sysadmin') + self.assertTrue(self.manager.has_role(user, 'sysadmin', project)) + + def test_add_user_role_doesnt_infect_project_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.manager.add_role(user, 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + + def test_can_list_user_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role(user, 'sysadmin') + roles = self.manager.get_user_roles(user) + self.assertTrue('sysadmin' in roles) + self.assertFalse('netadmin' in roles) + + def test_can_list_project_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role(user, 'sysadmin') + self.manager.add_role(user, 'sysadmin', project) + self.manager.add_role(user, 'netadmin', project) + project_roles = self.manager.get_user_roles(user, project) + self.assertTrue('sysadmin' in project_roles) + self.assertTrue('netadmin' in project_roles) + # has role should be false user-level role is missing + self.assertFalse(self.manager.has_role(user, 'netadmin', project)) + + def test_can_remove_user_roles(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role(user, 'sysadmin') + self.assertTrue(self.manager.has_role(user, 'sysadmin')) + self.manager.remove_role(user, 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin')) + + def test_removing_user_role_hides_it_from_project(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role(user, 'sysadmin') + self.manager.add_role(user, 'sysadmin', project) + self.assertTrue(self.manager.has_role(user, 'sysadmin', project)) + self.manager.remove_role(user, 'sysadmin') + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + + def test_can_remove_project_role_but_keep_user_role(self): + with user_and_project_generator(self.manager) as (user, project): + self.manager.add_role(user, 'sysadmin') + self.manager.add_role(user, 'sysadmin', project) + self.assertTrue(self.manager.has_role(user, 'sysadmin')) + self.manager.remove_role(user, 'sysadmin', project) + self.assertFalse(self.manager.has_role(user, 'sysadmin', project)) + self.assertTrue(self.manager.has_role(user, 'sysadmin')) + + def test_can_retrieve_project_by_user(self): + with user_and_project_generator(self.manager) as (user, project): + self.assertEqual(1, len(self.manager.get_projects('test1'))) + + def test_can_modify_project(self): + with user_and_project_generator(self.manager): + with user_generator(self.manager, name='test2'): + self.manager.modify_project('testproj', 'test2', 'new desc') + project = self.manager.get_project('testproj') + self.assertEqual('test2', project.project_manager_id) + self.assertEqual('new desc', project.description) + + def test_can_delete_project(self): + with user_generator(self.manager): + self.manager.create_project('testproj', 'test1') + self.assert_(self.manager.get_project('testproj')) + self.manager.delete_project('testproj') + projectlist = self.manager.get_projects() + self.assert_(not filter(lambda p: p.name == 'testproj', + projectlist)) + + def test_can_delete_user(self): + self.manager.create_user('test1') + self.assert_(self.manager.get_user('test1')) self.manager.delete_user('test1') - users = self.manager.get_users() - self.assertFalse(filter(lambda u: u.id == 'test1', users)) - self.manager.delete_user('test2') - self.assertEqual(self.manager.get_user('test2'), None) + userlist = self.manager.get_users() + self.assert_(not filter(lambda u: u.id == 'test1', userlist)) + + def test_can_modify_users(self): + with user_generator(self.manager): + self.manager.modify_user('test1', 'access', 'secret', True) + user = self.manager.get_user('test1') + self.assertEqual('access', user.access) + self.assertEqual('secret', user.secret) + self.assertTrue(user.is_admin()) if __name__ == "__main__": diff --git a/nova/tests/cloud_unittest.py b/nova/tests/cloud_unittest.py index 756ce519e..ae7dea1db 100644 --- a/nova/tests/cloud_unittest.py +++ b/nova/tests/cloud_unittest.py @@ -16,10 +16,13 @@ # License for the specific language governing permissions and limitations # under the License. +import json import logging from M2Crypto import BIO from M2Crypto import RSA +import os import StringIO +import tempfile import time from twisted.internet import defer @@ -36,15 +39,22 @@ from nova.auth import manager from nova.compute import power_state from nova.api.ec2 import context from nova.api.ec2 import cloud +from nova.objectstore import image FLAGS = flags.FLAGS -class CloudTestCase(test.BaseTestCase): +# Temp dirs for working with image attributes through the cloud controller +# (stole this from objectstore_unittest.py) +OSS_TEMPDIR = tempfile.mkdtemp(prefix='test_oss-') +IMAGES_PATH = os.path.join(OSS_TEMPDIR, 'images') +os.makedirs(IMAGES_PATH) + +class CloudTestCase(test.TrialTestCase): def setUp(self): super(CloudTestCase, self).setUp() - self.flags(connection_type='fake') + self.flags(connection_type='fake', images_path=IMAGES_PATH) self.conn = rpc.Connection.instance() logging.getLogger().setLevel(logging.DEBUG) @@ -55,9 +65,9 @@ class CloudTestCase(test.BaseTestCase): # set up a service self.compute = utils.import_class(FLAGS.compute_manager) self.compute_consumer = rpc.AdapterConsumer(connection=self.conn, - topic=FLAGS.compute_topic, - proxy=self.compute) - self.injected.append(self.compute_consumer.attach_to_tornado(self.ioloop)) + topic=FLAGS.compute_topic, + proxy=self.compute) + self.compute_consumer.attach_to_twisted() self.manager = manager.AuthManager() self.user = self.manager.create_user('admin', 'admin', 'admin', True) @@ -68,7 +78,7 @@ class CloudTestCase(test.BaseTestCase): def tearDown(self): self.manager.delete_project(self.project) self.manager.delete_user(self.user) - super(CloudTestCase, self).setUp() + super(CloudTestCase, self).tearDown() def _create_key(self, name): # NOTE(vish): create depends on pool, so just call helper directly @@ -191,3 +201,67 @@ class CloudTestCase(test.BaseTestCase): #for i in xrange(4): # data = self.cloud.get_metadata(instance(i)['private_dns_name']) # self.assert_(data['meta-data']['ami-id'] == 'ami-%s' % i) + + @staticmethod + def _fake_set_image_description(ctxt, image_id, description): + from nova.objectstore import handler + class req: + pass + request = req() + request.context = ctxt + request.args = {'image_id': [image_id], + 'description': [description]} + + resource = handler.ImagesResource() + resource.render_POST(request) + + def test_user_editable_image_endpoint(self): + pathdir = os.path.join(FLAGS.images_path, 'ami-testing') + os.mkdir(pathdir) + info = {'isPublic': False} + with open(os.path.join(pathdir, 'info.json'), 'w') as f: + json.dump(info, f) + img = image.Image('ami-testing') + # self.cloud.set_image_description(self.context, 'ami-testing', + # 'Foo Img') + # NOTE(vish): Above won't work unless we start objectstore or create + # a fake version of api/ec2/images.py conn that can + # call methods directly instead of going through boto. + # for now, just cheat and call the method directly + self._fake_set_image_description(self.context, 'ami-testing', + 'Foo Img') + self.assertEqual('Foo Img', img.metadata['description']) + self._fake_set_image_description(self.context, 'ami-testing', '') + self.assertEqual('', img.metadata['description']) + + def test_update_of_instance_display_fields(self): + inst = db.instance_create({}, {}) + self.cloud.update_instance(self.context, inst['ec2_id'], + display_name='c00l 1m4g3') + inst = db.instance_get({}, inst['id']) + self.assertEqual('c00l 1m4g3', inst['display_name']) + db.instance_destroy({}, inst['id']) + + def test_update_of_instance_wont_update_private_fields(self): + inst = db.instance_create({}, {}) + self.cloud.update_instance(self.context, inst['id'], + mac_address='DE:AD:BE:EF') + inst = db.instance_get({}, inst['id']) + self.assertEqual(None, inst['mac_address']) + db.instance_destroy({}, inst['id']) + + def test_update_of_volume_display_fields(self): + vol = db.volume_create({}, {}) + self.cloud.update_volume(self.context, vol['id'], + display_name='c00l v0lum3') + vol = db.volume_get({}, vol['id']) + self.assertEqual('c00l v0lum3', vol['display_name']) + db.volume_destroy({}, vol['id']) + + def test_update_of_volume_wont_update_private_fields(self): + vol = db.volume_create({}, {}) + self.cloud.update_volume(self.context, vol['id'], + mountpoint='/not/here') + vol = db.volume_get({}, vol['id']) + self.assertEqual(None, vol['mountpoint']) + db.volume_destroy({}, vol['id']) diff --git a/nova/tests/objectstore_unittest.py b/nova/tests/objectstore_unittest.py index dece4b5d5..5a599ff3a 100644 --- a/nova/tests/objectstore_unittest.py +++ b/nova/tests/objectstore_unittest.py @@ -53,7 +53,7 @@ os.makedirs(os.path.join(OSS_TEMPDIR, 'images')) os.makedirs(os.path.join(OSS_TEMPDIR, 'buckets')) -class ObjectStoreTestCase(test.BaseTestCase): +class ObjectStoreTestCase(test.TrialTestCase): """Test objectstore API directly.""" def setUp(self): # pylint: disable-msg=C0103 @@ -164,6 +164,12 @@ class ObjectStoreTestCase(test.BaseTestCase): self.context.project = self.auth_manager.get_project('proj2') self.assertFalse(my_img.is_authorized(self.context)) + # change user-editable fields + my_img.update_user_editable_fields({'display_name': 'my cool image'}) + self.assertEqual('my cool image', my_img.metadata['displayName']) + my_img.update_user_editable_fields({'display_name': ''}) + self.assert_(not my_img.metadata['displayName']) + class TestHTTPChannel(http.HTTPChannel): """Dummy site required for twisted.web""" diff --git a/nova/tests/rpc_unittest.py b/nova/tests/rpc_unittest.py index e12a28fbc..9652841f2 100644 --- a/nova/tests/rpc_unittest.py +++ b/nova/tests/rpc_unittest.py @@ -30,7 +30,7 @@ from nova import test FLAGS = flags.FLAGS -class RpcTestCase(test.BaseTestCase): +class RpcTestCase(test.TrialTestCase): """Test cases for rpc""" def setUp(self): # pylint: disable-msg=C0103 super(RpcTestCase, self).setUp() @@ -39,14 +39,13 @@ class RpcTestCase(test.BaseTestCase): self.consumer = rpc.AdapterConsumer(connection=self.conn, topic='test', proxy=self.receiver) - - self.injected.append(self.consumer.attach_to_tornado(self.ioloop)) + self.consumer.attach_to_twisted() def test_call_succeed(self): """Get a value through rpc call""" value = 42 - result = yield rpc.call('test', {"method": "echo", - "args": {"value": value}}) + result = yield rpc.call_twisted('test', {"method": "echo", + "args": {"value": value}}) self.assertEqual(value, result) def test_call_exception(self): @@ -57,12 +56,12 @@ class RpcTestCase(test.BaseTestCase): to an int in the test. """ value = 42 - self.assertFailure(rpc.call('test', {"method": "fail", - "args": {"value": value}}), + self.assertFailure(rpc.call_twisted('test', {"method": "fail", + "args": {"value": value}}), rpc.RemoteError) try: - yield rpc.call('test', {"method": "fail", - "args": {"value": value}}) + yield rpc.call_twisted('test', {"method": "fail", + "args": {"value": value}}) self.fail("should have thrown rpc.RemoteError") except rpc.RemoteError as exc: self.assertEqual(int(exc.value), value) diff --git a/nova/tests/service_unittest.py b/nova/tests/service_unittest.py index 01da0eb8a..06f80e82c 100644 --- a/nova/tests/service_unittest.py +++ b/nova/tests/service_unittest.py @@ -65,15 +65,20 @@ class ServiceTestCase(test.BaseTestCase): proxy=mox.IsA(service.Service)).AndReturn( rpc.AdapterConsumer) + rpc.AdapterConsumer.attach_to_twisted() + rpc.AdapterConsumer.attach_to_twisted() + # Stub out looping call a bit needlessly since we don't have an easy # way to cancel it (yet) when the tests finishes service.task.LoopingCall(mox.IgnoreArg()).AndReturn( service.task.LoopingCall) service.task.LoopingCall.start(interval=mox.IgnoreArg(), now=mox.IgnoreArg()) + service.task.LoopingCall(mox.IgnoreArg()).AndReturn( + service.task.LoopingCall) + service.task.LoopingCall.start(interval=mox.IgnoreArg(), + now=mox.IgnoreArg()) - rpc.AdapterConsumer.attach_to_twisted() - rpc.AdapterConsumer.attach_to_twisted() service_create = {'host': host, 'binary': binary, 'topic': topic, diff --git a/nova/virt/xenapi.py b/nova/virt/xenapi.py index 1c6de4403..0d06b1fce 100644 --- a/nova/virt/xenapi.py +++ b/nova/virt/xenapi.py @@ -103,8 +103,8 @@ class XenAPIConnection(object): self._conn.login_with_password(user, pw) def list_instances(self): - result = [self._conn.xenapi.VM.get_name_label(vm) \ - for vm in self._conn.xenapi.VM.get_all()] + return [self._conn.xenapi.VM.get_name_label(vm) \ + for vm in self._conn.xenapi.VM.get_all()] @defer.inlineCallbacks def spawn(self, instance): diff --git a/nova/volume/manager.py b/nova/volume/manager.py index 8472ff33b..081a2d695 100644 --- a/nova/volume/manager.py +++ b/nova/volume/manager.py @@ -77,7 +77,7 @@ class AOEManager(manager.Manager): size = volume_ref['size'] logging.debug("volume %s: creating lv of size %sG", volume_id, size) - yield self.driver.create_volume(volume_ref['str_id'], size) + yield self.driver.create_volume(volume_ref['ec2_id'], size) logging.debug("volume %s: allocating shelf & blade", volume_id) self._ensure_blades(context) @@ -87,7 +87,7 @@ class AOEManager(manager.Manager): logging.debug("volume %s: exporting shelf %s & blade %s", volume_id, shelf_id, blade_id) - yield self.driver.create_export(volume_ref['str_id'], + yield self.driver.create_export(volume_ref['ec2_id'], shelf_id, blade_id) @@ -112,10 +112,10 @@ class AOEManager(manager.Manager): logging.debug("Deleting volume with id of: %s", volume_id) shelf_id, blade_id = self.db.volume_get_shelf_and_blade(context, volume_id) - yield self.driver.remove_export(volume_ref['str_id'], + yield self.driver.remove_export(volume_ref['ec2_id'], shelf_id, blade_id) - yield self.driver.delete_volume(volume_ref['str_id']) + yield self.driver.delete_volume(volume_ref['ec2_id']) self.db.volume_destroy(context, volume_id) defer.returnValue(True) @@ -126,7 +126,7 @@ class AOEManager(manager.Manager): Returns path to device. """ volume_ref = self.db.volume_get(context, volume_id) - yield self.driver.discover_volume(volume_ref['str_id']) + yield self.driver.discover_volume(volume_ref['ec2_id']) shelf_id, blade_id = self.db.volume_get_shelf_and_blade(context, volume_id) defer.returnValue("/dev/etherd/e%s.%s" % (shelf_id, blade_id)) diff --git a/nova/wsgi.py b/nova/wsgi.py index 8a4e2a9f4..b91d91121 100644 --- a/nova/wsgi.py +++ b/nova/wsgi.py @@ -21,14 +21,17 @@ Utility methods for working with WSGI servers """ +import json import logging import sys +from xml.dom import minidom import eventlet import eventlet.wsgi eventlet.patcher.monkey_patch(all=False, socket=True) import routes import routes.middleware +import webob import webob.dec import webob.exc @@ -227,10 +230,19 @@ class Controller(object): serializer = Serializer(request.environ, _metadata) return serializer.to_content_type(data) + def _deserialize(self, data, request): + """ + Deserialize the request body to the response type requested in request. + Uses self._serialization_metadata if it exists, which is a dict mapping + MIME types to information needed to serialize to that type. + """ + _metadata = getattr(type(self), "_serialization_metadata", {}) + serializer = Serializer(request.environ, _metadata) + return serializer.deserialize(data) class Serializer(object): """ - Serializes a dictionary to a Content Type specified by a WSGI environment. + Serializes and deserializes dictionaries to certain MIME types. """ def __init__(self, environ, metadata=None): @@ -239,31 +251,77 @@ class Serializer(object): 'metadata' is an optional dict mapping MIME types to information needed to serialize a dictionary to that type. """ - self.environ = environ self.metadata = metadata or {} - self._methods = { - 'application/json': self._to_json, - 'application/xml': self._to_xml} + req = webob.Request(environ) + suffix = req.path_info.split('.')[-1].lower() + if suffix == 'json': + self.handler = self._to_json + elif suffix == 'xml': + self.handler = self._to_xml + elif 'application/json' in req.accept: + self.handler = self._to_json + elif 'application/xml' in req.accept: + self.handler = self._to_xml + else: + self.handler = self._to_json # default def to_content_type(self, data): """ - Serialize a dictionary into a string. The format of the string - will be decided based on the Content Type requested in self.environ: - by Accept: header, or by URL suffix. + Serialize a dictionary into a string. + + The format of the string will be decided based on the Content Type + requested in self.environ: by Accept: header, or by URL suffix. """ - mimetype = 'application/xml' - # TODO(gundlach): determine mimetype from request - return self._methods.get(mimetype, repr)(data) + return self.handler(data) + + def deserialize(self, datastring): + """ + Deserialize a string to a dictionary. + + The string must be in the format of a supported MIME type. + """ + datastring = datastring.strip() + try: + is_xml = (datastring[0] == '<') + if not is_xml: + return json.loads(datastring) + return self._from_xml(datastring) + except: + return None + + def _from_xml(self, datastring): + xmldata = self.metadata.get('application/xml', {}) + plurals = set(xmldata.get('plurals', {})) + node = minidom.parseString(datastring).childNodes[0] + return {node.nodeName: self._from_xml_node(node, plurals)} + + def _from_xml_node(self, node, listnames): + """ + Convert a minidom node to a simple Python type. + + listnames is a collection of names of XML nodes whose subnodes should + be considered list items. + """ + if len(node.childNodes) == 1 and node.childNodes[0].nodeType == 3: + return node.childNodes[0].nodeValue + elif node.nodeName in listnames: + return [self._from_xml_node(n, listnames) for n in node.childNodes] + else: + result = dict() + for attr in node.attributes.keys(): + result[attr] = node.attributes[attr].nodeValue + for child in node.childNodes: + if child.nodeType != node.TEXT_NODE: + result[child.nodeName] = self._from_xml_node(child, listnames) + return result def _to_json(self, data): - import json return json.dumps(data) def _to_xml(self, data): metadata = self.metadata.get('application/xml', {}) # We expect data to contain a single key which is the XML root. root_key = data.keys()[0] - from xml.dom import minidom doc = minidom.Document() node = self._to_xml_node(doc, metadata, root_key, data[root_key]) return node.toprettyxml(indent=' ') @@ -1,7 +1,8 @@ [Messages Control] # W0511: TODOs in code comments are fine. # W0142: *args and **kwargs are fine. -disable-msg=W0511,W0142 +# W0622: Redefining id is fine. +disable-msg=W0511,W0142,W0622 [Basic] # Variable names can be 1 to 31 characters long, with lowercase and underscores @@ -54,5 +54,5 @@ setup(name='nova', 'bin/nova-manage', 'bin/nova-network', 'bin/nova-objectstore', - 'bin/nova-api-new', + 'bin/nova-scheduler', 'bin/nova-volume']) diff --git a/tools/setup_iptables.sh b/tools/setup_iptables.sh new file mode 100755 index 000000000..673353eb4 --- /dev/null +++ b/tools/setup_iptables.sh @@ -0,0 +1,158 @@ +#!/usr/bin/env bash +# 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. + +# NOTE(vish): This script sets up some reasonable defaults for iptables and +# creates nova-specific chains. If you use this script you should +# run nova-network and nova-compute with --use_nova_chains=True + +# NOTE(vish): If you run nova-api on a different port, make sure to change +# the port here +API_PORT=${API_PORT:-"8773"} +if [ -n "$1" ]; then + CMD=$1 +else + CMD="all" +fi + +if [ -n "$2" ]; then + IP=$2 +else + # NOTE(vish): This will just get the first ip in the list, so if you + # have more than one eth device set up, this will fail, and + # you should explicitly pass in the ip of the instance + IP=`ifconfig | grep -m 1 'inet addr:'| cut -d: -f2 | awk '{print $1}'` +fi + +if [ -n "$3" ]; then + PRIVATE_RANGE=$3 +else + PRIVATE_RANGE="10.0.0.0/12" +fi + + +if [ -n "$4" ]; then + # NOTE(vish): Management IP is the ip over which to allow ssh traffic. It + # will also allow traffic to nova-api + MGMT_IP=$4 +else + MGMT_IP="$IP" +fi +if [ "$CMD" == "clear" ]; then + iptables -P INPUT ACCEPT + iptables -P FORWARD ACCEPT + iptables -P OUTPUT ACCEPT + iptables -F + iptables -t nat -F + iptables -F nova_input + iptables -F nova_output + iptables -F nova_forward + iptables -t nat -F nova_input + iptables -t nat -F nova_output + iptables -t nat -F nova_forward + iptables -t nat -X + iptables -X +fi + +if [ "$CMD" == "base" ] || [ "$CMD" == "all" ]; then + iptables -P INPUT DROP + iptables -A INPUT -m state --state INVALID -j DROP + iptables -A INPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A INPUT -m tcp -p tcp -d $MGMT_IP --dport 22 -j ACCEPT + iptables -A INPUT -m udp -p udp --dport 123 -j ACCEPT + iptables -N nova_input + iptables -A INPUT -j nova_input + iptables -A INPUT -p icmp -j ACCEPT + iptables -A INPUT -p tcp -j REJECT --reject-with tcp-reset + iptables -A INPUT -j REJECT --reject-with icmp-port-unreachable + + iptables -P FORWARD DROP + iptables -A FORWARD -m state --state INVALID -j DROP + iptables -A FORWARD -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -A FORWARD -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu + iptables -N nova_forward + iptables -A FORWARD -j nova_forward + + # NOTE(vish): DROP on output is too restrictive for now. We need to add + # in a bunch of more specific output rules to use it. + # iptables -P OUTPUT DROP + iptables -A OUTPUT -m state --state INVALID -j DROP + iptables -A OUTPUT -m state --state RELATED,ESTABLISHED -j ACCEPT + iptables -N nova_output + iptables -A OUTPUT -j nova_output + + iptables -t nat -N nova_prerouting + iptables -t nat -A PREROUTING -j nova_prerouting + + iptables -t nat -N nova_postrouting + iptables -t nat -A POSTROUTING -j nova_postrouting + + iptables -t nat -N nova_output + iptables -t nat -A OUTPUT -j nova_output +fi + +if [ "$CMD" == "ganglia" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 8649 -j ACCEPT + iptables -A nova_input -m udp -p udp -d $IP --dport 8649 -j ACCEPT +fi + +if [ "$CMD" == "web" ] || [ "$CMD" == "all" ]; then + # NOTE(vish): This opens up ports for web access, allowing web-based + # dashboards to work. + iptables -A nova_input -m tcp -p tcp -d $IP --dport 80 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 443 -j ACCEPT +fi + +if [ "$CMD" == "objectstore" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3333 -j ACCEPT +fi + +if [ "$CMD" == "api" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport $API_PORT -j ACCEPT + if [ "$IP" != "$MGMT_IP" ]; then + iptables -A nova_input -m tcp -p tcp -d $MGMT_IP --dport $API_PORT -j ACCEPT + fi +fi + +if [ "$CMD" == "redis" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 6379 -j ACCEPT +fi + +if [ "$CMD" == "mysql" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 3306 -j ACCEPT +fi + +if [ "$CMD" == "rabbitmq" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 4369 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 5672 -j ACCEPT + iptables -A nova_input -m tcp -p tcp -d $IP --dport 53284 -j ACCEPT +fi + +if [ "$CMD" == "dnsmasq" ] || [ "$CMD" == "all" ]; then + # NOTE(vish): this could theoretically be setup per network + # for each host, but it seems like overkill + iptables -A nova_input -m tcp -p tcp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp -s $PRIVATE_RANGE --dport 53 -j ACCEPT + iptables -A nova_input -m udp -p udp --dport 67 -j ACCEPT +fi + +if [ "$CMD" == "ldap" ] || [ "$CMD" == "all" ]; then + iptables -A nova_input -m tcp -p tcp -d $IP --dport 389 -j ACCEPT +fi + + |
