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
This commit is contained in:
Masahito Muroi 2019-01-10 19:02:58 +09:00
parent 84578965e3
commit f71d028e10
9 changed files with 382 additions and 0 deletions

View File

@ -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)

View File

@ -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)

View File

@ -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")

View File

@ -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'

View File

@ -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))

View File

@ -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')

View File

@ -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 .")

View File

@ -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)