From 19a4ddaf157ebb388cce37ddc142dfad304b8cf0 Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Fri, 12 Aug 2011 16:48:13 -0700 Subject: Added add securitygroup to instance and remove securitygroup from instance functionality --- nova/api/openstack/contrib/security_groups.py | 199 ++++++++++++-- nova/api/openstack/create_instance_helper.py | 30 ++- nova/db/api.py | 6 + nova/db/sqlalchemy/api.py | 15 ++ .../api/openstack/contrib/test_security_groups.py | 299 +++++++++++++++++++++ 5 files changed, 530 insertions(+), 19 deletions(-) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index 6c57fbb51..a104a42e4 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -25,10 +25,11 @@ from nova import db from nova import exception from nova import flags from nova import log as logging +from nova import rpc from nova.api.openstack import common from nova.api.openstack import extensions from nova.api.openstack import wsgi - +from nova.compute import power_state from xml.dom import minidom @@ -73,33 +74,28 @@ class SecurityGroupController(object): context, rule)] return security_group - def show(self, req, id): - """Return data about the given security group.""" - context = req.environ['nova.context'] + def _get_security_group(self, context, id): try: id = int(id) security_group = db.security_group_get(context, id) except ValueError: - msg = _("Security group id is not integer") - return exc.HTTPBadRequest(explanation=msg) + msg = _("Security group id should be integer") + raise exc.HTTPBadRequest(explanation=msg) except exception.NotFound as exp: - return exc.HTTPNotFound(explanation=unicode(exp)) + raise exc.HTTPNotFound(explanation=unicode(exp)) + return security_group + def show(self, req, id): + """Return data about the given security group.""" + context = req.environ['nova.context'] + security_group = self._get_security_group(context, id) return {'security_group': self._format_security_group(context, security_group)} def delete(self, req, id): """Delete a security group.""" context = req.environ['nova.context'] - try: - id = int(id) - security_group = db.security_group_get(context, id) - except ValueError: - msg = _("Security group id is not integer") - return exc.HTTPBadRequest(explanation=msg) - except exception.SecurityGroupNotFound as exp: - return exc.HTTPNotFound(explanation=unicode(exp)) - + security_group = self._get_security_group(context, id) LOG.audit(_("Delete security group %s"), id, context=context) db.security_group_destroy(context, security_group.id) @@ -172,6 +168,135 @@ class SecurityGroupController(object): "than 255 characters.") % typ raise exc.HTTPBadRequest(explanation=msg) + def associate(self, req, id, body): + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'security_group_associate' in body: + raise exc.HTTPUnprocessableEntity() + + security_group = self._get_security_group(context, id) + + servers = body['security_group_associate'].get('servers') + + if not servers: + msg = _("No servers found") + return exc.HTTPBadRequest(explanation=msg) + + hosts = set() + for server in servers: + if server['id']: + try: + # check if the server exists + inst = db.instance_get(context, server['id']) + #check if the security group is assigned to the server + if self._is_security_group_associated_to_server( + security_group, inst['id']): + msg = _("Security group %s is already associated with" + " the instance %s") % (security_group['id'], + server['id']) + raise exc.HTTPBadRequest(explanation=msg) + + #check if the instance is in running state + if inst['state'] != power_state.RUNNING: + msg = _("Server %s is not in the running state")\ + % server['id'] + raise exc.HTTPBadRequest(explanation=msg) + + hosts.add(inst['host']) + except exception.InstanceNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + + # Associate security group with the server in the db + for server in servers: + if server['id']: + db.instance_add_security_group(context.elevated(), + server['id'], + security_group['id']) + + for host in hosts: + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "refresh_security_group_rules", + "args": {"security_group_id": security_group['id']}}) + + return exc.HTTPAccepted() + + def _is_security_group_associated_to_server(self, security_group, + instance_id): + if not security_group: + return False + + instances = security_group.get('instances') + if not instances: + return False + + inst_id = None + for inst_id in (instance['id'] for instance in instances \ + if instance_id == instance['id']): + return True + + return False + + def disassociate(self, req, id, body): + context = req.environ['nova.context'] + + if not body: + raise exc.HTTPUnprocessableEntity() + + if not 'security_group_disassociate' in body: + raise exc.HTTPUnprocessableEntity() + + security_group = self._get_security_group(context, id) + + servers = body['security_group_disassociate'].get('servers') + + if not servers: + msg = _("No servers found") + return exc.HTTPBadRequest(explanation=msg) + + hosts = set() + for server in servers: + if server['id']: + try: + # check if the instance exists + inst = db.instance_get(context, server['id']) + # Check if the security group is not associated + # with the instance + if not self._is_security_group_associated_to_server( + security_group, inst['id']): + msg = _("Security group %s is not associated with the" + "instance %s") % (security_group['id'], + server['id']) + raise exc.HTTPBadRequest(explanation=msg) + + #check if the instance is in running state + if inst['state'] != power_state.RUNNING: + msg = _("Server %s is not in the running state")\ + % server['id'] + raise exp.HTTPBadRequest(explanation=msg) + + hosts.add(inst['host']) + except exception.InstanceNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + + # Disassociate security group from the server + for server in servers: + if server['id']: + db.instance_remove_security_group(context.elevated(), + server['id'], + security_group['id']) + + for host in hosts: + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, host), + {"method": "refresh_security_group_rules", + "args": {"security_group_id": security_group['id']}}) + + return exc.HTTPAccepted() + class SecurityGroupRulesController(SecurityGroupController): @@ -226,9 +351,9 @@ class SecurityGroupRulesController(SecurityGroupController): security_group_rule = db.security_group_rule_create(context, values) self.compute_api.trigger_security_group_rules_refresh(context, - security_group_id=security_group['id']) + security_group_id=security_group['id']) - return {'security_group_rule': self._format_security_group_rule( + return {"security_group_rule": self._format_security_group_rule( context, security_group_rule)} @@ -368,6 +493,10 @@ class Security_groups(extensions.ExtensionDescriptor): res = extensions.ResourceExtension('os-security-groups', controller=SecurityGroupController(), + member_actions={ + 'associate': 'POST', + 'disassociate': 'POST' + }, deserializer=deserializer, serializer=serializer) @@ -405,6 +534,40 @@ class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): security_group['description'] = self.extract_text(desc_node) return {'body': {'security_group': security_group}} + def _get_servers(self, node): + servers_dict = {'servers': []} + if node is not None: + servers_node = self.find_first_child_named(node, + 'servers') + if servers_node is not None: + for server_node in self.find_children_named(servers_node, + "server"): + servers_dict['servers'].append( + {"id": self.extract_text(server_node)}) + return servers_dict + + def associate(self, string): + """Deserialize an xml-formatted security group associate request""" + dom = minidom.parseString(string) + node = self.find_first_child_named(dom, + 'security_group_associate') + result = {'body': {}} + if node: + result['body']['security_group_associate'] = \ + self._get_servers(node) + return result + + def disassociate(self, string): + """Deserialize an xml-formatted security group disassociate request""" + dom = minidom.parseString(string) + node = self.find_first_child_named(dom, + 'security_group_disassociate') + result = {'body': {}} + if node: + result['body']['security_group_disassociate'] = \ + self._get_servers(node) + return result + class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): """ diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 1425521a9..4ceb972c0 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -111,6 +111,16 @@ class CreateInstanceHelper(object): if personality: injected_files = self._get_injected_files(personality) + sg_names = [] + security_groups = server_dict.get('security_groups') + if security_groups: + sg_names = [sg['name'] for sg in security_groups if sg.get('name')] + if not sg_names: + sg_names.append('default') + + sg_names = list(set(sg_names)) + LOG.debug(sg_names) + try: flavor_id = self.controller._flavor_id_from_req_data(body) except ValueError as error: @@ -161,7 +171,8 @@ class CreateInstanceHelper(object): zone_blob=zone_blob, reservation_id=reservation_id, min_count=min_count, - max_count=max_count)) + max_count=max_count, + security_group=sg_names)) except quota.QuotaError as error: self._handle_quota_error(error) except exception.ImageNotFound as error: @@ -170,6 +181,8 @@ class CreateInstanceHelper(object): except exception.FlavorNotFound as error: msg = _("Invalid flavorRef provided.") raise exc.HTTPBadRequest(explanation=msg) + except exception.SecurityGroupNotFound as error: + raise exc.HTTPBadRequest(explanation=unicode(error)) # Let the caller deal with unhandled exceptions. def _handle_quota_error(self, error): @@ -454,6 +467,8 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): if personality is not None: server["personality"] = personality + server["security_groups"] = self._extract_security_groups(server_node) + return server def _extract_personality(self, server_node): @@ -470,3 +485,16 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): return personality else: return None + + def _extract_security_groups(self, server_node): + """Marshal the security_groups attribute of a parsed request""" + node = self.find_first_child_named(server_node, "security_groups") + security_groups = [] + if node is not None: + for sg_node in self.find_children_named(node, "security_group"): + item = {} + name_node = self.find_first_child_named(sg_node, "name") + if name_node: + item["name"] = self.extract_text(name_node) + security_groups.append(item) + return security_groups diff --git a/nova/db/api.py b/nova/db/api.py index 0f2218752..cf814d43e 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -570,6 +570,12 @@ def instance_add_security_group(context, instance_id, security_group_id): security_group_id) +def instance_remove_security_group(context, instance_id, security_group_id): + """Disassociate the given security group from the given instance.""" + return IMPL.instance_remove_security_group(context, instance_id, + security_group_id) + + def instance_get_vcpu_sum_by_host_and_project(context, hostname, proj_id): """Get instances.vcpus by host and project.""" return IMPL.instance_get_vcpu_sum_by_host_and_project(context, diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index e5d35a20b..ba16f9109 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1482,6 +1482,19 @@ def instance_add_security_group(context, instance_id, security_group_id): instance_ref.save(session=session) +@require_context +def instance_remove_security_group(context, instance_id, security_group_id): + """Disassociate the given security group from the given instance""" + session = get_session() + + session.query(models.SecurityGroupInstanceAssociation).\ + filter_by(instance_id=instance_id).\ + filter_by(security_group_id=security_group_id).\ + update({'deleted': True, + 'deleted_at': utils.utcnow(), + 'updated_at': literal_column('updated_at')}) + + @require_context def instance_get_vcpu_sum_by_host_and_project(context, hostname, proj_id): session = get_session() @@ -2456,6 +2469,7 @@ def security_group_get(context, security_group_id, session=None): filter_by(deleted=can_read_deleted(context),).\ filter_by(id=security_group_id).\ options(joinedload_all('rules')).\ + options(joinedload_all('instances')).\ first() else: result = session.query(models.SecurityGroup).\ @@ -2463,6 +2477,7 @@ def security_group_get(context, security_group_id, session=None): filter_by(id=security_group_id).\ filter_by(project_id=context.project_id).\ options(joinedload_all('rules')).\ + options(joinedload_all('instances')).\ first() if not result: raise exception.SecurityGroupNotFound( diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py index 4317880ca..894b0c591 100644 --- a/nova/tests/api/openstack/contrib/test_security_groups.py +++ b/nova/tests/api/openstack/contrib/test_security_groups.py @@ -15,10 +15,13 @@ # under the License. import json +import mox +import nova import unittest import webob from xml.dom import minidom +from nova import exception from nova import test from nova.api.openstack.contrib import security_groups from nova.tests.api.openstack import fakes @@ -51,6 +54,28 @@ def _create_security_group_request_dict(security_group): return {'security_group': sg} +def return_server(context, server_id): + return {'id': server_id, 'state': 0x01, 'host': "localhost"} + + +def return_non_running_server(context, server_id): + return {'id': server_id, 'state': 0x02, + 'host': "localhost"} + + +def return_security_group(context, group_id): + return {'id': group_id, "instances": [ + {'id': 1}]} + + +def return_security_group_without_instances(context, group_id): + return {'id': group_id} + + +def return_server_nonexistant(context, server_id): + raise exception.InstanceNotFound() + + class TestSecurityGroups(test.TestCase): def setUp(self): super(TestSecurityGroups, self).setUp() @@ -325,6 +350,280 @@ class TestSecurityGroups(test.TestCase): response = self._delete_security_group(11111111) self.assertEquals(response.status_int, 404) + def test_associate_by_non_existing_security_group_id(self): + req = webob.Request.blank('/v1.1/os-security-groups/111111/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + {"id": '2'} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_associate_by_invalid_security_group_id(self): + req = webob.Request.blank('/v1.1/os-security-groups/invalid/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + {"id": "2"} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_without_body(self): + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(None) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_associate_no_security_group_element(self): + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate_invalid": { + "servers": [ + {"id": "2"} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_associate_no_instances(self): + #self.stubs.Set(nova.db.api, 'instance_get', return_server) + self.stubs.Set(nova.db, 'security_group_get', return_security_group) + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_non_existing_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + self.stubs.Set(nova.db, 'security_group_get', return_security_group) + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + {'id': 2} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_associate_non_running_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_non_running_server) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group_without_instances) + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + {'id': 1} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate_already_associated_security_group_to_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'security_group_get', return_security_group) + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + {'id': 1} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_associate(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_add_security_group') + nova.db.instance_add_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group_without_instances) + self.mox.ReplayAll() + + req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_associate": { + "servers": [ + {'id': 1} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_disassociate_by_non_existing_security_group_id(self): + req = webob.Request.blank('/v1.1/os-security-groups/1111/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + {"id": "2"} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_disassociate_by_invalid_security_group_id(self): + req = webob.Request.blank('/v1.1/os-security-groups/id/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + {"id": "2"} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_without_body(self): + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + req.body = json.dumps(None) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_disassociate_no_security_group_element(self): + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate_invalid": { + "servers": [ + {"id": "2"} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 422) + + def test_disassociate_no_instances(self): + #self.stubs.Set(nova.db.api, 'instance_get', return_server) + self.stubs.Set(nova.db, 'security_group_get', return_security_group) + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_non_existing_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) + self.stubs.Set(nova.db, 'security_group_get', return_security_group) + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + {'id': 2} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 404) + + def test_disassociate_non_running_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_non_running_server) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group_without_instances) + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + {'id': 1} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate_not_associated_security_group_to_instance(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.stubs.Set(nova.db, 'security_group_get', return_security_group) + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + {'id': 2} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 400) + + def test_disassociate(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group') + nova.db.instance_remove_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get', + return_security_group) + self.mox.ReplayAll() + + req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req.headers['Content-Type'] = 'application/json' + req.method = 'POST' + body_dict = {"security_group_disassociate": { + "servers": [ + {'id': 1} + ] + } + } + req.body = json.dumps(body_dict) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + class TestSecurityGroupRules(test.TestCase): def setUp(self): -- cgit From 83b45a371665fd069fc7e372628f82874258fd08 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 16 Aug 2011 00:31:54 -0700 Subject: redux of floating ip api --- nova/api/openstack/contrib/floating_ips.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 44b35c385..722320534 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -78,11 +78,14 @@ class FloatingIPController(object): def index(self, req): context = req.environ['nova.context'] - floating_ips = self.network_api.list_floating_ips(context) + try: + floating_ips = self.network_api.list_floating_ips(context) + except exception.FloatingIpNotFoundForProject: + floating_ips = [] return _translate_floating_ips_view(floating_ips) - def create(self, req): + def create(self, req, body): context = req.environ['nova.context'] try: @@ -95,9 +98,7 @@ class FloatingIPController(object): else: raise - return {'allocated': { - "id": ip['id'], - "floating_ip": ip['address']}} + return _translate_floating_ip_view(ip) def delete(self, req, id): context = req.environ['nova.context'] @@ -125,26 +126,22 @@ class FloatingIPController(object): except rpc.RemoteError: raise - return {'associated': - { - "floating_ip_id": id, - "floating_ip": floating_ip, - "fixed_ip": fixed_ip}} + floating_ip = self.network_api.get_floating_ip(context, id) + return _translate_floating_ip_view(floating_ip) def disassociate(self, req, id, body=None): """ POST /floating_ips/{id}/disassociate """ context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) address = floating_ip['address'] - fixed_ip = floating_ip['fixed_ip']['address'] try: self.network_api.disassociate_floating_ip(context, address) except rpc.RemoteError: raise - return {'disassociated': {'floating_ip': address, - 'fixed_ip': fixed_ip}} + floating_ip = self.network_api.get_floating_ip(context, id) + return _translate_floating_ip_view(floating_ip) def _get_ip_by_id(self, context, value): """Checks that value is id and then returns its address.""" -- cgit From fb43ea94e81e5eec51b73c2aab4a8a38cdf71361 Mon Sep 17 00:00:00 2001 From: Jesse Andrews Date: Tue, 16 Aug 2011 11:46:22 -0700 Subject: make delete more consistant --- nova/api/openstack/contrib/floating_ips.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 722320534..1276c0118 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -105,13 +105,13 @@ class FloatingIPController(object): ip = self.network_api.get_floating_ip(context, id) if 'fixed_ip' in ip: - self.disassociate(req, id) + try: + self.disassociate(req, id) + except exception.ApiError: + LOG.warn("disassociate failure %s", id) self.network_api.release_floating_ip(context, address=ip['address']) - - return {'released': { - "id": ip['id'], - "floating_ip": ip['address']}} + return exc.HTTPAccepted() def associate(self, req, id, body): """ /floating_ips/{id}/associate fixed ip in body """ -- cgit From ed3927455cd4054b5741fe5a3f0917d91a9066db Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 16 Aug 2011 16:50:15 -0700 Subject: fix unit tests --- .../api/openstack/contrib/test_floating_ips.py | 26 +++++++++++----------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index 704d06582..2f6f6a64d 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -153,15 +153,10 @@ class FloatingIpTest(test.TestCase): req = webob.Request.blank('/v1.1/os-floating-ips/1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - actual = json.loads(res.body)['released'] - expected = { - "id": 1, - "floating_ip": '10.10.10.10'} - self.assertEqual(actual, expected) + self.assertEqual(res.status_int, 202) def test_floating_ip_associate(self): - body = dict(associate_address=dict(fixed_ip='1.2.3.4')) + body = dict(associate_address=dict(fixed_ip='11.0.0.1')) req = webob.Request.blank('/v1.1/os-floating-ips/1/associate') req.method = 'POST' req.body = json.dumps(body) @@ -169,11 +164,13 @@ class FloatingIpTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - actual = json.loads(res.body)['associated'] + actual = json.loads(res.body)['floating_ip'] + expected = { - "floating_ip_id": '1', - "floating_ip": "10.10.10.10", - "fixed_ip": "1.2.3.4"} + "id": 1, + "instance_id": None, + "ip": "10.10.10.10", + "fixed_ip": "11.0.0.1"} self.assertEqual(actual, expected) def test_floating_ip_disassociate(self): @@ -184,8 +181,11 @@ class FloatingIpTest(test.TestCase): req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 200) - ip = json.loads(res.body)['disassociated'] + ip = json.loads(res.body)['floating_ip'] expected = { - "floating_ip": '10.10.10.10', + "id": 1, + "instance_id": None, + "ip": '10.10.10.10', "fixed_ip": '11.0.0.1'} + self.assertEqual(ip, expected) -- cgit From 83177757632b381d42cc5107fe7d1cba8830a10a Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Tue, 16 Aug 2011 16:59:36 -0700 Subject: all tests passing --- nova/api/openstack/contrib/floating_ips.py | 2 +- nova/tests/api/openstack/contrib/test_floating_ips.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 1276c0118..751b27c9f 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -85,7 +85,7 @@ class FloatingIPController(object): return _translate_floating_ips_view(floating_ips) - def create(self, req, body): + def create(self, req, body=None): context = req.environ['nova.context'] try: diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index 2f6f6a64d..9b41a58c0 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -143,10 +143,13 @@ class FloatingIpTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) print res self.assertEqual(res.status_int, 200) - ip = json.loads(res.body)['allocated'] + ip = json.loads(res.body)['floating_ip'] + expected = { "id": 1, - "floating_ip": '10.10.10.10'} + "instance_id": None, + "ip": "10.10.10.10", + "fixed_ip": None} self.assertEqual(ip, expected) def test_floating_ip_release(self): -- cgit From 4f3a33859c350ff13b2fd94e33de4f10a7f93bc1 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 17 Aug 2011 10:05:01 -0700 Subject: fix some naming inconsistencies, make associate/disassociate PUTs --- nova/api/openstack/contrib/floating_ips.py | 35 +++++++++------------- .../api/openstack/contrib/test_floating_ips.py | 6 ++-- 2 files changed, 17 insertions(+), 24 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 751b27c9f..af3eee16a 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -102,45 +102,38 @@ class FloatingIPController(object): def delete(self, req, id): context = req.environ['nova.context'] - ip = self.network_api.get_floating_ip(context, id) + floating_ip = self.network_api.get_floating_ip(context, id) - if 'fixed_ip' in ip: - try: - self.disassociate(req, id) - except exception.ApiError: - LOG.warn("disassociate failure %s", id) + if 'fixed_ip' in floating_ip: + self.network_api.disassociate_floating_ip(context, floating_ip['address']) - self.network_api.release_floating_ip(context, address=ip['address']) + self.network_api.release_floating_ip(context, address=floating_ip['address']) return exc.HTTPAccepted() def associate(self, req, id, body): - """ /floating_ips/{id}/associate fixed ip in body """ + """PUT /floating_ips/{id}/associate fixed ip in body """ context = req.environ['nova.context'] floating_ip = self._get_ip_by_id(context, id) - fixed_ip = body['associate_address']['fixed_ip'] + fixed_ip = body['floating_ip']['fixed_ip'] - try: - self.network_api.associate_floating_ip(context, - floating_ip, fixed_ip) - except rpc.RemoteError: - raise + self.network_api.associate_floating_ip(context, + floating_ip, fixed_ip) floating_ip = self.network_api.get_floating_ip(context, id) return _translate_floating_ip_view(floating_ip) def disassociate(self, req, id, body=None): - """ POST /floating_ips/{id}/disassociate """ + """PUT /floating_ips/{id}/disassociate """ context = req.environ['nova.context'] floating_ip = self.network_api.get_floating_ip(context, id) address = floating_ip['address'] - try: + # no-op if this ip is already disassociated + if 'fixed_ip' in floating_ip: self.network_api.disassociate_floating_ip(context, address) - except rpc.RemoteError: - raise + floating_ip = self.network_api.get_floating_ip(context, id) - floating_ip = self.network_api.get_floating_ip(context, id) return _translate_floating_ip_view(floating_ip) def _get_ip_by_id(self, context, value): @@ -170,8 +163,8 @@ class Floating_ips(extensions.ExtensionDescriptor): res = extensions.ResourceExtension('os-floating-ips', FloatingIPController(), member_actions={ - 'associate': 'POST', - 'disassociate': 'POST'}) + 'associate': 'PUT', + 'disassociate': 'PUT'}) resources.append(res) return resources diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index 9b41a58c0..e506519f4 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -159,9 +159,9 @@ class FloatingIpTest(test.TestCase): self.assertEqual(res.status_int, 202) def test_floating_ip_associate(self): - body = dict(associate_address=dict(fixed_ip='11.0.0.1')) + body = dict(floating_ip=dict(fixed_ip='11.0.0.1')) req = webob.Request.blank('/v1.1/os-floating-ips/1/associate') - req.method = 'POST' + req.method = 'PUT' req.body = json.dumps(body) req.headers["content-type"] = "application/json" @@ -179,7 +179,7 @@ class FloatingIpTest(test.TestCase): def test_floating_ip_disassociate(self): body = dict() req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate') - req.method = 'POST' + req.method = 'PUT' req.body = json.dumps(body) req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) -- cgit From 65d7db1136557b7af1f0b9413bacc8fc59e7211f Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Wed, 17 Aug 2011 10:23:44 -0700 Subject: pep8 fix --- nova/api/openstack/contrib/floating_ips.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index af3eee16a..2f5fdd001 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -105,9 +105,11 @@ class FloatingIPController(object): floating_ip = self.network_api.get_floating_ip(context, id) if 'fixed_ip' in floating_ip: - self.network_api.disassociate_floating_ip(context, floating_ip['address']) + self.network_api.disassociate_floating_ip(context, + floating_ip['address']) - self.network_api.release_floating_ip(context, address=floating_ip['address']) + self.network_api.release_floating_ip(context, + address=floating_ip['address']) return exc.HTTPAccepted() def associate(self, req, id, body): -- cgit From 7e3f360eb256ba82629a44de60d36be643d5105d Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 17 Aug 2011 15:33:08 -0400 Subject: Added migration for accessIPv4 and accessIPv6 --- .../versions/037_add_instances_accessip.py | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py b/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py new file mode 100644 index 000000000..82de2a874 --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py @@ -0,0 +1,49 @@ +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Integer, MetaData, Table, String + +meta = MetaData() + +accessIPv4 = Column( + 'access_ip_v4', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +accessIPv6 = Column( + 'access_ip_v6', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(accessIPv4) + instances.create_column(accessIPv6) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('access_ip_v4') + instances.drop_column('access_ip_v6') -- cgit From 8ba3ea03aa58d5b0791b9fd3654dd034cbd3a8bc Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 17 Aug 2011 15:40:17 -0400 Subject: Added accessip to models pep8 --- .../sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py | 1 - nova/db/sqlalchemy/models.py | 5 +++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py b/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py index 82de2a874..39f0dd6ce 100644 --- a/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py +++ b/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py @@ -33,7 +33,6 @@ instances = Table('instances', meta, ) - def upgrade(migrate_engine): # Upgrade operations go here. Don't create your own engine; # bind migrate_engine to your metadata diff --git a/nova/db/sqlalchemy/models.py b/nova/db/sqlalchemy/models.py index f2a4680b0..1249a6269 100644 --- a/nova/db/sqlalchemy/models.py +++ b/nova/db/sqlalchemy/models.py @@ -232,6 +232,11 @@ class Instance(BASE, NovaBase): root_device_name = Column(String(255)) + # User editable field meant to represent what ip should be used + # to connect to the instance + access_ip_v4 = Column(String(255)) + access_ip_v6 = 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 -- cgit From a4379a342798016a9dc40761561c996093945d87 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Wed, 17 Aug 2011 16:03:03 -0400 Subject: Updated server create XML deserializer to account for accessIPv4 and accessIPv6 --- nova/api/openstack/create_instance_helper.py | 3 +- nova/tests/api/openstack/test_servers.py | 56 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 4e1da549e..5ba8afe97 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -443,7 +443,8 @@ class ServerXMLDeserializerV11(wsgi.MetadataXMLDeserializer): server = {} server_node = self.find_first_child_named(node, 'server') - attributes = ["name", "imageRef", "flavorRef", "adminPass"] + attributes = ["name", "imageRef", "flavorRef", "adminPass", + "accessIPv4", "accessIPv6"] for attr in attributes: if server_node.getAttribute(attr): server[attr] = server_node.getAttribute(attr) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index a510d7d97..6f1173d46 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -2491,6 +2491,62 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): } self.assertEquals(request['body'], expected) + def test_access_ipv4(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ipv6(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv6": "fead:::::1234", + }, + } + self.assertEquals(request['body'], expected) + + def test_access_ip(self): + serial_request = """ +""" + request = self.deserializer.deserialize(serial_request, 'create') + expected = { + "server": { + "name": "new-server-test", + "imageRef": "1", + "flavorRef": "2", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead:::::1234", + }, + } + self.assertEquals(request['body'], expected) + def test_admin_pass(self): serial_request = """ Date: Thu, 18 Aug 2011 10:53:01 -0400 Subject: Added accessIPv4 and accessIPv6 to servers view builder Updated compute api to handle accessIPv4 and 6 --- nova/api/openstack/create_instance_helper.py | 2 + nova/api/openstack/views/servers.py | 4 + nova/compute/api.py | 15 ++- nova/tests/api/openstack/test_servers.py | 173 +++++++++++++++++++++++++++ 4 files changed, 189 insertions(+), 5 deletions(-) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 5ba8afe97..332d5d9bb 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -157,6 +157,8 @@ class CreateInstanceHelper(object): key_name=key_name, key_data=key_data, metadata=server_dict.get('metadata', {}), + access_ip_v4=server_dict.get('accessIPv4'), + access_ip_v6=server_dict.get('accessIPv6'), injected_files=injected_files, admin_password=password, zone_blob=zone_blob, diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index edc328129..3b91c037a 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -182,6 +182,10 @@ class ViewBuilderV11(ViewBuilder): def _build_extra(self, response, inst): self._build_links(response, inst) response['uuid'] = inst['uuid'] + if inst.get('access_ip_v4'): + response['accessIPv4'] = inst['access_ip_v4'] + if inst.get('access_ip_v6'): + response['accessIPv6'] = inst['access_ip_v6'] def _build_links(self, response, inst): href = self.generate_href(inst["id"]) diff --git a/nova/compute/api.py b/nova/compute/api.py index e909e9959..168d46689 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -153,7 +153,7 @@ class API(base.Base): key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None): + reservation_id=None, access_ip_v4=None, access_ip_v6=None): """Verify all the input parameters regardless of the provisioning strategy being performed.""" @@ -247,6 +247,8 @@ class API(base.Base): 'key_data': key_data, 'locked': False, 'metadata': metadata, + 'access_ip_v4': access_ip_v4, + 'access_ip_v6': access_ip_v6, 'availability_zone': availability_zone, 'os_type': os_type, 'architecture': architecture, @@ -421,6 +423,7 @@ class API(base.Base): 'num_instances': num_instances, } + print base_options rpc.cast(context, FLAGS.scheduler_topic, {"method": "run_instance", @@ -438,7 +441,8 @@ class API(base.Base): key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None, block_device_mapping=None): + reservation_id=None, block_device_mapping=None, + access_ip_v4=None, access_ip_v6=None): """Provision the instances by passing the whole request to the Scheduler for execution. Returns a Reservation ID related to the creation of all of these instances.""" @@ -454,7 +458,7 @@ class API(base.Base): key_name, key_data, security_group, availability_zone, user_data, metadata, injected_files, admin_password, zone_blob, - reservation_id) + reservation_id, access_ip_v4, access_ip_v6) self._ask_scheduler_to_create_instance(context, base_options, instance_type, zone_blob, @@ -472,7 +476,8 @@ class API(base.Base): key_name=None, key_data=None, security_group='default', availability_zone=None, user_data=None, metadata=None, injected_files=None, admin_password=None, zone_blob=None, - reservation_id=None, block_device_mapping=None): + reservation_id=None, block_device_mapping=None, + access_ip_v4=None, access_ip_v6=None): """ Provision the instances by sending off a series of single instance requests to the Schedulers. This is fine for trival @@ -496,7 +501,7 @@ class API(base.Base): key_name, key_data, security_group, availability_zone, user_data, metadata, injected_files, admin_password, zone_blob, - reservation_id) + reservation_id, access_ip_v4, access_ip_v6) block_device_mapping = block_device_mapping or [] instances = [] diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 6f1173d46..25fce95b4 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -1379,6 +1379,8 @@ class ServersTest(test.TestCase): 'display_name': 'server_test', 'uuid': FAKE_UUID, 'instance_type': dict(inst_type), + 'access_ip_v4': '1.2.3.4', + 'access_ip_v6': 'fead::1234', 'image_ref': image_ref, "created_at": datetime.datetime(2010, 10, 10, 12, 0, 0), "updated_at": datetime.datetime(2010, 11, 11, 11, 0, 0), @@ -1579,6 +1581,69 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_create_instance_with_access_ip_v1_1(self): + self._setup_for_create_instance() + + # proper local hrefs must start with 'http://localhost/v1.1/' + image_href = 'http://localhost/v1.1/images/2' + flavor_ref = 'http://localhost/flavors/3' + access_ipv4 = '1.2.3.4' + access_ipv6 = 'fead::1234' + expected_flavor = { + "id": "3", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/flavors/3', + }, + ], + } + expected_image = { + "id": "2", + "links": [ + { + "rel": "bookmark", + "href": 'http://localhost/images/2', + }, + ], + } + body = { + 'server': { + 'name': 'server_test', + 'imageRef': image_href, + 'flavorRef': flavor_ref, + 'accessIPv4': access_ipv4, + 'accessIPv6': access_ipv6, + 'metadata': { + 'hello': 'world', + 'open': 'stack', + }, + 'personality': [ + { + "path": "/etc/banner.txt", + "contents": "MQ==", + }, + ], + }, + } + + req = webob.Request.blank('/v1.1/servers') + req.method = 'POST' + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(res.status_int, 202) + server = json.loads(res.body)['server'] + self.assertEqual(16, len(server['adminPass'])) + self.assertEqual(1, server['id']) + self.assertEqual(0, server['progress']) + self.assertEqual('server_test', server['name']) + self.assertEqual(expected_flavor, server['flavor']) + self.assertEqual(expected_image, server['image']) + self.assertEqual(access_ipv4, server['accessIPv4']) + def test_create_instance_v1_1(self): self._setup_for_create_instance() @@ -3095,6 +3160,8 @@ class ServersViewBuilderV11Test(test.TestCase): "display_description": "", "locked": False, "metadata": [], + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::::1234", #"address": , #"floating_ips": [{"address":ip} for ip in public_addresses]} "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"} @@ -3237,6 +3304,112 @@ class ServersViewBuilderV11Test(test.TestCase): output = self.view_builder.build(self.instance, True) self.assertDictMatch(output, expected_server) + def test_build_server_detail_with_accessipv4(self): + + self.instance['access_ip_v4'] = '1.2.3.4' + + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + expected_server = { + "server": { + "id": 1, + "uuid": self.instance['uuid'], + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": {}, + "metadata": {}, + "accessIPv4": "1.2.3.4", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/servers/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/1", + }, + ], + } + } + + output = self.view_builder.build(self.instance, True) + self.assertDictMatch(output, expected_server) + + def test_build_server_detail_with_accessipv6(self): + + self.instance['access_ip_v6'] = 'fead::1234' + + image_bookmark = "http://localhost/images/5" + flavor_bookmark = "http://localhost/flavors/1" + expected_server = { + "server": { + "id": 1, + "uuid": self.instance['uuid'], + "updated": "2010-11-11T11:00:00Z", + "created": "2010-10-10T12:00:00Z", + "progress": 0, + "name": "test_server", + "status": "BUILD", + "hostId": '', + "image": { + "id": "5", + "links": [ + { + "rel": "bookmark", + "href": image_bookmark, + }, + ], + }, + "flavor": { + "id": "1", + "links": [ + { + "rel": "bookmark", + "href": flavor_bookmark, + }, + ], + }, + "addresses": {}, + "metadata": {}, + "accessIPv6": "fead::1234", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/servers/1", + }, + { + "rel": "bookmark", + "href": "http://localhost/servers/1", + }, + ], + } + } + + output = self.view_builder.build(self.instance, True) + self.assertDictMatch(output, expected_server) + def test_build_server_detail_with_metadata(self): metadata = [] -- cgit From 9b5416e8afc115fabb76664a65b6d33e9ba89b7f Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 18 Aug 2011 11:05:59 -0400 Subject: Updated ServersXMLSerializer to allow accessIPv4 and accessIPv6 in XML responses --- nova/api/openstack/servers.py | 4 ++++ nova/tests/api/openstack/test_servers.py | 6 ++++++ 2 files changed, 10 insertions(+) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index 335ecad86..f06ee6b62 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -837,6 +837,10 @@ class ServerXMLSerializer(wsgi.XMLDictSerializer): node.setAttribute('created', str(server['created'])) node.setAttribute('updated', str(server['updated'])) node.setAttribute('status', server['status']) + if 'accessIPv4' in server: + node.setAttribute('accessIPv4', str(server['accessIPv4'])) + if 'accessIPv6' in server: + node.setAttribute('accessIPv6', str(server['accessIPv6'])) if 'progress' in server: node.setAttribute('progress', str(server['progress'])) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 25fce95b4..0bdbb2006 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -3494,6 +3494,8 @@ class ServerXMLSerializationTest(test.TestCase): "name": "test_server", "status": "BUILD", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "image": { "id": "5", "links": [ @@ -3570,6 +3572,8 @@ class ServerXMLSerializationTest(test.TestCase): created="%(expected_now)s" hostId="e4d909c290d0fb1ca068ffaddf22cbd0" status="BUILD" + accessIPv4="1.2.3.4" + accessIPv6="fead::1234" progress="0"> @@ -3614,6 +3618,7 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv6": "fead::1234", "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "adminPass": "test_password", "image": { @@ -3692,6 +3697,7 @@ class ServerXMLSerializationTest(test.TestCase): created="%(expected_now)s" hostId="e4d909c290d0fb1ca068ffaddf22cbd0" status="BUILD" + accessIPv6="fead::1234" adminPass="test_password" progress="0"> -- cgit From 155d640d3d53bcf76daa0ff0ae67ac5dbbe3022a Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 18 Aug 2011 12:19:47 -0400 Subject: Fixed issue where accessIP was added in none detail responses --- nova/api/openstack/views/servers.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 3b91c037a..8b3a1e221 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -143,6 +143,12 @@ class ViewBuilderV11(ViewBuilder): response['server']['progress'] = 100 elif response['server']['status'] == "BUILD": response['server']['progress'] = 0 + + if inst.get('access_ip_v4'): + response['server']['accessIPv4'] = inst['access_ip_v4'] + if inst.get('access_ip_v6'): + response['server']['accessIPv6'] = inst['access_ip_v6'] + return response def _build_image(self, response, inst): @@ -182,10 +188,6 @@ class ViewBuilderV11(ViewBuilder): def _build_extra(self, response, inst): self._build_links(response, inst) response['uuid'] = inst['uuid'] - if inst.get('access_ip_v4'): - response['accessIPv4'] = inst['access_ip_v4'] - if inst.get('access_ip_v6'): - response['accessIPv6'] = inst['access_ip_v6'] def _build_links(self, response, inst): href = self.generate_href(inst["id"]) -- cgit From 9033d4879556452d3b7c0ee9fa9fcafbea59e5be Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 18 Aug 2011 12:55:27 -0400 Subject: minor cleanup --- nova/compute/api.py | 1 - nova/tests/api/openstack/test_servers.py | 10 +++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/nova/compute/api.py b/nova/compute/api.py index 168d46689..49222d476 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -423,7 +423,6 @@ class API(base.Base): 'num_instances': num_instances, } - print base_options rpc.cast(context, FLAGS.scheduler_topic, {"method": "run_instance", diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 0bdbb2006..b3e9bfe04 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -2580,14 +2580,14 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): name="new-server-test" imageRef="1" flavorRef="2" - accessIPv6="fead:::::1234"/>""" + accessIPv6="fead::1234"/>""" request = self.deserializer.deserialize(serial_request, 'create') expected = { "server": { "name": "new-server-test", "imageRef": "1", "flavorRef": "2", - "accessIPv6": "fead:::::1234", + "accessIPv6": "fead::1234", }, } self.assertEquals(request['body'], expected) @@ -2599,7 +2599,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): imageRef="1" flavorRef="2" accessIPv4="1.2.3.4" - accessIPv6="fead:::::1234"/>""" + accessIPv6="fead::1234"/>""" request = self.deserializer.deserialize(serial_request, 'create') expected = { "server": { @@ -2607,7 +2607,7 @@ class TestServerCreateRequestXMLDeserializerV11(test.TestCase): "imageRef": "1", "flavorRef": "2", "accessIPv4": "1.2.3.4", - "accessIPv6": "fead:::::1234", + "accessIPv6": "fead::1234", }, } self.assertEquals(request['body'], expected) @@ -3161,7 +3161,7 @@ class ServersViewBuilderV11Test(test.TestCase): "locked": False, "metadata": [], "accessIPv4": "1.2.3.4", - "accessIPv6": "fead::::1234", + "accessIPv6": "fead::1234", #"address": , #"floating_ips": [{"address":ip} for ip in public_addresses]} "uuid": "deadbeef-feed-edee-beef-d0ea7beefedd"} -- cgit From cca07a461d6c826a9dcc902b7b88afe602377756 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Thu, 18 Aug 2011 16:27:49 -0400 Subject: updated PUT to severs/id to handle accessIPv4 and accessIPv6 --- nova/api/openstack/servers.py | 10 +++++- nova/tests/api/openstack/test_servers.py | 53 +++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/nova/api/openstack/servers.py b/nova/api/openstack/servers.py index f06ee6b62..df55d981a 100644 --- a/nova/api/openstack/servers.py +++ b/nova/api/openstack/servers.py @@ -163,7 +163,7 @@ class Controller(object): @scheduler_api.redirect_handler def update(self, req, id, body): - """Update server name then pass on to version-specific controller""" + """Update server then pass on to version-specific controller""" if len(req.body) == 0: raise exc.HTTPUnprocessableEntity() @@ -178,6 +178,14 @@ class Controller(object): self.helper._validate_server_name(name) update_dict['display_name'] = name.strip() + if 'accessIPv4' in body['server']: + access_ipv4 = body['server']['accessIPv4'] + update_dict['access_ip_v4'] = access_ipv4.strip() + + if 'accessIPv6' in body['server']: + access_ipv6 = body['server']['accessIPv6'] + update_dict['access_ip_v6'] = access_ipv6.strip() + try: self.compute_api.update(ctxt, id, **update_dict) except exception.NotFound: diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index b3e9bfe04..a813d4f96 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -145,7 +145,8 @@ def instance_addresses(context, instance_id): def stub_instance(id, user_id='fake', project_id='fake', private_address=None, public_addresses=None, host=None, power_state=0, reservation_id="", uuid=FAKE_UUID, image_ref="10", - flavor_id="1", interfaces=None, name=None): + flavor_id="1", interfaces=None, name=None, + access_ipv4=None, access_ipv6=None): metadata = [] metadata.append(InstanceMetadata(key='seq', value=id)) @@ -197,6 +198,8 @@ def stub_instance(id, user_id='fake', project_id='fake', private_address=None, "display_description": "", "locked": False, "metadata": metadata, + "access_ip_v4": access_ipv4, + "access_ip_v6": access_ipv6, "uuid": uuid, "virtual_interfaces": interfaces} @@ -1944,6 +1947,28 @@ class ServersTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 400) + def test_update_server_all_attributes_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(name='server_test', + access_ipv4='0.0.0.0', + access_ipv6='beef::0123')) + req = webob.Request.blank('/v1.1/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + body = {'server': { + 'name': 'server_test', + 'accessIPv4': '0.0.0.0', + 'accessIPv6': 'beef::0123', + }} + req.body = json.dumps(body) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['name'], 'server_test') + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') + def test_update_server_name_v1_1(self): self.stubs.Set(nova.db.api, 'instance_get', return_server_with_attributes(name='server_test')) @@ -1957,6 +1982,32 @@ class ServersTest(test.TestCase): self.assertEqual(res_dict['server']['id'], 1) self.assertEqual(res_dict['server']['name'], 'server_test') + def test_update_server_access_ipv4_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(access_ipv4='0.0.0.0')) + req = webob.Request.blank('/v1.1/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + req.body = json.dumps({'server': {'accessIPv4': '0.0.0.0'}}) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['accessIPv4'], '0.0.0.0') + + def test_update_server_access_ipv6_v1_1(self): + self.stubs.Set(nova.db.api, 'instance_get', + return_server_with_attributes(access_ipv6='beef::0123')) + req = webob.Request.blank('/v1.1/servers/1') + req.method = 'PUT' + req.content_type = 'application/json' + req.body = json.dumps({'server': {'accessIPv6': 'beef::0123'}}) + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(res.status_int, 200) + res_dict = json.loads(res.body) + self.assertEqual(res_dict['server']['id'], 1) + self.assertEqual(res_dict['server']['accessIPv6'], 'beef::0123') + def test_update_server_adminPass_ignored_v1_1(self): inst_dict = dict(name='server_test', adminPass='bacon') self.body = json.dumps(dict(server=inst_dict)) -- cgit From be0c70562ce978e3ffa85465fc08dd5cb3ca07c3 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 19 Aug 2011 10:47:16 -0400 Subject: updated migration number --- .../versions/037_add_instances_accessip.py | 48 ---------------------- .../versions/038_add_instances_accessip.py | 48 ++++++++++++++++++++++ 2 files changed, 48 insertions(+), 48 deletions(-) delete mode 100644 nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py create mode 100644 nova/db/sqlalchemy/migrate_repo/versions/038_add_instances_accessip.py diff --git a/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py b/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py deleted file mode 100644 index 39f0dd6ce..000000000 --- a/nova/db/sqlalchemy/migrate_repo/versions/037_add_instances_accessip.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright 2011 OpenStack LLC. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from sqlalchemy import Column, Integer, MetaData, Table, String - -meta = MetaData() - -accessIPv4 = Column( - 'access_ip_v4', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False), - nullable=True) - -accessIPv6 = Column( - 'access_ip_v6', - String(length=255, convert_unicode=False, assert_unicode=None, - unicode_error=None, _warn_on_bytestring=False), - nullable=True) - -instances = Table('instances', meta, - Column('id', Integer(), primary_key=True, nullable=False), - ) - - -def upgrade(migrate_engine): - # Upgrade operations go here. Don't create your own engine; - # bind migrate_engine to your metadata - meta.bind = migrate_engine - instances.create_column(accessIPv4) - instances.create_column(accessIPv6) - - -def downgrade(migrate_engine): - # Operations to reverse the above upgrade go here. - meta.bind = migrate_engine - instances.drop_column('access_ip_v4') - instances.drop_column('access_ip_v6') diff --git a/nova/db/sqlalchemy/migrate_repo/versions/038_add_instances_accessip.py b/nova/db/sqlalchemy/migrate_repo/versions/038_add_instances_accessip.py new file mode 100644 index 000000000..39f0dd6ce --- /dev/null +++ b/nova/db/sqlalchemy/migrate_repo/versions/038_add_instances_accessip.py @@ -0,0 +1,48 @@ +# Copyright 2011 OpenStack LLC. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from sqlalchemy import Column, Integer, MetaData, Table, String + +meta = MetaData() + +accessIPv4 = Column( + 'access_ip_v4', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +accessIPv6 = Column( + 'access_ip_v6', + String(length=255, convert_unicode=False, assert_unicode=None, + unicode_error=None, _warn_on_bytestring=False), + nullable=True) + +instances = Table('instances', meta, + Column('id', Integer(), primary_key=True, nullable=False), + ) + + +def upgrade(migrate_engine): + # Upgrade operations go here. Don't create your own engine; + # bind migrate_engine to your metadata + meta.bind = migrate_engine + instances.create_column(accessIPv4) + instances.create_column(accessIPv6) + + +def downgrade(migrate_engine): + # Operations to reverse the above upgrade go here. + meta.bind = migrate_engine + instances.drop_column('access_ip_v4') + instances.drop_column('access_ip_v6') -- cgit From c11a156b1e50fde6cf3047057746564d491634e2 Mon Sep 17 00:00:00 2001 From: Sandy Walsh Date: Fri, 19 Aug 2011 10:01:25 -0700 Subject: Fixes primitive with builtins, modules, etc --- nova/tests/test_utils.py | 10 ++++++++++ nova/utils.py | 12 +++++++++--- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/nova/tests/test_utils.py b/nova/tests/test_utils.py index ec5098a37..28e366a8e 100644 --- a/nova/tests/test_utils.py +++ b/nova/tests/test_utils.py @@ -384,3 +384,13 @@ class ToPrimitiveTestCase(test.TestCase): def test_typeerror(self): x = bytearray # Class, not instance self.assertEquals(utils.to_primitive(x), u"") + + def test_nasties(self): + def foo(): + pass + x = [datetime, foo, dir] + ret = utils.to_primitive(x) + self.assertEquals(len(ret), 3) + self.assertTrue(ret[0].startswith(u"') diff --git a/nova/utils.py b/nova/utils.py index 54126f644..b42f76457 100644 --- a/nova/utils.py +++ b/nova/utils.py @@ -547,11 +547,17 @@ def to_primitive(value, convert_instances=False, level=0): Therefore, convert_instances=True is lossy ... be aware. """ - if inspect.isclass(value): - return unicode(value) + nasty = [inspect.ismodule, inspect.isclass, inspect.ismethod, + inspect.isfunction, inspect.isgeneratorfunction, + inspect.isgenerator, inspect.istraceback, inspect.isframe, + inspect.iscode, inspect.isbuiltin, inspect.isroutine, + inspect.isabstract] + for test in nasty: + if test(value): + return unicode(value) if level > 3: - return [] + return '?' # The try block may not be necessary after the class check above, # but just in case ... -- cgit From fe8800ada8670cb29417fcdec085800b66cd881f Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 19 Aug 2011 15:21:04 -0400 Subject: Updated test_show in ServerXMLSerializationTest to use XML validation --- nova/tests/api/openstack/test_servers.py | 101 ++++++++++++++++++------------- 1 file changed, 58 insertions(+), 43 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 326962d72..cfe3f624e 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -19,6 +19,7 @@ import base64 import datetime import json import unittest +from lxml import etree from xml.dom import minidom import webob @@ -32,6 +33,7 @@ import nova.api.openstack from nova.api.openstack import create_instance_helper from nova.api.openstack import servers from nova.api.openstack import wsgi +from nova.api.openstack import xmlutil import nova.compute.api from nova.compute import instance_types from nova.compute import power_state @@ -46,6 +48,8 @@ from nova.tests.api.openstack import fakes FAKE_UUID = 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa' +NS = "{http://docs.openstack.org/compute/api/v1.1}" +ATOMNS = "{http://www.w3.org/2005/Atom}" def fake_gen_uuid(): @@ -3605,7 +3609,9 @@ class ServerXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'show') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') expected_server_href = self.SERVER_HREF expected_server_bookmark = self.SERVER_BOOKMARK @@ -3613,49 +3619,58 @@ class ServerXMLSerializationTest(test.TestCase): expected_flavor_bookmark = self.FLAVOR_BOOKMARK expected_now = self.TIMESTAMP expected_uuid = FAKE_UUID - expected = minidom.parseString(""" - - - - - - - - - - - - Stack - - - 1 - - - - - - - - - - - - - - """.replace(" ", "") % (locals())) + server_dict = fixture['server'] + + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(root.get(key), str(server_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) - self.assertEqual(expected.toxml(), actual.toxml()) def test_create(self): serializer = servers.ServerXMLSerializer() -- cgit From c75e132786a65501477f77efa1bc9147b7763c31 Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 19 Aug 2011 15:55:56 -0400 Subject: Finished changing ServerXMLSerializationTest to use XML validation and lxml --- nova/api/openstack/schemas/v1.1/server.rng | 50 ++++ nova/api/openstack/schemas/v1.1/servers.rng | 6 + nova/api/openstack/schemas/v1.1/servers_index.rng | 12 + nova/tests/api/openstack/test_servers.py | 349 ++++++++++++---------- 4 files changed, 251 insertions(+), 166 deletions(-) create mode 100644 nova/api/openstack/schemas/v1.1/server.rng create mode 100644 nova/api/openstack/schemas/v1.1/servers.rng create mode 100644 nova/api/openstack/schemas/v1.1/servers_index.rng diff --git a/nova/api/openstack/schemas/v1.1/server.rng b/nova/api/openstack/schemas/v1.1/server.rng new file mode 100644 index 000000000..dbd169a83 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/server.rng @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/nova/api/openstack/schemas/v1.1/servers.rng b/nova/api/openstack/schemas/v1.1/servers.rng new file mode 100644 index 000000000..4e2bb8853 --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/servers.rng @@ -0,0 +1,6 @@ + + + + + diff --git a/nova/api/openstack/schemas/v1.1/servers_index.rng b/nova/api/openstack/schemas/v1.1/servers_index.rng new file mode 100644 index 000000000..768f0912d --- /dev/null +++ b/nova/api/openstack/schemas/v1.1/servers_index.rng @@ -0,0 +1,12 @@ + + + + + + + + + + + diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index cfe3f624e..961d2fb7c 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -3684,6 +3684,7 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "1.2.3.4", "accessIPv6": "fead::1234", "hostId": "e4d909c290d0fb1ca068ffaddf22cbd0", "adminPass": "test_password", @@ -3745,7 +3746,9 @@ class ServerXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'create') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') expected_server_href = self.SERVER_HREF expected_server_bookmark = self.SERVER_BOOKMARK @@ -3753,49 +3756,57 @@ class ServerXMLSerializationTest(test.TestCase): expected_flavor_bookmark = self.FLAVOR_BOOKMARK expected_now = self.TIMESTAMP expected_uuid = FAKE_UUID - expected = minidom.parseString(""" - - - - - - - - - - - - Stack - - - 1 - - - - - - - - - - - - - - """.replace(" ", "") % (locals())) + server_dict = fixture['server'] - self.assertEqual(expected.toxml(), actual.toxml()) + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6', 'adminPass']: + self.assertEqual(root.get(key), str(server_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) def test_index(self): serializer = servers.ServerXMLSerializer() @@ -3836,23 +3847,21 @@ class ServerXMLSerializationTest(test.TestCase): ]} output = serializer.serialize(fixture, 'index') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - - - - - - - - - - - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers_index') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + for key in ['name', 'id']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) + + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) def test_detail(self): serializer = servers.ServerXMLSerializer() @@ -3875,6 +3884,8 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', "image": { "id": "5", @@ -3928,6 +3939,8 @@ class ServerXMLSerializationTest(test.TestCase): "progress": 100, "name": "test_server_2", "status": "ACTIVE", + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', "image": { "id": "5", @@ -3976,71 +3989,61 @@ class ServerXMLSerializationTest(test.TestCase): ]} output = serializer.serialize(fixture, 'detail') - actual = minidom.parseString(output.replace(" ", "")) - - expected = minidom.parseString(""" - - - - - - - - - - - - - 1 - - - - - - - - - - - - - - - - - - - - - 2 - - - - - - - - - - - """.replace(" ", "") % (locals())) - - self.assertEqual(expected.toxml(), actual.toxml()) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'servers') + server_elems = root.findall('{0}server'.format(NS)) + self.assertEqual(len(server_elems), 2) + for i, server_elem in enumerate(server_elems): + server_dict = fixture['servers'][i] + + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(server_elem.get(key), str(server_dict[key])) + + link_nodes = server_elem.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = server_elem.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = server_elem.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = server_elem.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = server_elem.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) def test_update(self): serializer = servers.ServerXMLSerializer() @@ -4055,6 +4058,8 @@ class ServerXMLSerializationTest(test.TestCase): "name": "test_server", "status": "BUILD", "hostId": 'e4d909c290d0fb1ca068ffaddf22cbd0', + "accessIPv4": "1.2.3.4", + "accessIPv6": "fead::1234", "image": { "id": "5", "links": [ @@ -4113,7 +4118,9 @@ class ServerXMLSerializationTest(test.TestCase): } output = serializer.serialize(fixture, 'update') - actual = minidom.parseString(output.replace(" ", "")) + print output + root = etree.XML(output) + xmlutil.validate_schema(root, 'server') expected_server_href = self.SERVER_HREF expected_server_bookmark = self.SERVER_BOOKMARK @@ -4121,44 +4128,54 @@ class ServerXMLSerializationTest(test.TestCase): expected_flavor_bookmark = self.FLAVOR_BOOKMARK expected_now = self.TIMESTAMP expected_uuid = FAKE_UUID - expected = minidom.parseString(""" - - - - - - - - - - - - Stack - - - 1 - - - - - - - - - - - - - - """.replace(" ", "") % (locals())) + server_dict = fixture['server'] - self.assertEqual(expected.toxml(), actual.toxml()) + for key in ['name', 'id', 'uuid', 'created', 'accessIPv4', + 'updated', 'progress', 'status', 'hostId', + 'accessIPv6']: + self.assertEqual(root.get(key), str(server_dict[key])) + + link_nodes = root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 2) + for i, link in enumerate(server_dict['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + metadata_root = root.find('{0}metadata'.format(NS)) + metadata_elems = metadata_root.findall('{0}meta'.format(NS)) + self.assertEqual(len(metadata_elems), 2) + for i, metadata_elem in enumerate(metadata_elems): + (meta_key, meta_value) = server_dict['metadata'].items()[i] + self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) + self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + + image_root = root.find('{0}image'.format(NS)) + self.assertEqual(image_root.get('id'), server_dict['image']['id']) + link_nodes = image_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['image']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + flavor_root = root.find('{0}flavor'.format(NS)) + self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) + self.assertEqual(len(link_nodes), 1) + for i, link in enumerate(server_dict['flavor']['links']): + for key, value in link.items(): + self.assertEqual(link_nodes[i].get(key), value) + + addresses_root = root.find('{0}addresses'.format(NS)) + addresses_dict = server_dict['addresses'] + network_elems = addresses_root.findall('{0}network'.format(NS)) + self.assertEqual(len(network_elems), 2) + for i, network_elem in enumerate(network_elems): + network = addresses_dict.items()[i] + self.assertEqual(str(network_elem.get('id')), str(network[0])) + ip_elems = network_elem.findall('{0}ip'.format(NS)) + for z, ip_elem in enumerate(ip_elems): + ip = network[1][z] + self.assertEqual(str(ip_elem.get('version')), + str(ip['version'])) + self.assertEqual(str(ip_elem.get('addr')), + str(ip['addr'])) -- cgit From 9827c92838d144f7c129e9e5545126f100926dba Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 19 Aug 2011 15:58:50 -0400 Subject: pep8 --- nova/tests/api/openstack/test_servers.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 961d2fb7c..6cf2d2d6a 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -3671,7 +3671,6 @@ class ServerXMLSerializationTest(test.TestCase): self.assertEqual(str(ip_elem.get('addr')), str(ip['addr'])) - def test_create(self): serializer = servers.ServerXMLSerializer() @@ -4013,7 +4012,8 @@ class ServerXMLSerializationTest(test.TestCase): for i, metadata_elem in enumerate(metadata_elems): (meta_key, meta_value) = server_dict['metadata'].items()[i] self.assertEqual(str(metadata_elem.get('key')), str(meta_key)) - self.assertEqual(str(metadata_elem.text).strip(), str(meta_value)) + self.assertEqual(str(metadata_elem.text).strip(), + str(meta_value)) image_root = server_elem.find('{0}image'.format(NS)) self.assertEqual(image_root.get('id'), server_dict['image']['id']) @@ -4024,7 +4024,8 @@ class ServerXMLSerializationTest(test.TestCase): self.assertEqual(link_nodes[i].get(key), value) flavor_root = server_elem.find('{0}flavor'.format(NS)) - self.assertEqual(flavor_root.get('id'), server_dict['flavor']['id']) + self.assertEqual(flavor_root.get('id'), + server_dict['flavor']['id']) link_nodes = flavor_root.findall('{0}link'.format(ATOMNS)) self.assertEqual(len(link_nodes), 1) for i, link in enumerate(server_dict['flavor']['links']): -- cgit From 5366332a84b89bc5a056bd7f43e528a908e8d188 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Fri, 19 Aug 2011 13:15:42 -0700 Subject: incorporate feedback from brian waldon and brian lamar. Move associate/disassociate to server actions --- nova/api/openstack/contrib/floating_ips.py | 69 ++++++++++++++-------- .../api/openstack/contrib/test_floating_ips.py | 57 ++++++++---------- 2 files changed, 69 insertions(+), 57 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 2f5fdd001..b305ebdcb 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -15,8 +15,9 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License -from webob import exc +import webob +from nova import compute from nova import exception from nova import log as logging from nova import network @@ -71,7 +72,7 @@ class FloatingIPController(object): try: floating_ip = self.network_api.get_floating_ip(context, id) except exception.NotFound: - return faults.Fault(exc.HTTPNotFound()) + return faults.Fault(webob.exc.HTTPNotFound()) return _translate_floating_ip_view(floating_ip) @@ -110,40 +111,49 @@ class FloatingIPController(object): self.network_api.release_floating_ip(context, address=floating_ip['address']) - return exc.HTTPAccepted() + return webob.exc.HTTPAccepted() - def associate(self, req, id, body): - """PUT /floating_ips/{id}/associate fixed ip in body """ + def _get_ip_by_id(self, context, value): + """Checks that value is id and then returns its address.""" + return self.network_api.get_floating_ip(context, value)['address'] + + +class Floating_ips(extensions.ExtensionDescriptor): + def __init__(self): + self.compute_api = compute.API() + self.network_api = network.API() + super(Floating_ips, self).__init__() + + def _add_floating_ip(self, input_dict, req, instance_id): + """Associate floating_ip to an instance.""" context = req.environ['nova.context'] - floating_ip = self._get_ip_by_id(context, id) - fixed_ip = body['floating_ip']['fixed_ip'] + try: + address = input_dict['addFloatingIp']['address'] + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) - self.network_api.associate_floating_ip(context, - floating_ip, fixed_ip) + self.compute_api.associate_floating_ip(context, instance_id, address) - floating_ip = self.network_api.get_floating_ip(context, id) - return _translate_floating_ip_view(floating_ip) + return webob.Response(status_int=202) - def disassociate(self, req, id, body=None): - """PUT /floating_ips/{id}/disassociate """ + def _remove_floating_ip(self, input_dict, req, instance_id): + """Dissociate floating_ip from an instance.""" context = req.environ['nova.context'] - floating_ip = self.network_api.get_floating_ip(context, id) - address = floating_ip['address'] - # no-op if this ip is already disassociated + try: + address = input_dict['removeFloatingIp']['address'] + except KeyError: + msg = _("Address not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + floating_ip = self.network_api.get_floating_ip_by_ip(context, address) if 'fixed_ip' in floating_ip: self.network_api.disassociate_floating_ip(context, address) - floating_ip = self.network_api.get_floating_ip(context, id) - - return _translate_floating_ip_view(floating_ip) - - def _get_ip_by_id(self, context, value): - """Checks that value is id and then returns its address.""" - return self.network_api.get_floating_ip(context, value)['address'] + return webob.Response(status_int=202) -class Floating_ips(extensions.ExtensionDescriptor): def get_name(self): return "Floating_ips" @@ -170,3 +180,14 @@ class Floating_ips(extensions.ExtensionDescriptor): resources.append(res) return resources + + def get_actions(self): + """Return the actions the extension adds, as required by contract.""" + actions = [ + extensions.ActionExtension("servers", "addFloatingIp", + self._add_floating_ip), + extensions.ActionExtension("servers", "removeFloatingIp", + self._remove_floating_ip), + ] + + return actions diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index e506519f4..09234072a 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -17,6 +17,7 @@ import json import stubout import webob +from nova import compute from nova import context from nova import db from nova import test @@ -29,6 +30,11 @@ from nova.api.openstack.contrib.floating_ips import _translate_floating_ip_view def network_api_get_floating_ip(self, context, id): + return {'id': 1, 'address': '10.10.10.10', + 'fixed_ip': None} + + +def network_api_get_floating_ip_by_ip(self, context, address): return {'id': 1, 'address': '10.10.10.10', 'fixed_ip': {'address': '11.0.0.1'}} @@ -50,7 +56,7 @@ def network_api_release(self, context, address): pass -def network_api_associate(self, context, floating_ip, fixed_ip): +def compute_api_associate(self, context, instance_id, floating_ip): pass @@ -78,14 +84,16 @@ class FloatingIpTest(test.TestCase): fakes.stub_out_rate_limiting(self.stubs) self.stubs.Set(network.api.API, "get_floating_ip", network_api_get_floating_ip) + self.stubs.Set(network.api.API, "get_floating_ip_by_ip", + network_api_get_floating_ip) self.stubs.Set(network.api.API, "list_floating_ips", network_api_list_floating_ips) self.stubs.Set(network.api.API, "allocate_floating_ip", network_api_allocate) self.stubs.Set(network.api.API, "release_floating_ip", network_api_release) - self.stubs.Set(network.api.API, "associate_floating_ip", - network_api_associate) + self.stubs.Set(compute.api.API, "associate_floating_ip", + compute_api_associate) self.stubs.Set(network.api.API, "disassociate_floating_ip", network_api_disassociate) self.context = context.get_admin_context() @@ -133,7 +141,6 @@ class FloatingIpTest(test.TestCase): res_dict = json.loads(res.body) self.assertEqual(res_dict['floating_ip']['id'], 1) self.assertEqual(res_dict['floating_ip']['ip'], '10.10.10.10') - self.assertEqual(res_dict['floating_ip']['fixed_ip'], '11.0.0.1') self.assertEqual(res_dict['floating_ip']['instance_id'], None) def test_floating_ip_allocate(self): @@ -141,7 +148,6 @@ class FloatingIpTest(test.TestCase): req.method = 'POST' req.headers['Content-Type'] = 'application/json' res = req.get_response(fakes.wsgi_app()) - print res self.assertEqual(res.status_int, 200) ip = json.loads(res.body)['floating_ip'] @@ -158,37 +164,22 @@ class FloatingIpTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 202) - def test_floating_ip_associate(self): - body = dict(floating_ip=dict(fixed_ip='11.0.0.1')) - req = webob.Request.blank('/v1.1/os-floating-ips/1/associate') - req.method = 'PUT' + def test_add_floating_ip_to_instance(self): + body = dict(addFloatingIp=dict(address='11.0.0.1')) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" req.body = json.dumps(body) req.headers["content-type"] = "application/json" - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - actual = json.loads(res.body)['floating_ip'] - - expected = { - "id": 1, - "instance_id": None, - "ip": "10.10.10.10", - "fixed_ip": "11.0.0.1"} - self.assertEqual(actual, expected) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) - def test_floating_ip_disassociate(self): - body = dict() - req = webob.Request.blank('/v1.1/os-floating-ips/1/disassociate') - req.method = 'PUT' + def test_remove_floating_ip_from_instance(self): + body = dict(removeFloatingIp=dict(address='11.0.0.1')) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" req.body = json.dumps(body) - req.headers['Content-Type'] = 'application/json' - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 200) - ip = json.loads(res.body)['floating_ip'] - expected = { - "id": 1, - "instance_id": None, - "ip": '10.10.10.10', - "fixed_ip": '11.0.0.1'} + req.headers["content-type"] = "application/json" - self.assertEqual(ip, expected) + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 202) -- cgit From 468893c667c7ce6cddb9d62906dfcb807fcd6da1 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Fri, 19 Aug 2011 13:25:33 -0700 Subject: a few tweaks - remove unused member functions, add comment --- nova/api/openstack/contrib/floating_ips.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index b305ebdcb..3b400807a 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -80,6 +80,7 @@ class FloatingIPController(object): context = req.environ['nova.context'] try: + # FIXME - why does self.network_api.list_floating_ips raise this? floating_ips = self.network_api.list_floating_ips(context) except exception.FloatingIpNotFoundForProject: floating_ips = [] @@ -174,9 +175,7 @@ class Floating_ips(extensions.ExtensionDescriptor): res = extensions.ResourceExtension('os-floating-ips', FloatingIPController(), - member_actions={ - 'associate': 'PUT', - 'disassociate': 'PUT'}) + member_actions={}) resources.append(res) return resources -- cgit From ce4ac4be2b813a8f025a9f2891fbc1ed4101c496 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Fri, 19 Aug 2011 13:31:49 -0700 Subject: tweak to comment --- nova/api/openstack/contrib/floating_ips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 3b400807a..0f27f2f27 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -80,7 +80,7 @@ class FloatingIPController(object): context = req.environ['nova.context'] try: - # FIXME - why does self.network_api.list_floating_ips raise this? + # FIXME(ja) - why does self.network_api.list_floating_ips raise? floating_ips = self.network_api.list_floating_ips(context) except exception.FloatingIpNotFoundForProject: floating_ips = [] -- cgit From 5f6cd490425d8d91870de1b4a492a6cb34502bcb Mon Sep 17 00:00:00 2001 From: Alex Meade Date: Fri, 19 Aug 2011 16:36:20 -0400 Subject: Updated accessIPv4 and accessIPv6 to always be in a servers response --- nova/api/openstack/views/servers.py | 6 ++---- nova/tests/api/openstack/test_servers.py | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/nova/api/openstack/views/servers.py b/nova/api/openstack/views/servers.py index 8b3a1e221..d2c1b0ba1 100644 --- a/nova/api/openstack/views/servers.py +++ b/nova/api/openstack/views/servers.py @@ -144,10 +144,8 @@ class ViewBuilderV11(ViewBuilder): elif response['server']['status'] == "BUILD": response['server']['progress'] = 0 - if inst.get('access_ip_v4'): - response['server']['accessIPv4'] = inst['access_ip_v4'] - if inst.get('access_ip_v6'): - response['server']['accessIPv6'] = inst['access_ip_v6'] + response['server']['accessIPv4'] = inst.get('access_ip_v4') or "" + response['server']['accessIPv6'] = inst.get('access_ip_v6') or "" return response diff --git a/nova/tests/api/openstack/test_servers.py b/nova/tests/api/openstack/test_servers.py index 6cf2d2d6a..d3eb4c517 100644 --- a/nova/tests/api/openstack/test_servers.py +++ b/nova/tests/api/openstack/test_servers.py @@ -341,6 +341,8 @@ class ServersTest(test.TestCase): "progress": 0, "name": "server1", "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "10", @@ -438,6 +440,8 @@ class ServersTest(test.TestCase): created="%(expected_created)s" hostId="" status="BUILD" + accessIPv4="" + accessIPv6="" progress="0"> @@ -503,6 +507,8 @@ class ServersTest(test.TestCase): "progress": 100, "name": "server1", "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "10", @@ -594,6 +600,8 @@ class ServersTest(test.TestCase): "progress": 100, "name": "server1", "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "10", @@ -1650,6 +1658,7 @@ class ServersTest(test.TestCase): self.assertEqual(expected_flavor, server['flavor']) self.assertEqual(expected_image, server['image']) self.assertEqual(access_ipv4, server['accessIPv4']) + self.assertEqual(access_ipv6, server['accessIPv6']) def test_create_instance_v1_1(self): self._setup_for_create_instance() @@ -1708,6 +1717,8 @@ class ServersTest(test.TestCase): self.assertEqual('server_test', server['name']) self.assertEqual(expected_flavor, server['flavor']) self.assertEqual(expected_image, server['image']) + self.assertEqual('1.2.3.4', server['accessIPv4']) + self.assertEqual('fead::1234', server['accessIPv6']) def test_create_instance_v1_1_invalid_flavor_href(self): self._setup_for_create_instance() @@ -3271,6 +3282,8 @@ class ServersViewBuilderV11Test(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "5", @@ -3322,6 +3335,8 @@ class ServersViewBuilderV11Test(test.TestCase): "progress": 100, "name": "test_server", "status": "ACTIVE", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "5", @@ -3396,6 +3411,7 @@ class ServersViewBuilderV11Test(test.TestCase): "addresses": {}, "metadata": {}, "accessIPv4": "1.2.3.4", + "accessIPv6": "", "links": [ { "rel": "self", @@ -3448,6 +3464,7 @@ class ServersViewBuilderV11Test(test.TestCase): }, "addresses": {}, "metadata": {}, + "accessIPv4": "", "accessIPv6": "fead::1234", "links": [ { @@ -3483,6 +3500,8 @@ class ServersViewBuilderV11Test(test.TestCase): "progress": 0, "name": "test_server", "status": "BUILD", + "accessIPv4": "", + "accessIPv6": "", "hostId": '', "image": { "id": "5", -- cgit From 9b65cdf0b2d5cc7ed7adcaca0dde4d6e2a10bf95 Mon Sep 17 00:00:00 2001 From: Anthony Young Date: Fri, 19 Aug 2011 14:16:57 -0700 Subject: better handle malformed input, and add associated tests --- nova/api/openstack/contrib/floating_ips.py | 6 ++++ .../api/openstack/contrib/test_floating_ips.py | 40 ++++++++++++++++++++++ 2 files changed, 46 insertions(+) diff --git a/nova/api/openstack/contrib/floating_ips.py b/nova/api/openstack/contrib/floating_ips.py index 0f27f2f27..40086f778 100644 --- a/nova/api/openstack/contrib/floating_ips.py +++ b/nova/api/openstack/contrib/floating_ips.py @@ -131,6 +131,9 @@ class Floating_ips(extensions.ExtensionDescriptor): try: address = input_dict['addFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) except KeyError: msg = _("Address not specified") raise webob.exc.HTTPBadRequest(explanation=msg) @@ -145,6 +148,9 @@ class Floating_ips(extensions.ExtensionDescriptor): try: address = input_dict['removeFloatingIp']['address'] + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) except KeyError: msg = _("Address not specified") raise webob.exc.HTTPBadRequest(explanation=msg) diff --git a/nova/tests/api/openstack/contrib/test_floating_ips.py b/nova/tests/api/openstack/contrib/test_floating_ips.py index 09234072a..d2ca9c365 100644 --- a/nova/tests/api/openstack/contrib/test_floating_ips.py +++ b/nova/tests/api/openstack/contrib/test_floating_ips.py @@ -183,3 +183,43 @@ class FloatingIpTest(test.TestCase): resp = req.get_response(fakes.wsgi_app()) self.assertEqual(resp.status_int, 202) + + def test_bad_address_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp=dict(badparam='11.0.0.1')) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_missing_dict_param_in_remove_floating_ip(self): + body = dict(removeFloatingIp='11.0.0.1') + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_bad_address_param_in_add_floating_ip(self): + body = dict(addFloatingIp=dict(badparam='11.0.0.1')) + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) + + def test_missing_dict_param_in_add_floating_ip(self): + body = dict(addFloatingIp='11.0.0.1') + req = webob.Request.blank('/v1.1/servers/test_inst/action') + req.method = "POST" + req.body = json.dumps(body) + req.headers["content-type"] = "application/json" + + resp = req.get_response(fakes.wsgi_app()) + self.assertEqual(resp.status_int, 400) -- cgit From bb989133196744779527e36cba22a76bd44e533b Mon Sep 17 00:00:00 2001 From: Tushar Patil Date: Sat, 20 Aug 2011 15:38:13 -0700 Subject: add/remove security groups to/from the servers as server actions --- nova/api/openstack/contrib/security_groups.py | 248 ++++++----------- nova/compute/api.py | 72 +++++ nova/exception.py | 10 + .../api/openstack/contrib/test_security_groups.py | 294 ++++++++++----------- 4 files changed, 296 insertions(+), 328 deletions(-) diff --git a/nova/api/openstack/contrib/security_groups.py b/nova/api/openstack/contrib/security_groups.py index a104a42e4..1fd64f3b8 100644 --- a/nova/api/openstack/contrib/security_groups.py +++ b/nova/api/openstack/contrib/security_groups.py @@ -168,135 +168,6 @@ class SecurityGroupController(object): "than 255 characters.") % typ raise exc.HTTPBadRequest(explanation=msg) - def associate(self, req, id, body): - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'security_group_associate' in body: - raise exc.HTTPUnprocessableEntity() - - security_group = self._get_security_group(context, id) - - servers = body['security_group_associate'].get('servers') - - if not servers: - msg = _("No servers found") - return exc.HTTPBadRequest(explanation=msg) - - hosts = set() - for server in servers: - if server['id']: - try: - # check if the server exists - inst = db.instance_get(context, server['id']) - #check if the security group is assigned to the server - if self._is_security_group_associated_to_server( - security_group, inst['id']): - msg = _("Security group %s is already associated with" - " the instance %s") % (security_group['id'], - server['id']) - raise exc.HTTPBadRequest(explanation=msg) - - #check if the instance is in running state - if inst['state'] != power_state.RUNNING: - msg = _("Server %s is not in the running state")\ - % server['id'] - raise exc.HTTPBadRequest(explanation=msg) - - hosts.add(inst['host']) - except exception.InstanceNotFound as exp: - return exc.HTTPNotFound(explanation=unicode(exp)) - - # Associate security group with the server in the db - for server in servers: - if server['id']: - db.instance_add_security_group(context.elevated(), - server['id'], - security_group['id']) - - for host in hosts: - rpc.cast(context, - db.queue_get_for(context, FLAGS.compute_topic, host), - {"method": "refresh_security_group_rules", - "args": {"security_group_id": security_group['id']}}) - - return exc.HTTPAccepted() - - def _is_security_group_associated_to_server(self, security_group, - instance_id): - if not security_group: - return False - - instances = security_group.get('instances') - if not instances: - return False - - inst_id = None - for inst_id in (instance['id'] for instance in instances \ - if instance_id == instance['id']): - return True - - return False - - def disassociate(self, req, id, body): - context = req.environ['nova.context'] - - if not body: - raise exc.HTTPUnprocessableEntity() - - if not 'security_group_disassociate' in body: - raise exc.HTTPUnprocessableEntity() - - security_group = self._get_security_group(context, id) - - servers = body['security_group_disassociate'].get('servers') - - if not servers: - msg = _("No servers found") - return exc.HTTPBadRequest(explanation=msg) - - hosts = set() - for server in servers: - if server['id']: - try: - # check if the instance exists - inst = db.instance_get(context, server['id']) - # Check if the security group is not associated - # with the instance - if not self._is_security_group_associated_to_server( - security_group, inst['id']): - msg = _("Security group %s is not associated with the" - "instance %s") % (security_group['id'], - server['id']) - raise exc.HTTPBadRequest(explanation=msg) - - #check if the instance is in running state - if inst['state'] != power_state.RUNNING: - msg = _("Server %s is not in the running state")\ - % server['id'] - raise exp.HTTPBadRequest(explanation=msg) - - hosts.add(inst['host']) - except exception.InstanceNotFound as exp: - return exc.HTTPNotFound(explanation=unicode(exp)) - - # Disassociate security group from the server - for server in servers: - if server['id']: - db.instance_remove_security_group(context.elevated(), - server['id'], - security_group['id']) - - for host in hosts: - rpc.cast(context, - db.queue_get_for(context, FLAGS.compute_topic, host), - {"method": "refresh_security_group_rules", - "args": {"security_group_id": security_group['id']}}) - - return exc.HTTPAccepted() - class SecurityGroupRulesController(SecurityGroupController): @@ -461,6 +332,11 @@ class SecurityGroupRulesController(SecurityGroupController): class Security_groups(extensions.ExtensionDescriptor): + + def __init__(self): + self.compute_api = compute.API() + super(Security_groups, self).__init__() + def get_name(self): return "SecurityGroups" @@ -476,6 +352,82 @@ class Security_groups(extensions.ExtensionDescriptor): def get_updated(self): return "2011-07-21T00:00:00+00:00" + def _addSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['addSecurityGroup'] + group_name = body['name'] + instance_id = int(instance_id) + except ValueError: + msg = _("Server id should be integer") + raise exc.HTTPBadRequest(explanation=msg) + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + self.compute_api.add_security_group(context, instance_id, + group_name) + except exception.SecurityGroupNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + return exc.HTTPBadRequest(explanation=unicode(exp)) + + return exc.HTTPAccepted() + + def _removeSecurityGroup(self, input_dict, req, instance_id): + context = req.environ['nova.context'] + + try: + body = input_dict['removeSecurityGroup'] + group_name = body['name'] + instance_id = int(instance_id) + except ValueError: + msg = _("Server id should be integer") + raise exc.HTTPBadRequest(explanation=msg) + except TypeError: + msg = _("Missing parameter dict") + raise webob.exc.HTTPBadRequest(explanation=msg) + except KeyError: + msg = _("Security group not specified") + raise webob.exc.HTTPBadRequest(explanation=msg) + + if not group_name or group_name.strip() == '': + msg = _("Security group name cannot be empty") + raise webob.exc.HTTPBadRequest(explanation=msg) + + try: + self.compute_api.remove_security_group(context, instance_id, + group_name) + except exception.SecurityGroupNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.InstanceNotFound as exp: + return exc.HTTPNotFound(explanation=unicode(exp)) + except exception.Invalid as exp: + return exc.HTTPBadRequest(explanation=unicode(exp)) + + return exc.HTTPAccepted() + + def get_actions(self): + """Return the actions the extensions adds""" + actions = [ + extensions.ActionExtension("servers", "addSecurityGroup", + self._addSecurityGroup), + extensions.ActionExtension("servers", "removeSecurityGroup", + self._removeSecurityGroup) + ] + return actions + def get_resources(self): resources = [] @@ -493,10 +445,6 @@ class Security_groups(extensions.ExtensionDescriptor): res = extensions.ResourceExtension('os-security-groups', controller=SecurityGroupController(), - member_actions={ - 'associate': 'POST', - 'disassociate': 'POST' - }, deserializer=deserializer, serializer=serializer) @@ -534,40 +482,6 @@ class SecurityGroupXMLDeserializer(wsgi.MetadataXMLDeserializer): security_group['description'] = self.extract_text(desc_node) return {'body': {'security_group': security_group}} - def _get_servers(self, node): - servers_dict = {'servers': []} - if node is not None: - servers_node = self.find_first_child_named(node, - 'servers') - if servers_node is not None: - for server_node in self.find_children_named(servers_node, - "server"): - servers_dict['servers'].append( - {"id": self.extract_text(server_node)}) - return servers_dict - - def associate(self, string): - """Deserialize an xml-formatted security group associate request""" - dom = minidom.parseString(string) - node = self.find_first_child_named(dom, - 'security_group_associate') - result = {'body': {}} - if node: - result['body']['security_group_associate'] = \ - self._get_servers(node) - return result - - def disassociate(self, string): - """Deserialize an xml-formatted security group disassociate request""" - dom = minidom.parseString(string) - node = self.find_first_child_named(dom, - 'security_group_disassociate') - result = {'body': {}} - if node: - result['body']['security_group_disassociate'] = \ - self._get_servers(node) - return result - class SecurityGroupRulesXMLDeserializer(wsgi.MetadataXMLDeserializer): """ diff --git a/nova/compute/api.py b/nova/compute/api.py index efc9da79b..0c6beacaa 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -613,6 +613,78 @@ class API(base.Base): self.db.queue_get_for(context, FLAGS.compute_topic, host), {'method': 'refresh_provider_fw_rules', 'args': {}}) + def _is_security_group_associated_with_server(self, security_group, + instance_id): + """Check if the security group is already associated + with the instance. If Yes, return True. + """ + + if not security_group: + return False + + instances = security_group.get('instances') + if not instances: + return False + + inst_id = None + for inst_id in (instance['id'] for instance in instances \ + if instance_id == instance['id']): + return True + + return False + + def add_security_group(self, context, instance_id, security_group_name): + """Add security group to the instance""" + security_group = db.security_group_get_by_name(context, + context.project_id, + security_group_name) + # check if the server exists + inst = db.instance_get(context, instance_id) + #check if the security group is associated with the server + if self._is_security_group_associated_with_server(security_group, + instance_id): + raise exception.SecurityGroupExistsForInstance( + security_group_id=security_group['id'], + instance_id=instance_id) + + #check if the instance is in running state + if inst['state'] != power_state.RUNNING: + raise exception.InstanceNotRunning(instance_id=instance_id) + + db.instance_add_security_group(context.elevated(), + instance_id, + security_group['id']) + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, inst['host']), + {"method": "refresh_security_group_rules", + "args": {"security_group_id": security_group['id']}}) + + def remove_security_group(self, context, instance_id, security_group_name): + """Remove the security group associated with the instance""" + security_group = db.security_group_get_by_name(context, + context.project_id, + security_group_name) + # check if the server exists + inst = db.instance_get(context, instance_id) + #check if the security group is associated with the server + if not self._is_security_group_associated_with_server(security_group, + instance_id): + raise exception.SecurityGroupNotExistsForInstance( + security_group_id=security_group['id'], + instance_id=instance_id) + + #check if the instance is in running state + if inst['state'] != power_state.RUNNING: + raise exception.InstanceNotRunning(instance_id=instance_id) + + db.instance_remove_security_group(context.elevated(), + instance_id, + security_group['id']) + rpc.cast(context, + db.queue_get_for(context, FLAGS.compute_topic, inst['host']), + {"method": "refresh_security_group_rules", + "args": {"security_group_id": security_group['id']}}) + @scheduler_api.reroute_compute("update") def update(self, context, instance_id, **kwargs): """Updates the instance in the datastore. diff --git a/nova/exception.py b/nova/exception.py index b09d50797..e8cb7bcb5 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -541,6 +541,16 @@ class SecurityGroupNotFoundForRule(SecurityGroupNotFound): message = _("Security group with rule %(rule_id)s not found.") +class SecurityGroupExistsForInstance(Invalid): + message = _("Security group %(security_group_id)s is already associated" + " with the instance %(instance_id)s") + + +class SecurityGroupNotExistsForInstance(Invalid): + message = _("Security group %(security_group_id)s is not associated with" + " the instance %(instance_id)s") + + class MigrationNotFound(NotFound): message = _("Migration %(migration_id)s could not be found.") diff --git a/nova/tests/api/openstack/contrib/test_security_groups.py b/nova/tests/api/openstack/contrib/test_security_groups.py index 894b0c591..b44ebc9fb 100644 --- a/nova/tests/api/openstack/contrib/test_security_groups.py +++ b/nova/tests/api/openstack/contrib/test_security_groups.py @@ -63,17 +63,17 @@ def return_non_running_server(context, server_id): 'host': "localhost"} -def return_security_group(context, group_id): - return {'id': group_id, "instances": [ +def return_security_group(context, project_id, group_name): + return {'id': 1, 'name': group_name, "instances": [ {'id': 1}]} -def return_security_group_without_instances(context, group_id): - return {'id': group_id} +def return_security_group_without_instances(context, project_id, group_name): + return {'id': 1, 'name': group_name} def return_server_nonexistant(context, server_id): - raise exception.InstanceNotFound() + raise exception.InstanceNotFound(instance_id=server_id) class TestSecurityGroups(test.TestCase): @@ -350,117 +350,89 @@ class TestSecurityGroups(test.TestCase): response = self._delete_security_group(11111111) self.assertEquals(response.status_int, 404) - def test_associate_by_non_existing_security_group_id(self): - req = webob.Request.blank('/v1.1/os-security-groups/111111/associate') + def test_associate_by_non_existing_security_group_name(self): + body = dict(addSecurityGroup=dict(name='non-existing')) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - {"id": '2'} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 404) - def test_associate_by_invalid_security_group_id(self): - req = webob.Request.blank('/v1.1/os-security-groups/invalid/associate') + def test_associate_by_invalid_server_id(self): + body = dict(addSecurityGroup=dict(name='test')) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/servers/invalid/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - {"id": "2"} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) def test_associate_without_body(self): - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + req = webob.Request.blank('/v1.1/servers/1/action') + body = dict(addSecurityGroup=None) + self.stubs.Set(nova.db, 'instance_get', return_server) req.headers['Content-Type'] = 'application/json' req.method = 'POST' - req.body = json.dumps(None) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) - self.assertEquals(response.status_int, 422) + self.assertEquals(response.status_int, 400) - def test_associate_no_security_group_element(self): - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + def test_associate_no_security_group_name(self): + req = webob.Request.blank('/v1.1/servers/1/action') + body = dict(addSecurityGroup=dict()) + self.stubs.Set(nova.db, 'instance_get', return_server) req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate_invalid": { - "servers": [ - {"id": "2"} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) - self.assertEquals(response.status_int, 422) + self.assertEquals(response.status_int, 400) - def test_associate_no_instances(self): - #self.stubs.Set(nova.db.api, 'instance_get', return_server) - self.stubs.Set(nova.db, 'security_group_get', return_security_group) - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + def test_associate_security_group_name_with_whitespaces(self): + req = webob.Request.blank('/v1.1/servers/1/action') + body = dict(addSecurityGroup=dict(name=" ")) + self.stubs.Set(nova.db, 'instance_get', return_server) req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) def test_associate_non_existing_instance(self): self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) - self.stubs.Set(nova.db, 'security_group_get', return_security_group) - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + body = dict(addSecurityGroup=dict(name="test")) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/servers/10000/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - {'id': 2} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 404) def test_associate_non_running_instance(self): self.stubs.Set(nova.db, 'instance_get', return_non_running_server) - self.stubs.Set(nova.db, 'security_group_get', + self.stubs.Set(nova.db, 'security_group_get_by_name', return_security_group_without_instances) - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + body = dict(addSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - {'id': 1} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) def test_associate_already_associated_security_group_to_instance(self): self.stubs.Set(nova.db, 'instance_get', return_server) - self.stubs.Set(nova.db, 'security_group_get', return_security_group) - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + body = dict(addSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - {'id': 1} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) @@ -470,134 +442,120 @@ class TestSecurityGroups(test.TestCase): nova.db.instance_add_security_group(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) - self.stubs.Set(nova.db, 'security_group_get', + self.stubs.Set(nova.db, 'security_group_get_by_name', return_security_group_without_instances) self.mox.ReplayAll() - req = webob.Request.blank('/v1.1/os-security-groups/1/associate') + body = dict(addSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_associate": { - "servers": [ - {'id': 1} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_associate_xml(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_add_security_group') + nova.db.instance_add_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + self.mox.ReplayAll() + + req = webob.Request.blank('/v1.1/servers/1/action') + req.headers['Content-Type'] = 'application/xml' + req.method = 'POST' + req.body = """ + test + """ response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 202) - def test_disassociate_by_non_existing_security_group_id(self): - req = webob.Request.blank('/v1.1/os-security-groups/1111/disassociate') + def test_disassociate_by_non_existing_security_group_name(self): + body = dict(removeSecurityGroup=dict(name='non-existing')) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - {"id": "2"} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 404) - def test_disassociate_by_invalid_security_group_id(self): - req = webob.Request.blank('/v1.1/os-security-groups/id/disassociate') + def test_disassociate_by_invalid_server_id(self): + body = dict(removeSecurityGroup=dict(name='test')) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/servers/invalid/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - {"id": "2"} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) def test_disassociate_without_body(self): - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + req = webob.Request.blank('/v1.1/servers/1/action') + body = dict(removeSecurityGroup=None) + self.stubs.Set(nova.db, 'instance_get', return_server) req.headers['Content-Type'] = 'application/json' req.method = 'POST' - req.body = json.dumps(None) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) - self.assertEquals(response.status_int, 422) + self.assertEquals(response.status_int, 400) - def test_disassociate_no_security_group_element(self): - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + def test_disassociate_no_security_group_name(self): + req = webob.Request.blank('/v1.1/servers/1/action') + body = dict(removeSecurityGroup=dict()) + self.stubs.Set(nova.db, 'instance_get', return_server) req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate_invalid": { - "servers": [ - {"id": "2"} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) - self.assertEquals(response.status_int, 422) + self.assertEquals(response.status_int, 400) - def test_disassociate_no_instances(self): - #self.stubs.Set(nova.db.api, 'instance_get', return_server) - self.stubs.Set(nova.db, 'security_group_get', return_security_group) - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + def test_disassociate_security_group_name_with_whitespaces(self): + req = webob.Request.blank('/v1.1/servers/1/action') + body = dict(removeSecurityGroup=dict(name=" ")) + self.stubs.Set(nova.db, 'instance_get', return_server) req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) def test_disassociate_non_existing_instance(self): self.stubs.Set(nova.db, 'instance_get', return_server_nonexistant) - self.stubs.Set(nova.db, 'security_group_get', return_security_group) - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + body = dict(removeSecurityGroup=dict(name="test")) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + req = webob.Request.blank('/v1.1/servers/10000/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - {'id': 2} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 404) def test_disassociate_non_running_instance(self): self.stubs.Set(nova.db, 'instance_get', return_non_running_server) - self.stubs.Set(nova.db, 'security_group_get', - return_security_group_without_instances) - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + body = dict(removeSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - {'id': 1} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) - def test_disassociate_not_associated_security_group_to_instance(self): + def test_disassociate_already_associated_security_group_to_instance(self): self.stubs.Set(nova.db, 'instance_get', return_server) - self.stubs.Set(nova.db, 'security_group_get', return_security_group) - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group_without_instances) + body = dict(removeSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - {'id': 2} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 400) @@ -607,20 +565,34 @@ class TestSecurityGroups(test.TestCase): nova.db.instance_remove_security_group(mox.IgnoreArg(), mox.IgnoreArg(), mox.IgnoreArg()) - self.stubs.Set(nova.db, 'security_group_get', + self.stubs.Set(nova.db, 'security_group_get_by_name', return_security_group) self.mox.ReplayAll() - req = webob.Request.blank('/v1.1/os-security-groups/1/disassociate') + body = dict(removeSecurityGroup=dict(name="test")) + req = webob.Request.blank('/v1.1/servers/1/action') req.headers['Content-Type'] = 'application/json' req.method = 'POST' - body_dict = {"security_group_disassociate": { - "servers": [ - {'id': 1} - ] - } - } - req.body = json.dumps(body_dict) + req.body = json.dumps(body) + response = req.get_response(fakes.wsgi_app()) + self.assertEquals(response.status_int, 202) + + def test_disassociate_xml(self): + self.stubs.Set(nova.db, 'instance_get', return_server) + self.mox.StubOutWithMock(nova.db, 'instance_remove_security_group') + nova.db.instance_remove_security_group(mox.IgnoreArg(), + mox.IgnoreArg(), + mox.IgnoreArg()) + self.stubs.Set(nova.db, 'security_group_get_by_name', + return_security_group) + self.mox.ReplayAll() + + req = webob.Request.blank('/v1.1/servers/1/action') + req.headers['Content-Type'] = 'application/xml' + req.method = 'POST' + req.body = """ + test + """ response = req.get_response(fakes.wsgi_app()) self.assertEquals(response.status_int, 202) -- cgit