From f71d028e108bbf9d302e966acb2978a2d11623b5 Mon Sep 17 00:00:00 2001 From: Masahito Muroi Date: Thu, 10 Jan 2019 19:02:58 +0900 Subject: [PATCH] Add floatingip plugin to support floating IP resource To support floating IP reservation, this patch adds the floatingip plugin. As first step to the goal, the plugin supports create, get and delete floatingip resource. Partially Implements: blueprint floatingip-reservation Change-Id: I271cfa4b4ad685c7095fa5ef4ac6721a86a5b07a --- blazar/db/api.py | 24 +++ blazar/db/sqlalchemy/api.py | 48 ++++++ blazar/manager/exceptions.py | 11 ++ blazar/plugins/floatingips/__init__.py | 15 ++ .../plugins/floatingips/floatingip_plugin.py | 101 ++++++++++++ blazar/tests/plugins/floatingips/__init__.py | 0 .../floatingips/test_floatingip_plugin.py | 145 ++++++++++++++++++ blazar/utils/openstack/exceptions.py | 9 ++ blazar/utils/openstack/neutron.py | 29 ++++ 9 files changed, 382 insertions(+) create mode 100644 blazar/plugins/floatingips/__init__.py create mode 100644 blazar/plugins/floatingips/floatingip_plugin.py create mode 100644 blazar/tests/plugins/floatingips/__init__.py create mode 100644 blazar/tests/plugins/floatingips/test_floatingip_plugin.py diff --git a/blazar/db/api.py b/blazar/db/api.py index c712e64e..cb06505c 100644 --- a/blazar/db/api.py +++ b/blazar/db/api.py @@ -416,3 +416,27 @@ def host_extra_capability_get_all_per_name(host_id, def host_get_all_by_queries_including_extracapabilities(queries): """Returns hosts filtered by an array of queries.""" return IMPL.host_get_all_by_queries_including_extracapabilities(queries) + + +# Floating ip + +def floatingip_create(values): + """Create a floating ip from the values.""" + return IMPL.floatingip_create(values) + + +@to_dict +def floatingip_get(floatingip_id): + """Return a specific floating ip.""" + return IMPL.floatingip_get(floatingip_id) + + +@to_dict +def floatingip_list(): + """Return a list of floating ip.""" + return IMPL.floatingip_list() + + +def floatingip_destroy(floatingip_id): + """Delete specific floating ip.""" + IMPL.floatingip_destroy(floatingip_id) diff --git a/blazar/db/sqlalchemy/api.py b/blazar/db/sqlalchemy/api.py index e88ec5ab..0c7e3334 100644 --- a/blazar/db/sqlalchemy/api.py +++ b/blazar/db/sqlalchemy/api.py @@ -828,3 +828,51 @@ def host_extra_capability_get_all_per_name(host_id, capability_name): with session.begin(): query = _host_extra_capability_get_all_per_host(session, host_id) return query.filter_by(capability_name=capability_name).all() + + +# Floating IP +def _floatingip_get(session, floatingip_id): + query = model_query(models.FloatingIP, session) + return query.filter_by(id=floatingip_id).first() + + +def _floatingip_get_all(session): + query = model_query(models.FloatingIP, session) + return query + + +def floatingip_get(floatingip_id): + return _floatingip_get(get_session(), floatingip_id) + + +def floatingip_list(): + return model_query(models.FloatingIP, get_session()).all() + + +def floatingip_create(values): + values = values.copy() + floatingip = models.FloatingIP() + floatingip.update(values) + + session = get_session() + with session.begin(): + try: + floatingip.save(session=session) + except common_db_exc.DBDuplicateEntry as e: + # raise exception about duplicated columns (e.columns) + raise db_exc.BlazarDBDuplicateEntry( + model=floatingip.__class__.__name__, columns=e.columns) + + return floatingip_get(floatingip.id) + + +def floatingip_destroy(floatingip_id): + session = get_session() + with session.begin(): + floatingip = _floatingip_get(session, floatingip_id) + + if not floatingip: + # raise not found error + raise db_exc.BlazarDBNotFound(id=floatingip_id, model='FloatingIP') + + session.delete(floatingip) diff --git a/blazar/manager/exceptions.py b/blazar/manager/exceptions.py index 0663194c..8751f773 100644 --- a/blazar/manager/exceptions.py +++ b/blazar/manager/exceptions.py @@ -191,3 +191,14 @@ class CantUpdateParameter(exceptions.BlazarException): class InvalidPeriod(exceptions.BlazarException): code = 400 msg_fmt = _('The end_date must be later than the start_date.') + + +# floating ip plugin related exceptions + +class FloatingIPNotFound(exceptions.NotFound): + msg_fmt = _("Floating IP %(floatingip)s not found.") + + +class CantDeleteFloatingIP(exceptions.BlazarException): + code = 409 + msg_fmt = _("Can't delete floating IP %(floatingip)s. %(msg)s") diff --git a/blazar/plugins/floatingips/__init__.py b/blazar/plugins/floatingips/__init__.py new file mode 100644 index 00000000..4539689d --- /dev/null +++ b/blazar/plugins/floatingips/__init__.py @@ -0,0 +1,15 @@ +# Copyright (c) 2019 NTT. +# +# 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. + +RESOURCE_TYPE = u'virtual:floatingip' diff --git a/blazar/plugins/floatingips/floatingip_plugin.py b/blazar/plugins/floatingips/floatingip_plugin.py new file mode 100644 index 00000000..22d51133 --- /dev/null +++ b/blazar/plugins/floatingips/floatingip_plugin.py @@ -0,0 +1,101 @@ +# Copyright (c) 2019 NTT. +# +# 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 oslo_log import log as logging + +from blazar.db import api as db_api +from blazar.db import exceptions as db_ex +from blazar import exceptions +from blazar.manager import exceptions as manager_ex +from blazar.plugins import base +from blazar.plugins import floatingips as plugin +from blazar.utils.openstack import neutron + + +LOG = logging.getLogger(__name__) + + +class FloatingIpPlugin(base.BasePlugin): + """Plugin for floating IP resource.""" + + resource_type = plugin.RESOURCE_TYPE + title = 'Floating IP Plugin' + description = 'This plugin creates and assigns floating IPs.' + + def reserve_resource(self, reservation_id, values): + raise NotImplementedError + + def on_start(self, resource_id): + raise NotImplementedError + + def on_end(self, resource_id): + raise NotImplementedError + + def validate_floatingip_params(self, values): + marshall_attributes = set(['floating_network_id', + 'floating_ip_address']) + missing_attr = marshall_attributes - set(values.keys()) + if missing_attr: + raise manager_ex.MissingParameter(param=','.join(missing_attr)) + + def create_floatingip(self, values): + + self.validate_floatingip_params(values) + + network_id = values.pop('floating_network_id') + floatingip_address = values.pop('floating_ip_address') + + pool = neutron.FloatingIPPool(network_id) + # validate the floating ip address is out of allocation_pools and + # within its subnet cidr. + try: + subnet = pool.fetch_subnet(floatingip_address) + except exceptions.BlazarException: + LOG.info("Floating IP %s in network %s can't be used " + "for Blazar's resource.", floatingip_address, network_id) + raise + + floatingip_values = { + 'floating_network_id': network_id, + 'subnet_id': subnet['id'], + 'floating_ip_address': floatingip_address + } + + floatingip = db_api.floatingip_create(floatingip_values) + + return floatingip + + def get_floatingip(self, fip_id): + fip = db_api.floatingip_get(fip_id) + if fip is None: + raise manager_ex.FloatingIPNotFound(floatingip=fip_id) + return fip + + def list_floatingip(self): + fips = db_api.floatingip_list() + return fips + + def delete_floatingip(self, fip_id): + fip = db_api.floatingip_get(fip_id) + if fip is None: + raise manager_ex.FloatingIPNotFound(floatingip=fip_id) + + # TODO(masahito): Check no allocation exists for the floating ip here + # once this plugin supports reserve_resource method. + + try: + db_api.floatingip_destroy(fip_id) + except db_ex.BlazarDBException as e: + raise manager_ex.CantDeleteFloatingIP(floatingip=fip_id, + msg=str(e)) diff --git a/blazar/tests/plugins/floatingips/__init__.py b/blazar/tests/plugins/floatingips/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/blazar/tests/plugins/floatingips/test_floatingip_plugin.py b/blazar/tests/plugins/floatingips/test_floatingip_plugin.py new file mode 100644 index 00000000..14df5620 --- /dev/null +++ b/blazar/tests/plugins/floatingips/test_floatingip_plugin.py @@ -0,0 +1,145 @@ +# Copyright (c) 2019 NTT. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import mock + +from blazar.db import api as db_api +from blazar.manager import exceptions as mgr_exceptions +from blazar.plugins.floatingips import floatingip_plugin +from blazar import tests +from blazar.utils.openstack import exceptions as opst_exceptions +from blazar.utils.openstack import neutron + + +class FloatingIpPluginTest(tests.TestCase): + + def setUp(self): + super(FloatingIpPluginTest, self).setUp() + self.fip_pool = self.patch(neutron, 'FloatingIPPool') + + def test_create_floatingip(self): + m = mock.MagicMock() + m.fetch_subnet.return_value = {'id': 'subnet-id'} + self.fip_pool.return_value = m + fip_row = { + 'id': 'fip-id', + 'network_id': 'net-id', + 'subnet_id': 'subnet-id', + 'floating_ip_address': '172.24.4.100', + 'reservable': True + } + patch_fip_create = self.patch(db_api, 'floatingip_create') + patch_fip_create.return_value = fip_row + + data = { + 'floating_ip_address': '172.24.4.100', + 'floating_network_id': 'net-id' + } + expected = fip_row + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + ret = fip_plugin.create_floatingip(data) + + self.assertDictEqual(expected, ret) + m.fetch_subnet.assert_called_once_with('172.24.4.100') + patch_fip_create.assert_called_once_with({ + 'floating_network_id': 'net-id', + 'subnet_id': 'subnet-id', + 'floating_ip_address': '172.24.4.100'}) + + def test_create_floatingip_with_invalid_ip(self): + m = mock.MagicMock() + m.fetch_subnet.side_effect = opst_exceptions.NeutronUsesFloatingIP() + self.fip_pool.return_value = m + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + self.assertRaises(opst_exceptions.NeutronUsesFloatingIP, + fip_plugin.create_floatingip, + {'floating_ip_address': 'invalid-ip', + 'floating_network_id': 'id'}) + + def test_get_floatingip(self): + fip_row = { + 'id': 'fip-id', + 'network_id': 'net-id', + 'subnet_id': 'subnet-id', + 'floating_ip_address': '172.24.4.100', + 'reservable': True + } + patch_fip_get = self.patch(db_api, 'floatingip_get') + patch_fip_get.return_value = fip_row + + expected = fip_row + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + ret = fip_plugin.get_floatingip('fip-id') + + self.assertDictEqual(expected, ret) + patch_fip_get.assert_called_once_with('fip-id') + + def test_get_floatingip_with_no_exist(self): + patch_fip_get = self.patch(db_api, 'floatingip_get') + patch_fip_get.return_value = None + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + self.assertRaises(mgr_exceptions.FloatingIPNotFound, + fip_plugin.get_floatingip, 'fip-id') + + patch_fip_get.assert_called_once_with('fip-id') + + def test_get_list_floatingips(self): + fip_rows = [{ + 'id': 'fip-id', + 'network_id': 'net-id', + 'subnet_id': 'subnet-id', + 'floating_ip_address': '172.24.4.100', + 'reservable': True + }] + patch_fip_list = self.patch(db_api, 'floatingip_list') + patch_fip_list.return_value = fip_rows + + expected = fip_rows + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + ret = fip_plugin.list_floatingip() + + self.assertListEqual(expected, ret) + patch_fip_list.assert_called_once_with() + + def test_delete_floatingip(self): + fip_row = { + 'id': 'fip-id', + 'network_id': 'net-id', + 'subnet_id': 'subnet-id', + 'floating_ip_address': '172.24.4.100', + 'reservable': True + } + patch_fip_get = self.patch(db_api, 'floatingip_get') + patch_fip_get.return_value = fip_row + patch_fip_destroy = self.patch(db_api, 'floatingip_destroy') + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + fip_plugin.delete_floatingip('fip-id') + + patch_fip_get.assert_called_once_with('fip-id') + patch_fip_destroy.assert_called_once_with('fip-id') + + def test_delete_floatingip_with_no_exist(self): + patch_fip_get = self.patch(db_api, 'floatingip_get') + patch_fip_get.return_value = None + + fip_plugin = floatingip_plugin.FloatingIpPlugin() + self.assertRaises(mgr_exceptions.FloatingIPNotFound, + fip_plugin.delete_floatingip, + 'non-exists-id') diff --git a/blazar/utils/openstack/exceptions.py b/blazar/utils/openstack/exceptions.py index 65c39ddc..c5e15c69 100644 --- a/blazar/utils/openstack/exceptions.py +++ b/blazar/utils/openstack/exceptions.py @@ -46,3 +46,12 @@ class InventoryUpdateFailed(exceptions.BlazarException): class FloatingIPNetworkNotFound(exceptions.InvalidInput): msg_fmt = _("Failed to find network %(network)s") + + +class FloatingIPSubnetNotFound(exceptions.NotFound): + msg_fmt = _("Valid subnet for the floating IP %(fip)s is not found.") + + +class NeutronUsesFloatingIP(exceptions.InvalidInput): + msg_fmt = _("The floating IP %(floatingip)s is used in allocation_pools " + "or gateway_ip in subnet %(subnet)s .") diff --git a/blazar/utils/openstack/neutron.py b/blazar/utils/openstack/neutron.py index 8e4651ec..10f67b22 100644 --- a/blazar/utils/openstack/neutron.py +++ b/blazar/utils/openstack/neutron.py @@ -15,6 +15,7 @@ from keystoneauth1.identity import v3 from keystoneauth1 import session +import netaddr from neutronclient.common import exceptions as neutron_exceptions from neutronclient.v2_0 import client as neutron_client @@ -72,3 +73,31 @@ class FloatingIPPool(BlazarNeutronClient): raise exceptions.FloatingIPNetworkNotFound(network=network_id) self.network_id = network_id + + def fetch_subnet(self, floatingip): + fip = netaddr.IPAddress(floatingip) + network = self.neutron.show_network(self.network_id)['network'] + subnet_ids = network['subnets'] + + for sub_id in subnet_ids: + subnet = self.neutron.show_subnet(sub_id)['subnet'] + cidr = netaddr.IPNetwork(subnet['cidr']) + + # skip the subnet because it has not valid cidr for the floating ip + if fip not in cidr: + continue + + allocated_ip = netaddr.IPSet() + + allocated_ip.add(netaddr.IPAddress(subnet['gateway_ip'])) + for alloc in subnet['allocation_pools']: + allocated_ip.add(netaddr.IPRange(alloc['start'], alloc['end'])) + + if fip in allocated_ip: + raise exceptions.NeutronUsesFloatingIP(floatingip=fip, + subnet=subnet['id']) + else: + self.subnet_id = subnet['id'] + return subnet + + raise exceptions.FloatingIPSubnetNotFound(fip=floatingip)