diff options
-rw-r--r-- | nova/api/openstack/compute/contrib/floating_ips.py | 29 | ||||
-rw-r--r-- | nova/exception.py | 9 | ||||
-rw-r--r-- | nova/network/quantumv2/api.py | 183 | ||||
-rw-r--r-- | nova/tests/network/test_quantumv2.py | 283 | ||||
-rw-r--r-- | tools/pip-requires | 2 |
5 files changed, 482 insertions, 24 deletions
diff --git a/nova/api/openstack/compute/contrib/floating_ips.py b/nova/api/openstack/compute/contrib/floating_ips.py index 1c17591a4..b835ca61d 100644 --- a/nova/api/openstack/compute/contrib/floating_ips.py +++ b/nova/api/openstack/compute/contrib/floating_ips.py @@ -118,18 +118,23 @@ class FloatingIPController(object): return self.compute_api.get(context, instance_id) def _set_metadata(self, context, floating_ip): - fixed_ip_id = floating_ip['fixed_ip_id'] - floating_ip['fixed_ip'] = self._get_fixed_ip(context, - fixed_ip_id) - instance_uuid = None - if floating_ip['fixed_ip']: - instance_uuid = floating_ip['fixed_ip']['instance_uuid'] - - if instance_uuid: - floating_ip['instance'] = self._get_instance(context, - instance_uuid) - else: - floating_ip['instance'] = None + # When Quantum v2 API is used, 'fixed_ip' and 'instance' are + # already set. In this case we don't need to update the fields. + + if 'fixed_ip' not in floating_ip: + fixed_ip_id = floating_ip['fixed_ip_id'] + floating_ip['fixed_ip'] = self._get_fixed_ip(context, + fixed_ip_id) + if 'instance' not in floating_ip: + instance_uuid = None + if floating_ip['fixed_ip']: + instance_uuid = floating_ip['fixed_ip']['instance_uuid'] + + if instance_uuid: + floating_ip['instance'] = self._get_instance(context, + instance_uuid) + else: + floating_ip['instance'] = None @wsgi.serializers(xml=FloatingIPTemplate) def show(self, req, id): diff --git a/nova/exception.py b/nova/exception.py index 0b969e625..4261ad3ab 100644 --- a/nova/exception.py +++ b/nova/exception.py @@ -648,6 +648,15 @@ class FloatingIpNotFoundForHost(FloatingIpNotFound): message = _("Floating ip not found for host %(host)s.") +class FloatingIpMultipleFoundForAddress(NovaException): + message = _("Multiple floating ips are found for address %(address)s.") + + +class FloatingIpPoolNotFound(NotFound): + message = _("Floating ip pool not found.") + safe = True + + class NoMoreFloatingIps(FloatingIpNotFound): message = _("Zero floating ips available.") safe = True diff --git a/nova/network/quantumv2/api.py b/nova/network/quantumv2/api.py index 6c47acb1d..049b005d1 100644 --- a/nova/network/quantumv2/api.py +++ b/nova/network/quantumv2/api.py @@ -25,6 +25,7 @@ from nova.network import quantumv2 from nova.openstack.common import cfg from nova.openstack.common import excutils from nova.openstack.common import log as logging +from nova import utils quantum_opts = [ @@ -49,10 +50,14 @@ quantum_opts = [ 'quantum in admin context'), ] +flags.DECLARE('default_floating_pool', 'nova.network.manager') + FLAGS = flags.FLAGS FLAGS.register_opts(quantum_opts) LOG = logging.getLogger(__name__) +NET_EXTERNAL = 'router:external' + class API(base.Base): """API for interacting with the quantum 2.x API.""" @@ -261,12 +266,40 @@ class API(base.Base): self.security_group_api.trigger_handler('security_group_members', admin_context, group_ids) + def _get_port_id_by_fixed_address(self, client, + instance, address): + zone = 'compute:%s' % FLAGS.node_availability_zone + search_opts = {'device_id': instance['uuid'], + 'device_owner': zone} + data = client.list_ports(**search_opts) + ports = data['ports'] + port_id = None + for p in ports: + for ip in p['fixed_ips']: + if ip['ip_address'] == address: + port_id = p['id'] + break + if not port_id: + raise exception.FixedIpNotFoundForAddress(address=address) + return port_id + @refresh_cache def associate_floating_ip(self, context, instance, floating_address, fixed_address, affect_auto_assigned=False): """Associate a floating ip with a fixed ip.""" - raise NotImplementedError() + + # Note(amotoki): 'affect_auto_assigned' is not respected + # since it is not used anywhere in nova code and I could + # find why this parameter exists. + + client = quantumv2.get_client(context) + port_id = self._get_port_id_by_fixed_address(client, instance, + fixed_address) + fip = self._get_floating_ip_by_address(client, floating_address) + param = {'port_id': port_id, + 'fixed_ip_address': fixed_address} + client.update_floatingip(fip['id'], {'floatingip': param}) def get_all(self, context): raise NotImplementedError() @@ -293,23 +326,94 @@ class API(base.Base): raise exception.FixedIpAssociatedWithMultipleInstances( address=address) + def _setup_net_dict(self, client, network_id): + if not network_id: + return {} + pool = client.show_network(network_id)['network'] + return {pool['id']: pool} + + def _setup_port_dict(self, client, port_id): + if not port_id: + return {} + port = client.show_port(port_id)['port'] + return {port['id']: port} + + def _setup_pools_dict(self, client): + pools = self._get_floating_ip_pools(client) + return dict([(i['id'], i) for i in pools]) + + def _setup_ports_dict(self, client, project_id=None): + search_opts = {'tenant_id': project_id} if project_id else {} + ports = client.list_ports(**search_opts)['ports'] + return dict([(p['id'], p) for p in ports]) + def get_floating_ip(self, context, id): - raise NotImplementedError() + client = quantumv2.get_client(context) + fip = client.show_floatingip(id)['floatingip'] + pool_dict = self._setup_net_dict(client, + fip['floating_network_id']) + port_dict = self._setup_port_dict(client, fip['port_id']) + return self._format_floating_ip_model(fip, pool_dict, port_dict) + + def _get_floating_ip_pools(self, client, project_id=None): + search_opts = {NET_EXTERNAL: True} + if project_id: + search_opts.update({'tenant_id': project_id}) + data = client.list_networks(**search_opts) + return data['networks'] def get_floating_ip_pools(self, context): - return [] + client = quantumv2.get_client(context) + pools = self._get_floating_ip_pools(client) + return [{'name': n['name'] or n['id']} for n in pools] + + def _format_floating_ip_model(self, fip, pool_dict, port_dict): + pool = pool_dict[fip['floating_network_id']] + result = {'id': fip['id'], + 'address': fip['floating_ip_address'], + 'pool': pool['name'] or pool['id'], + 'project_id': fip['tenant_id'], + # In Quantum v2, an exact fixed_ip_id does not exist. + 'fixed_ip_id': fip['port_id'], + } + # In Quantum v2 API fixed_ip_address and instance uuid + # (= device_id) are known here, so pass it as a result. + result['fixed_ip'] = {'address': fip['fixed_ip_address']} + if fip['port_id']: + instance_uuid = port_dict[fip['port_id']]['device_id'] + result['instance'] = {'uuid': instance_uuid} + else: + result['instance'] = None + return result def get_floating_ip_by_address(self, context, address): - raise NotImplementedError() + client = quantumv2.get_client(context) + fip = self._get_floating_ip_by_address(client, address) + pool_dict = self._setup_net_dict(client, + fip['floating_network_id']) + port_dict = self._setup_port_dict(client, fip['port_id']) + return self._format_floating_ip_model(fip, pool_dict, port_dict) def get_floating_ips_by_project(self, context): - return [] + client = quantumv2.get_client(context) + project_id = context.project_id + fips = client.list_floatingips(tenant_id=project_id)['floatingips'] + pool_dict = self._setup_pools_dict(client) + port_dict = self._setup_ports_dict(client, project_id) + return [self._format_floating_ip_model(fip, pool_dict, port_dict) + for fip in fips] def get_floating_ips_by_fixed_address(self, context, fixed_address): return [] def get_instance_id_by_floating_address(self, context, address): - raise NotImplementedError() + """Returns the instance id a floating ip's fixed ip is allocated to""" + client = quantumv2.get_client(context) + fip = self._get_floating_ip_by_address(client, address) + if not fip['port_id']: + return None + port = client.show_port(fip['port_id'])['port'] + return port['device_id'] def get_vifs_by_instance(self, context, instance): raise NotImplementedError() @@ -317,21 +421,78 @@ class API(base.Base): def get_vif_by_mac_address(self, context, mac_address): raise NotImplementedError() + def _get_floating_ip_pool_id_by_name_or_id(self, client, name_or_id): + search_opts = {NET_EXTERNAL: True, 'fields': 'id'} + if utils.is_uuid_like(name_or_id): + search_opts.update({'id': name_or_id}) + else: + search_opts.update({'name': name_or_id}) + data = client.list_networks(**search_opts) + nets = data['networks'] + + if len(nets) == 1: + return nets[0]['id'] + elif len(nets) == 0: + raise exception.FloatingIpPoolNotFound() + else: + msg = (_("Multiple floating IP pools matches found for name '%s'") + % name_or_id) + raise exception.NovaException(message=msg) + def allocate_floating_ip(self, context, pool=None): """Add a floating ip to a project from a pool.""" - raise NotImplementedError() + client = quantumv2.get_client(context) + pool = pool or FLAGS.default_floating_pool + pool_id = self._get_floating_ip_pool_id_by_name_or_id(client, pool) + + # TODO(amotoki): handle exception during create_floatingip() + # At this timing it is ensured that a network for pool exists. + # quota error may be returned. + param = {'floatingip': {'floating_network_id': pool_id}} + fip = client.create_floatingip(param) + return fip['floatingip']['floating_ip_address'] + + def _get_floating_ip_by_address(self, client, address): + """Get floatingip from floating ip address""" + data = client.list_floatingips(floating_ip_address=address) + fips = data['floatingips'] + if len(fips) == 0: + raise exception.FloatingIpNotFoundForAddress(address=address) + elif len(fips) > 1: + raise exception.FloatingIpMultipleFoundForAddress(address=address) + return fips[0] def release_floating_ip(self, context, address, affect_auto_assigned=False): """Remove a floating ip with the given address from a project.""" - raise NotImplementedError() + + # Note(amotoki): We cannot handle a case where multiple pools + # have overlapping IP address range. In this case we cannot use + # 'address' as a unique key. + # This is a limitation of the current nova. + + # Note(amotoki): 'affect_auto_assigned' is not respected + # since it is not used anywhere in nova code and I could + # find why this parameter exists. + + client = quantumv2.get_client(context) + fip = self._get_floating_ip_by_address(client, address) + if fip['port_id']: + raise exception.FloatingIpAssociated(address=address) + client.delete_floatingip(fip['id']) @refresh_cache def disassociate_floating_ip(self, context, instance, address, affect_auto_assigned=False): - """Disassociate a floating ip from the fixed ip - it is associated with.""" - raise NotImplementedError() + """Disassociate a floating ip from the instance.""" + + # Note(amotoki): 'affect_auto_assigned' is not respected + # since it is not used anywhere in nova code and I could + # find why this parameter exists. + + client = quantumv2.get_client(context) + fip = self._get_floating_ip_by_address(client, address) + client.update_floatingip(fip['id'], {'floatingip': {'port_id': None}}) def add_network_to_project(self, context, project_id, network_uuid=None): """Force add a network to the project.""" diff --git a/nova/tests/network/test_quantumv2.py b/nova/tests/network/test_quantumv2.py index 888027b21..aacd19760 100644 --- a/nova/tests/network/test_quantumv2.py +++ b/nova/tests/network/test_quantumv2.py @@ -196,6 +196,30 @@ class TestQuantumv2(test.TestCase): 'gateway_ip': '10.0.2.1', 'dns_nameservers': ['8.8.2.1', '8.8.2.2']}) + self.fip_pool = {'id': '4fdbfd74-eaf8-4884-90d9-00bd6f10c2d3', + 'name': 'ext_net', + 'router:external': True, + 'tenant_id': 'admin_tenantid'} + self.fip_pool_nova = {'id': '435e20c3-d9f1-4f1b-bee5-4611a1dd07db', + 'name': 'nova', + 'router:external': True, + 'tenant_id': 'admin_tenantid'} + self.fip_unassociated = {'tenant_id': 'my_tenantid', + 'id': 'fip_id1', + 'floating_ip_address': '172.24.4.227', + 'floating_network_id': self.fip_pool['id'], + 'port_id': None, + 'fixed_ip_address': None, + 'router_id': None} + fixed_ip_address = self.port_data2[1]['fixed_ips'][0]['ip_address'] + self.fip_associated = {'tenant_id': 'my_tenantid', + 'id': 'fip_id2', + 'floating_ip_address': '172.24.4.228', + 'floating_network_id': self.fip_pool['id'], + 'port_id': self.port_data2[1]['id'], + 'fixed_ip_address': fixed_ip_address, + 'router_id': 'router_id1'} + def tearDown(self): try: self.mox.UnsetStubs() @@ -618,3 +642,262 @@ class TestQuantumv2(test.TestCase): # specify only first and last network req_ids = [net['id'] for net in (self.nets3[0], self.nets3[-1])] self._get_available_networks(prv_nets, pub_nets, req_ids) + + def test_get_floating_ip_pools(self): + api = quantumapi.API() + search_opts = {'router:external': True} + self.moxed_client.list_networks(**search_opts).\ + AndReturn({'networks': [self.fip_pool, self.fip_pool_nova]}) + self.mox.ReplayAll() + pools = api.get_floating_ip_pools(self.context) + expected = [{'name': self.fip_pool['name']}, + {'name': self.fip_pool_nova['name']}] + self.assertEqual(expected, pools) + + def _get_expected_fip_model(self, fip_data, idx=0): + expected = {'id': fip_data['id'], + 'address': fip_data['floating_ip_address'], + 'pool': self.fip_pool['name'], + 'project_id': fip_data['tenant_id'], + 'fixed_ip_id': fip_data['port_id'], + 'fixed_ip': + {'address': fip_data['fixed_ip_address']}, + 'instance': ({'uuid': self.port_data2[idx]['device_id']} + if fip_data['port_id'] + else None)} + return expected + + def _test_get_floating_ip(self, fip_data, idx=0, by_address=False): + api = quantumapi.API() + fip_id = fip_data['id'] + net_id = fip_data['floating_network_id'] + address = fip_data['floating_ip_address'] + if by_address: + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [fip_data]}) + else: + self.moxed_client.show_floatingip(fip_id).\ + AndReturn({'floatingip': fip_data}) + self.moxed_client.show_network(net_id).\ + AndReturn({'network': self.fip_pool}) + if fip_data['port_id']: + self.moxed_client.show_port(fip_data['port_id']).\ + AndReturn({'port': self.port_data2[idx]}) + self.mox.ReplayAll() + + expected = self._get_expected_fip_model(fip_data, idx) + + if by_address: + fip = api.get_floating_ip_by_address(self.context, address) + else: + fip = api.get_floating_ip(self.context, fip_id) + self.assertEqual(expected, fip) + + def test_get_floating_ip_unassociated(self): + self._test_get_floating_ip(self.fip_unassociated, idx=0) + + def test_get_floating_ip_associated(self): + self._test_get_floating_ip(self.fip_associated, idx=1) + + def test_get_floating_ip_by_address(self): + self._test_get_floating_ip(self.fip_unassociated, idx=0, + by_address=True) + + def test_get_floating_ip_by_address_associated(self): + self._test_get_floating_ip(self.fip_associated, idx=1, + by_address=True) + + def test_get_floating_ip_by_address_not_found(self): + api = quantumapi.API() + address = self.fip_unassociated['floating_ip_address'] + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': []}) + self.mox.ReplayAll() + self.assertRaises(exception.FloatingIpNotFoundForAddress, + api.get_floating_ip_by_address, + self.context, address) + + def test_get_floating_ip_by_address_multiple_found(self): + api = quantumapi.API() + address = self.fip_unassociated['floating_ip_address'] + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [self.fip_unassociated] * 2}) + self.mox.ReplayAll() + self.assertRaises(exception.FloatingIpMultipleFoundForAddress, + api.get_floating_ip_by_address, + self.context, address) + + def test_get_floating_ips_by_project(self): + api = quantumapi.API() + project_id = self.context.project_id + self.moxed_client.list_floatingips(tenant_id=project_id).\ + AndReturn({'floatingips': [self.fip_unassociated, + self.fip_associated]}) + search_opts = {'router:external': True} + self.moxed_client.list_networks(**search_opts).\ + AndReturn({'networks': [self.fip_pool, self.fip_pool_nova]}) + self.moxed_client.list_ports(tenant_id=project_id).\ + AndReturn({'ports': self.port_data2}) + self.mox.ReplayAll() + + expected = [self._get_expected_fip_model(self.fip_unassociated), + self._get_expected_fip_model(self.fip_associated, idx=1)] + fips = api.get_floating_ips_by_project(self.context) + self.assertEqual(expected, fips) + + def _test_get_instance_id_by_floating_address(self, fip_data, + associated=False): + api = quantumapi.API() + address = fip_data['floating_ip_address'] + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [fip_data]}) + if associated: + self.moxed_client.show_port(fip_data['port_id']).\ + AndReturn({'port': self.port_data2[1]}) + self.mox.ReplayAll() + + if associated: + expected = self.port_data2[1]['device_id'] + else: + expected = None + fip = api.get_instance_id_by_floating_address(self.context, address) + self.assertEqual(expected, fip) + + def test_get_instance_id_by_floating_address(self): + self._test_get_instance_id_by_floating_address(self.fip_unassociated) + + def test_get_instance_id_by_floating_address_associated(self): + self._test_get_instance_id_by_floating_address(self.fip_associated, + associated=True) + + def test_allocate_floating_ip(self): + api = quantumapi.API() + pool_name = self.fip_pool['name'] + pool_id = self.fip_pool['id'] + search_opts = {'router:external': True, + 'fields': 'id', + 'name': pool_name} + self.moxed_client.list_networks(**search_opts).\ + AndReturn({'networks': [self.fip_pool]}) + self.moxed_client.create_floatingip( + {'floatingip': {'floating_network_id': pool_id}}).\ + AndReturn({'floatingip': self.fip_unassociated}) + self.mox.ReplayAll() + fip = api.allocate_floating_ip(self.context, 'ext_net') + self.assertEqual(fip, self.fip_unassociated['floating_ip_address']) + + def test_allocate_floating_ip_with_pool_id(self): + api = quantumapi.API() + pool_name = self.fip_pool['name'] + pool_id = self.fip_pool['id'] + search_opts = {'router:external': True, + 'fields': 'id', + 'id': pool_id} + self.moxed_client.list_networks(**search_opts).\ + AndReturn({'networks': [self.fip_pool]}) + self.moxed_client.create_floatingip( + {'floatingip': {'floating_network_id': pool_id}}).\ + AndReturn({'floatingip': self.fip_unassociated}) + self.mox.ReplayAll() + fip = api.allocate_floating_ip(self.context, pool_id) + self.assertEqual(fip, self.fip_unassociated['floating_ip_address']) + + def test_allocate_floating_ip_with_default_pool(self): + api = quantumapi.API() + pool_name = self.fip_pool_nova['name'] + pool_id = self.fip_pool_nova['id'] + search_opts = {'router:external': True, + 'fields': 'id', + 'name': pool_name} + self.moxed_client.list_networks(**search_opts).\ + AndReturn({'networks': [self.fip_pool_nova]}) + self.moxed_client.create_floatingip( + {'floatingip': {'floating_network_id': pool_id}}).\ + AndReturn({'floatingip': self.fip_unassociated}) + self.mox.ReplayAll() + fip = api.allocate_floating_ip(self.context) + self.assertEqual(fip, self.fip_unassociated['floating_ip_address']) + + def test_release_floating_ip(self): + api = quantumapi.API() + address = self.fip_unassociated['floating_ip_address'] + fip_id = self.fip_unassociated['id'] + + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [self.fip_unassociated]}) + self.moxed_client.delete_floatingip(fip_id) + self.mox.ReplayAll() + api.release_floating_ip(self.context, address) + + def test_release_floating_ip_associated(self): + api = quantumapi.API() + address = self.fip_associated['floating_ip_address'] + fip_id = self.fip_associated['id'] + + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [self.fip_associated]}) + self.mox.ReplayAll() + self.assertRaises(exception.FloatingIpAssociated, + api.release_floating_ip, self.context, address) + + def _setup_mock_for_refresh_cache(self, api): + nw_info = self.mox.CreateMock(model.NetworkInfo) + nw_info.json() + self.mox.StubOutWithMock(api, '_get_instance_nw_info') + api._get_instance_nw_info(mox.IgnoreArg(), self.instance).\ + AndReturn(nw_info) + self.mox.StubOutWithMock(api.db, 'instance_info_cache_update') + api.db.instance_info_cache_update(mox.IgnoreArg(), + self.instance['uuid'], + mox.IgnoreArg()) + + def test_associate_floating_ip(self): + api = quantumapi.API() + address = self.fip_associated['floating_ip_address'] + fixed_address = self.fip_associated['fixed_ip_address'] + fip_id = self.fip_associated['id'] + + search_opts = {'device_owner': 'compute:nova', + 'device_id': self.instance['uuid']} + self.moxed_client.list_ports(**search_opts).\ + AndReturn({'ports': [self.port_data2[1]]}) + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [self.fip_associated]}) + self.moxed_client.update_floatingip( + fip_id, {'floatingip': {'port_id': self.fip_associated['port_id'], + 'fixed_ip_address': fixed_address}}) + self._setup_mock_for_refresh_cache(api) + + self.mox.ReplayAll() + api.associate_floating_ip(self.context, self.instance, + address, fixed_address) + + def test_associate_floating_ip_not_found_fixed_ip(self): + api = quantumapi.API() + address = self.fip_associated['floating_ip_address'] + fixed_address = self.fip_associated['fixed_ip_address'] + fip_id = self.fip_associated['id'] + + search_opts = {'device_owner': 'compute:nova', + 'device_id': self.instance['uuid']} + self.moxed_client.list_ports(**search_opts).\ + AndReturn({'ports': [self.port_data2[0]]}) + + self.mox.ReplayAll() + self.assertRaises(exception.FixedIpNotFoundForAddress, + api.associate_floating_ip, self.context, + self.instance, address, fixed_address) + + def test_disassociate_floating_ip(self): + api = quantumapi.API() + address = self.fip_associated['floating_ip_address'] + fip_id = self.fip_associated['id'] + + self.moxed_client.list_floatingips(floating_ip_address=address).\ + AndReturn({'floatingips': [self.fip_associated]}) + self.moxed_client.update_floatingip( + fip_id, {'floatingip': {'port_id': None}}) + self._setup_mock_for_refresh_cache(api) + + self.mox.ReplayAll() + api.disassociate_floating_ip(self.context, self.instance, address) diff --git a/tools/pip-requires b/tools/pip-requires index b3716352c..1e43a9c04 100644 --- a/tools/pip-requires +++ b/tools/pip-requires @@ -19,5 +19,5 @@ Babel>=0.9.6 iso8601>=0.1.4 httplib2 setuptools_git>=0.4 -python-quantumclient>=2.0 +python-quantumclient>=2.1 python-glanceclient>=0.5.0,<2 |