From 9af40c167879096a8f0f209bde4e6c5cc9295b86 Mon Sep 17 00:00:00 2001 From: Alessio Ababilov Date: Mon, 16 Jul 2012 18:27:58 +0300 Subject: Implement network association in OS API Networks are associated with projects automatically during launch of an instance. The network is chosen rather randomly. This commit adds support for association of given network and project. DocImpact - when this lands, need to update openstack-manuals repo: doc/src/docbkx/openstack-api-site/src/wadls/compute-api/src/ext/os-networks.wadl Implements: blueprint os-api-network-associate Change-Id: Iafaf4a5ae3d3e16a6c649f1d7850fceba732efab --- nova/api/openstack/compute/contrib/networks.py | 35 ++++++++++++-- nova/db/api.py | 4 +- nova/db/sqlalchemy/api.py | 11 +++-- nova/network/api.py | 5 +- nova/network/manager.py | 8 +++- nova/network/quantumv2/api.py | 2 +- .../api/openstack/compute/contrib/test_networks.py | 53 ++++++++++++++++------ 7 files changed, 91 insertions(+), 27 deletions(-) diff --git a/nova/api/openstack/compute/contrib/networks.py b/nova/api/openstack/compute/contrib/networks.py index ece331fbb..fb1d3d77e 100644 --- a/nova/api/openstack/compute/contrib/networks.py +++ b/nova/api/openstack/compute/contrib/networks.py @@ -17,6 +17,7 @@ # under the License. +import webob from webob import exc from nova.api.openstack import extensions @@ -113,6 +114,31 @@ class NetworkController(object): def create(self, req, id, body=None): raise exc.HTTPNotImplemented() + def add(self, req, body): + context = req.environ['nova.context'] + authorize(context) + if not body: + raise exc.HTTPUnprocessableEntity() + + network_id = body.get('id', None) + project_id = context.project_id + LOG.debug(_("Associating network %(network)s" + " with project %(project)s") % + {"network": network_id or "", + "project": project_id}) + try: + self.network_api.add_network_to_project( + context, project_id, network_id) + except Exception as ex: + msg = (_("Cannot associate network %(network)s" + " with project %(project)s: %(message)s") % + {"network": network_id or "", + "project": project_id, + "message": getattr(ex, "value", str(ex))}) + raise exc.HTTPBadRequest(explanation=msg) + + return webob.Response(status_int=202) + class Networks(extensions.ExtensionDescriptor): """Admin-only Network Management Extension""" @@ -124,7 +150,10 @@ class Networks(extensions.ExtensionDescriptor): def get_resources(self): member_actions = {'action': 'POST'} - res = extensions.ResourceExtension('os-networks', - NetworkController(), - member_actions=member_actions) + collection_actions = {'add': 'POST'} + res = extensions.ResourceExtension( + 'os-networks', + NetworkController(), + member_actions=member_actions, + collection_actions=collection_actions) return [res] diff --git a/nova/db/api.py b/nova/db/api.py index 48b9fba1e..aa3548c7e 100644 --- a/nova/db/api.py +++ b/nova/db/api.py @@ -779,9 +779,9 @@ def key_pair_count_by_user(context, user_id): #################### -def network_associate(context, project_id, force=False): +def network_associate(context, project_id, network_id=None, force=False): """Associate a free network to a project.""" - return IMPL.network_associate(context, project_id, force) + return IMPL.network_associate(context, project_id, network_id, force) def network_count(context): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 2b5b9f993..a9d324da5 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -1996,7 +1996,7 @@ def key_pair_count_by_user(context, user_id): @require_admin_context -def network_associate(context, project_id, force=False): +def network_associate(context, project_id, network_id=None, force=False): """Associate a project with a network. called by project_get_networks under certain conditions @@ -2014,10 +2014,13 @@ def network_associate(context, project_id, force=False): session = get_session() with session.begin(): - def network_query(project_filter): + def network_query(project_filter, id=None): + filter_kwargs = {'project_id': project_filter} + if id is not None: + filter_kwargs['id'] = id return model_query(context, models.Network, session=session, read_deleted="no").\ - filter_by(project_id=project_filter).\ + filter_by(**filter_kwargs).\ with_lockmode('update').\ first() @@ -2030,7 +2033,7 @@ def network_associate(context, project_id, force=False): # with a new network # get new network - network_ref = network_query(None) + network_ref = network_query(None, network_id) if not network_ref: raise db.NoMoreNetworks() diff --git a/nova/network/api.py b/nova/network/api.py index ae230455f..daa41a2e7 100644 --- a/nova/network/api.py +++ b/nova/network/api.py @@ -278,11 +278,12 @@ class API(base.Base): {'method': 'remove_fixed_ip_from_instance', 'args': args}) - def add_network_to_project(self, context, project_id): + def add_network_to_project(self, context, project_id, network_uuid=None): """Force adds another network to a project.""" rpc.call(context, FLAGS.network_topic, {'method': 'add_network_to_project', - 'args': {'project_id': project_id}}) + 'args': {'project_id': project_id, + 'network_uuid': network_uuid}}) @refresh_cache def get_instance_nw_info(self, context, instance): diff --git a/nova/network/manager.py b/nova/network/manager.py index a79700dcd..470a186ee 100644 --- a/nova/network/manager.py +++ b/nova/network/manager.py @@ -1907,9 +1907,13 @@ class VlanManager(RPCAllocateFixedIP, FloatingIP, NetworkManager): return address @wrap_check_policy - def add_network_to_project(self, context, project_id): + def add_network_to_project(self, context, project_id, network_uuid=None): """Force adds another network to a project.""" - self.db.network_associate(context, project_id, force=True) + if network_uuid is not None: + network_id = self.get_network(context, network_uuid)['id'] + else: + network_id = None + self.db.network_associate(context, project_id, network_id, force=True) def _get_networks_for_instance(self, context, instance_id, project_id, requested_networks=None): diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py index 50042278b..ca5b44e25 100644 --- a/nova/network/quantumv2/api.py +++ b/nova/network/quantumv2/api.py @@ -244,7 +244,7 @@ class API(base.Base): it is associated with.""" raise NotImplementedError() - def add_network_to_project(self, context, project_id): + def add_network_to_project(self, context, project_id, network_uuid=None): """Force add a network to the project.""" raise NotImplementedError() diff --git a/nova/tests/api/openstack/compute/contrib/test_networks.py b/nova/tests/api/openstack/compute/contrib/test_networks.py index 91858f156..efbb7ceba 100644 --- a/nova/tests/api/openstack/compute/contrib/test_networks.py +++ b/nova/tests/api/openstack/compute/contrib/test_networks.py @@ -46,6 +46,7 @@ FAKE_NETWORKS = [ 'bridge': 'br101', 'vpn_public_port': 1001, 'dhcp_start': '10.0.0.11', 'bridge_interface': 'eth0', 'updated_at': None, 'id': 2, 'cidr_v6': None, + 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf000', 'deleted_at': None, 'gateway': '10.0.0.9', 'label': 'mynet_1', 'project_id': None, 'vpn_private_address': '10.0.0.10', 'deleted': False, @@ -70,7 +71,7 @@ FAKE_USER_NETWORKS = [ 'id': 2, 'cidr': '10.0.0.10/29', 'netmask': '255.255.255.248', 'gateway': '10.0.0.9', 'broadcast': '10.0.0.15', 'dns1': None, 'dns2': None, 'cidr_v6': None, 'gateway_v6': None, 'label': 'mynet_1', - 'netmask_v6': None, + 'netmask_v6': None, 'uuid': '20c8acc0-f747-4d71-a389-46d078ebf000', }, ] @@ -87,13 +88,26 @@ class FakeNetworkAPI(object): return True raise exception.NetworkNotFoundForUUID() - #NOTE(bcwaldon): this does nothing other than check for existance - def disassociate(self, context, network_id): - for i, network in enumerate(self.networks): - if network.get('uuid') == network_id: + def disassociate(self, context, network_uuid): + for network in self.networks: + if network.get('uuid') == network_uuid: + network['project_id'] = None return True raise exception.NetworkNotFound() + def add_network_to_project(self, context, + project_id, network_uuid=None): + if network_uuid: + for network in self.networks: + if network.get('project_id', None) is None: + network['project_id'] = project_id + return + return + for network in self.networks: + if network.get('uuid') == network_uuid: + network['project_id'] = project_id + return + def get_all(self, context): return self.networks @@ -113,13 +127,18 @@ class NetworksTest(test.TestCase): fakes.stub_out_networking(self.stubs) fakes.stub_out_rate_limiting(self.stubs) + @staticmethod + def network_uuid_to_id(network): + network['id'] = network['uuid'] + del network['uuid'] + def test_network_list_all_as_user(self): self.maxDiff = None req = fakes.HTTPRequest.blank('/v2/1234/os-networks') res_dict = self.controller.index(req) expected = copy.deepcopy(FAKE_USER_NETWORKS) - expected[0]['id'] = expected[0]['uuid'] - del expected[0]['uuid'] + for network in expected: + self.network_uuid_to_id(network) self.assertEquals(res_dict, {'networks': expected}) def test_network_list_all_as_admin(self): @@ -127,8 +146,8 @@ class NetworksTest(test.TestCase): req.environ["nova.context"].is_admin = True res_dict = self.controller.index(req) expected = copy.deepcopy(FAKE_NETWORKS) - expected[0]['id'] = expected[0]['uuid'] - del expected[0]['uuid'] + for network in expected: + self.network_uuid_to_id(network) self.assertEquals(res_dict, {'networks': expected}) def test_network_disassociate(self): @@ -148,8 +167,7 @@ class NetworksTest(test.TestCase): req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s' % uuid) res_dict = self.controller.show(req, uuid) expected = {'network': copy.deepcopy(FAKE_USER_NETWORKS[0])} - expected['network']['id'] = expected['network']['uuid'] - del expected['network']['uuid'] + self.network_uuid_to_id(expected['network']) self.assertEqual(res_dict, expected) def test_network_get_as_admin(self): @@ -158,8 +176,7 @@ class NetworksTest(test.TestCase): req.environ["nova.context"].is_admin = True res_dict = self.controller.show(req, uuid) expected = {'network': copy.deepcopy(FAKE_NETWORKS[0])} - expected['network']['id'] = expected['network']['uuid'] - del expected['network']['uuid'] + self.network_uuid_to_id(expected['network']) self.assertEqual(res_dict, expected) def test_network_get_not_found(self): @@ -177,3 +194,13 @@ class NetworksTest(test.TestCase): req = fakes.HTTPRequest.blank('/v2/1234/os-networks/100') self.assertRaises(webob.exc.HTTPNotFound, self.controller.delete, req, 100) + + def test_network_add(self): + uuid = FAKE_NETWORKS[1]['uuid'] + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/add') + res = self.controller.add(req, {'id': uuid}) + self.assertEqual(res.status_int, 202) + req = fakes.HTTPRequest.blank('/v2/1234/os-networks/%s' % uuid) + req.environ["nova.context"].is_admin = True + res_dict = self.controller.show(req, uuid) + self.assertEqual(res_dict['network']['project_id'], 'fake') -- cgit