diff --git a/manila/db/api.py b/manila/db/api.py index e5459a79d1..9635150b63 100644 --- a/manila/db/api.py +++ b/manila/db/api.py @@ -552,6 +552,11 @@ def network_allocations_get_for_share_server(context, share_server_id, session=session) +def network_allocations_get_by_ip_address(context, ip_address): + """Get network allocations by IP address.""" + return IMPL.network_allocations_get_by_ip_address(context, ip_address) + + ################## diff --git a/manila/db/sqlalchemy/api.py b/manila/db/sqlalchemy/api.py index 6e2be785e8..c69e142c60 100644 --- a/manila/db/sqlalchemy/api.py +++ b/manila/db/sqlalchemy/api.py @@ -1959,6 +1959,7 @@ def share_server_backend_details_get(context, share_server_id, @require_context def network_allocation_create(context, values): + values['id'] = values.get('id', six.text_type(uuid.uuid4())) alloc_ref = models.NetworkAllocation() alloc_ref.update(values) session = get_session() @@ -1986,6 +1987,14 @@ def network_allocation_get(context, id, session=None): return result +@require_context +def network_allocations_get_by_ip_address(context, ip_address): + session = get_session() + result = model_query(context, models.NetworkAllocation, session=session).\ + filter_by(ip_address=ip_address).all() + return result or [] + + @require_context def network_allocations_get_for_share_server(context, share_server_id, session=None): diff --git a/manila/network/standalone_network_plugin.py b/manila/network/standalone_network_plugin.py new file mode 100644 index 0000000000..b7d501d43d --- /dev/null +++ b/manila/network/standalone_network_plugin.py @@ -0,0 +1,262 @@ +# Copyright 2015 Mirantis, Inc. +# All Rights Reserved. +# +# 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 netaddr +from oslo_config import cfg +import six + +from manila.common import constants +from manila import exception +from manila.i18n import _ +from manila import network +from manila.openstack.common import log as logging +from manila import utils + +standalone_network_plugin_opts = [ + cfg.StrOpt( + 'standalone_network_plugin_gateway', + help="Gateway IPv4 address that should be used. Required.", + deprecated_group='DEFAULT'), + cfg.StrOpt( + 'standalone_network_plugin_mask', + help="Network mask that will be used. Can be either decimal " + "like '24' or binary like '255.255.255.0'. Required.", + deprecated_group='DEFAULT'), + cfg.StrOpt( + 'standalone_network_plugin_segmentation_id', + help="Set it if network has segmentation (VLAN, VXLAN, etc...). " + "It will be assigned to share-network and share drivers will be " + "able to use this for network interfaces within provisioned " + "share servers. Optional. Example: 1001", + deprecated_group='DEFAULT'), + cfg.ListOpt( + 'standalone_network_plugin_allowed_ip_ranges', + help="Can be IP address, range of IP addresses or list of addresses " + "or ranges. Contains addresses from IP network that are allowed " + "to be used. If empty, then will be assumed that all host " + "addresses from network can be used. Optional. " + "Examples: 10.0.0.10 or 10.0.0.10-10.0.0.20 or " + "10.0.0.10-10.0.0.20,10.0.0.30-10.0.0.40,10.0.0.50", + deprecated_group='DEFAULT'), + cfg.IntOpt( + 'standalone_network_plugin_ip_version', + default=4, + help="IP version of network. Optional." + "Allowed values are '4' and '6'. Default value is '4'.", + deprecated_group='DEFAULT'), +] + +CONF = cfg.CONF +LOG = logging.getLogger(__name__) + + +class StandaloneNetworkPlugin(network.NetworkBaseAPI): + """Standalone network plugin for share drivers. + + This network plugin can be used with any network platform. + It can serve flat networks as well as segmented. + It does not require some specific network services in OpenStack like + Neutron or Nova. + The only thing that plugin does is reservation and release of IP addresses + from some network. + """ + + def __init__(self, config_group_name=None, db_driver=None): + super(StandaloneNetworkPlugin, self).__init__(db_driver=db_driver) + self.config_group_name = config_group_name or 'DEFAULT' + CONF.register_opts( + standalone_network_plugin_opts, + group=self.config_group_name) + self.configuration = getattr(CONF, self.config_group_name, CONF) + self._set_persistent_network_data() + LOG.debug( + "\nStandalone network plugin data for config group " + "'%(config_group)s': \n" + "IP version - %(ip_version)s\n" + "Used network - %(net)s\n" + "Used gateway - %(gateway)s\n" + "Used segmentation ID - %(segmentation_id)s\n" + "Allowed CIDRs - %(cidrs)s\n" + "Original allowed IP ranges - %(ip_ranges)s\n" + "Reserved IP addresses - %(reserved)s\n", + dict( + config_group=self.config_group_name, + ip_version=self.ip_version, + net=six.text_type(self.net), + gateway=self.gateway, + segmentation_id=self.segmentation_id, + cidrs=self.allowed_cidrs, + ip_ranges=self.allowed_ip_ranges, + reserved=self.reserved_addresses)) + + def _set_persistent_network_data(self): + """Sets persistent data for whole plugin.""" + self.segmentation_id = ( + self.configuration.standalone_network_plugin_segmentation_id) + self.gateway = self.configuration.standalone_network_plugin_gateway + self.mask = self.configuration.standalone_network_plugin_mask + self.allowed_ip_ranges = ( + self.configuration.standalone_network_plugin_allowed_ip_ranges) + self.ip_version = int( + self.configuration.standalone_network_plugin_ip_version) + self.net = self._get_network() + self.allowed_cidrs = self._get_list_of_allowed_addresses() + self.reserved_addresses = ( + six.text_type(self.net.network), + self.gateway, + six.text_type(self.net.broadcast)) + + def _get_network(self): + """Returns IPNetwork object calculated from gateway and netmask.""" + if not isinstance(self.gateway, six.string_types): + raise exception.NetworkBadConfigurationException( + _("Configuration option 'standalone_network_plugin_gateway' " + "is required and has improper value '%s'.") % self.gateway) + if not isinstance(self.mask, six.string_types): + raise exception.NetworkBadConfigurationException( + _("Configuration option 'standalone_network_plugin_mask' is " + "required and has improper value '%s'.") % self.mask) + try: + return netaddr.IPNetwork(self.gateway + '/' + self.mask) + except netaddr.AddrFormatError as e: + raise exception.NetworkBadConfigurationException( + reason=e) + + def _get_list_of_allowed_addresses(self): + """Returns list of CIDRs that can be used for getting IP addresses. + + Reads information provided via configuration, such as gateway, + netmask, segmentation ID and allowed IP ranges, then performs + validation of provided data. + + :returns: list of CIDRs as text types. + :raises: exception.NetworkBadConfigurationException + """ + cidrs = [] + if self.allowed_ip_ranges: + for ip_range in self.allowed_ip_ranges: + ip_range_start = ip_range_end = None + if utils.is_valid_ip_address(ip_range, self.ip_version): + ip_range_start = ip_range_end = ip_range + elif '-' in ip_range: + ip_range_list = ip_range.split('-') + if len(ip_range_list) == 2: + ip_range_start = ip_range_list[0] + ip_range_end = ip_range_list[1] + for ip in ip_range_list: + utils.is_valid_ip_address(ip, self.ip_version) + else: + msg = _("Wrong value for IP range " + "'%s' was provided.") % ip_range + raise exception.NetworkBadConfigurationException( + reason=msg) + else: + msg = _("Config option " + "'standalone_network_plugin_allowed_ip_ranges' " + "has incorrect value " + "'%s'") % self.allowed_ip_ranges + raise exception.NetworkBadConfigurationException( + reason=msg) + + range_instance = netaddr.IPRange(ip_range_start, ip_range_end) + + if range_instance not in self.net: + data = dict( + range=six.text_type(range_instance), + net=six.text_type(self.net), + gateway=self.gateway, + netmask=self.net.netmask) + msg = _("One of provided allowed IP ranges ('%(range)s') " + "does not fit network '%(net)s' combined from " + "gateway '%(gateway)s' and netmask " + "'%(netmask)s'.") % data + raise exception.NetworkBadConfigurationException( + reason=msg) + + cidrs.extend( + six.text_type(cidr) for cidr in range_instance.cidrs()) + else: + if self.net.version != self.ip_version: + msg = _("Configured invalid IP version '%(conf_v)s', network " + "has version ""'%(net_v)s'") % dict( + conf_v=self.ip_version, net_v=self.net.version) + raise exception.NetworkBadConfigurationException(reason=msg) + cidrs.append(six.text_type(self.net)) + + return cidrs + + def _get_available_ips(self, context, amount): + """Returns IP addresses from allowed IP range if there are unused IPs. + + :returns: IP addresses as list of text types + :raises: exception.NetworkBadConfigurationException + """ + ips = [] + if amount < 1: + return ips + iterator = netaddr.iter_unique_ips(*self.allowed_cidrs) + for ip in iterator: + ip = six.text_type(ip) + if (ip in self.reserved_addresses or + self.db.network_allocations_get_by_ip_address(context, + ip)): + continue + else: + ips.append(ip) + if len(ips) == amount: + return ips + msg = _("No available IP addresses left in CIDRs %(cidrs)s. " + "Requested amount of IPs to be provided '%(amount)s', " + "available only '%(available)s'.") % { + 'cidrs': self.allowed_cidrs, + 'amount': amount, + 'available': len(ips)} + raise exception.NetworkBadConfigurationException(reason=msg) + + def _save_network_info(self, context, share_network): + """Update share-network with plugin specific data.""" + data = dict( + segmentation_id=self.segmentation_id, + cidr=self.net.cidr, + ip_version=self.ip_version) + self.db.share_network_update(context, share_network['id'], data) + + @utils.synchronized( + "allocate_network_for_standalone_network_plugin", external=True) + def allocate_network(self, context, share_server, share_network, **kwargs): + """Allocate network resources using one dedicated network. + + This one has interprocess lock to avoid concurrency in creation of + share servers with same IP addresses using different share-networks. + """ + allocation_count = kwargs.get('count', 1) + self._save_network_info(context, share_network) + allocations = [] + ip_addresses = self._get_available_ips(context, allocation_count) + for ip_address in ip_addresses: + data = dict( + share_server_id=share_server['id'], + ip_address=ip_address, + status=constants.STATUS_ACTIVE) + allocations.append( + self.db.network_allocation_create(context, data)) + return allocations + + def deallocate_network(self, context, share_server_id): + """Deallocate network resources for share server.""" + allocations = self.db.network_allocations_get_for_share_server( + context, share_server_id) + for allocation in allocations: + self.db.network_allocation_delete(context, allocation['id']) diff --git a/manila/opts.py b/manila/opts.py index c81e21d675..0e41e60693 100644 --- a/manila/opts.py +++ b/manila/opts.py @@ -34,6 +34,7 @@ import manila.network import manila.network.linux.interface import manila.network.neutron.api import manila.network.neutron.neutron_network_plugin +import manila.network.standalone_network_plugin import manila.openstack.common.eventlet_backdoor import manila.openstack.common.log import manila.openstack.common.policy @@ -85,6 +86,7 @@ _global_opt_lists = [ manila.network.neutron.api.neutron_opts, manila.network.neutron.neutron_network_plugin. neutron_single_network_plugin_opts, + manila.network.standalone_network_plugin.standalone_network_plugin_opts, manila.openstack.common.eventlet_backdoor.eventlet_backdoor_opts, manila.openstack.common.log.common_cli_opts, manila.openstack.common.log.generic_log_opts, diff --git a/manila/tests/network/test_standalone_network_plugin.py b/manila/tests/network/test_standalone_network_plugin.py new file mode 100644 index 0000000000..e65340499d --- /dev/null +++ b/manila/tests/network/test_standalone_network_plugin.py @@ -0,0 +1,389 @@ +# Copyright 2015 Mirantis, Inc. +# All Rights Reserved +# +# 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 ddt +import mock +import netaddr +from oslo_config import cfg + +from manila.common import constants +from manila import context +from manila import exception +from manila.network import standalone_network_plugin as plugin +from manila import test +from manila.tests import utils as test_utils + +CONF = cfg.CONF + +fake_context = context.RequestContext( + user_id='fake user', project_id='fake project', is_admin=False) +fake_share_server = dict(id='fake_share_server_id') +fake_share_network = dict(id='fake_share_network_id') + + +@ddt.ddt +class StandaloneNetworkPluginTest(test.TestCase): + + @ddt.data('custom_config_group_name', 'DEFAULT') + def test_init_only_with_required_data_v4(self, group_name): + data = { + group_name: { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '24', + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin( + config_group_name=group_name) + + self.assertEqual('10.0.0.1', instance.gateway) + self.assertEqual('24', instance.mask) + self.assertEqual(None, instance.segmentation_id) + self.assertEqual(None, instance.allowed_ip_ranges) + self.assertEqual(4, instance.ip_version) + self.assertEqual(netaddr.IPNetwork('10.0.0.1/24'), instance.net) + self.assertEqual(['10.0.0.1/24'], instance.allowed_cidrs) + self.assertEqual( + ('10.0.0.0', '10.0.0.1', '10.0.0.255'), + instance.reserved_addresses) + + @ddt.data('custom_config_group_name', 'DEFAULT') + def test_init_with_all_data_v4(self, group_name): + data = { + group_name: { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '255.255.0.0', + 'standalone_network_plugin_segmentation_id': '1001', + 'standalone_network_plugin_allowed_ip_ranges': ( + '10.0.0.3-10.0.0.7,10.0.0.69-10.0.0.157,10.0.0.213'), + 'standalone_network_plugin_ip_version': 4, + }, + } + allowed_cidrs = [ + '10.0.0.3/32', '10.0.0.4/30', '10.0.0.69/32', '10.0.0.70/31', + '10.0.0.72/29', '10.0.0.80/28', '10.0.0.96/27', '10.0.0.128/28', + '10.0.0.144/29', '10.0.0.152/30', '10.0.0.156/31', '10.0.0.213/32', + ] + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin( + config_group_name=group_name) + + self.assertEqual(4, instance.ip_version) + self.assertEqual('10.0.0.1', instance.gateway) + self.assertEqual('255.255.0.0', instance.mask) + self.assertEqual('1001', instance.segmentation_id) + self.assertEqual(allowed_cidrs, instance.allowed_cidrs) + self.assertEqual( + ['10.0.0.3-10.0.0.7', '10.0.0.69-10.0.0.157', '10.0.0.213'], + instance.allowed_ip_ranges) + self.assertEqual( + netaddr.IPNetwork('10.0.0.1/255.255.0.0'), instance.net) + self.assertEqual( + ('10.0.0.0', '10.0.0.1', '10.0.255.255'), + instance.reserved_addresses) + + @ddt.data('custom_config_group_name', 'DEFAULT') + def test_init_only_with_required_data_v6(self, group_name): + data = { + group_name: { + 'standalone_network_plugin_gateway': ( + '2001:cdba::3257:9652'), + 'standalone_network_plugin_mask': '48', + 'standalone_network_plugin_ip_version': 6, + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin( + config_group_name=group_name) + + self.assertEqual( + '2001:cdba::3257:9652', instance.gateway) + self.assertEqual('48', instance.mask) + self.assertEqual(None, instance.segmentation_id) + self.assertEqual(None, instance.allowed_ip_ranges) + self.assertEqual(6, instance.ip_version) + self.assertEqual( + netaddr.IPNetwork('2001:cdba::3257:9652/48'), + instance.net) + self.assertEqual( + ['2001:cdba::3257:9652/48'], instance.allowed_cidrs) + self.assertEqual( + ('2001:cdba::', '2001:cdba::3257:9652', + '2001:cdba:0:ffff:ffff:ffff:ffff:ffff'), + instance.reserved_addresses) + + @ddt.data('custom_config_group_name', 'DEFAULT') + def test_init_with_all_data_v6(self, group_name): + data = { + group_name: { + 'standalone_network_plugin_gateway': '2001:db8::0001', + 'standalone_network_plugin_mask': '88', + 'standalone_network_plugin_segmentation_id': '3999', + 'standalone_network_plugin_allowed_ip_ranges': ( + '2001:db8::-2001:db8:0000:0000:0000:007f:ffff:ffff'), + 'standalone_network_plugin_ip_version': 6, + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin( + config_group_name=group_name) + + self.assertEqual(6, instance.ip_version) + self.assertEqual('2001:db8::0001', instance.gateway) + self.assertEqual('88', instance.mask) + self.assertEqual('3999', instance.segmentation_id) + self.assertEqual(['2001:db8::/89'], instance.allowed_cidrs) + self.assertEqual( + ['2001:db8::-2001:db8:0000:0000:0000:007f:ffff:ffff'], + instance.allowed_ip_ranges) + self.assertEqual( + netaddr.IPNetwork('2001:db8::0001/88'), instance.net) + self.assertEqual( + ('2001:db8::', '2001:db8::0001', '2001:db8::ff:ffff:ffff'), + instance.reserved_addresses) + + @ddt.data('custom_config_group_name', 'DEFAULT') + def test_invalid_init_without_any_config_definitions(self, group_name): + self.assertRaises( + exception.NetworkBadConfigurationException, + plugin.StandaloneNetworkPlugin, + config_group_name=group_name) + + @ddt.data( + {}, + {'gateway': '20.0.0.1'}, + {'mask': '8'}, + {'gateway': '20.0.0.1', 'mask': '33'}, + {'gateway': '20.0.0.256', 'mask': '16'}) + def test_invalid_init_required_data_improper(self, data): + group_name = 'custom_group_name' + if 'gateway' in data: + data['standalone_network_plugin_gateway'] = data.pop('gateway') + if 'mask' in data: + data['standalone_network_plugin_mask'] = data.pop('mask') + data = {group_name: data} + with test_utils.create_temp_config_with_opts(data): + self.assertRaises( + exception.NetworkBadConfigurationException, + plugin.StandaloneNetworkPlugin, + config_group_name=group_name) + + @ddt.data( + 'fake', + '11.0.0.0-11.0.0.5-11.0.0.11', + '11.0.0.0-11.0.0.5', + '10.0.10.0-10.0.10.5', + '10.0.0.0-10.0.0.5,fake', + '10.0.10.0-10.0.10.5,10.0.0.0-10.0.0.5', + '10.0.10.0-10.0.10.5,10.0.0.10-10.0.10.5', + '10.0.0.0-10.0.0.5,10.0.10.0-10.0.10.5') + def test_invalid_init_incorrect_allowed_ip_ranges_v4(self, ip_range): + group_name = 'DEFAULT' + data = { + group_name: { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '255.255.255.0', + 'standalone_network_plugin_allowed_ip_ranges': ip_range, + }, + } + with test_utils.create_temp_config_with_opts(data): + self.assertRaises( + exception.NetworkBadConfigurationException, + plugin.StandaloneNetworkPlugin, + config_group_name=group_name) + + @ddt.data( + {'gateway': '2001:db8::0001', 'vers': 4}, + {'gateway': '10.0.0.1', 'vers': 6}) + @ddt.unpack + def test_invalid_init_mismatch_of_versions(self, gateway, vers): + group_name = 'DEFAULT' + data = { + group_name: { + 'standalone_network_plugin_gateway': gateway, + 'standalone_network_plugin_ip_version': vers, + 'standalone_network_plugin_mask': '25', + }, + } + with test_utils.create_temp_config_with_opts(data): + self.assertRaises( + exception.NetworkBadConfigurationException, + plugin.StandaloneNetworkPlugin, + config_group_name=group_name) + + def test_deallocate_network(self): + share_server_id = 'fake_share_server_id' + data = { + 'DEFAULT': { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '24', + }, + } + fake_allocations = [{'id': 'fake1'}, {'id': 'fake2'}] + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin() + self.mock_object( + instance.db, 'network_allocations_get_for_share_server', + mock.Mock(return_value=fake_allocations)) + self.mock_object(instance.db, 'network_allocation_delete') + + instance.deallocate_network(fake_context, share_server_id) + + instance.db.network_allocations_get_for_share_server.\ + assert_called_once_with(fake_context, share_server_id) + instance.db.network_allocation_delete.\ + assert_has_calls([ + mock.call(fake_context, 'fake1'), + mock.call(fake_context, 'fake2'), + ]) + + def test_allocate_network_zero_addresses_ipv4(self): + data = { + 'DEFAULT': { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '24', + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin() + self.mock_object(instance.db, 'share_network_update') + + allocations = instance.allocate_network( + fake_context, fake_share_server, fake_share_network, count=0) + + self.assertEqual([], allocations) + instance.db.share_network_update.assert_called_once_wth( + fake_context, fake_share_network['id'], + dict(segmentation_id=None, cidr=instance.net.cidr, ip_version=4)) + + def test_allocate_network_zero_addresses_ipv6(self): + data = { + 'DEFAULT': { + 'standalone_network_plugin_gateway': '2001:db8::0001', + 'standalone_network_plugin_mask': '64', + 'standalone_network_plugin_ip_version': 6, + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin() + self.mock_object(instance.db, 'share_network_update') + + allocations = instance.allocate_network( + fake_context, fake_share_server, fake_share_network, count=0) + + self.assertEqual([], allocations) + instance.db.share_network_update.assert_called_once_with( + fake_context, fake_share_network['id'], + dict(segmentation_id=None, cidr=instance.net.cidr, ip_version=6)) + + def test_allocate_network_one_ip_address_ipv4_no_usages_exist(self): + data = { + 'DEFAULT': { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '24', + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin() + self.mock_object(instance.db, 'share_network_update') + self.mock_object(instance.db, 'network_allocation_create') + self.mock_object( + instance.db, 'network_allocations_get_by_ip_address', + mock.Mock(return_value=[])) + + allocations = instance.allocate_network( + fake_context, fake_share_server, fake_share_network) + + self.assertEqual(1, len(allocations)) + instance.db.share_network_update.assert_called_once_with( + fake_context, fake_share_network['id'], + dict(segmentation_id=None, cidr=instance.net.cidr, ip_version=4)) + instance.db.network_allocations_get_by_ip_address.assert_has_calls( + [mock.call(fake_context, '10.0.0.2')]) + instance.db.network_allocation_create.assert_called_once_with( + fake_context, + dict(share_server_id=fake_share_server['id'], + ip_address='10.0.0.2', status=constants.STATUS_ACTIVE)) + + def test_allocate_network_two_ip_addresses_ipv4_two_usages_exist(self): + ctxt = type('FakeCtxt', (object,), {'fake': ['10.0.0.2', '10.0.0.4']}) + + def fake_get_allocations_by_ip_address(context, ip_address): + if ip_address not in context.fake: + context.fake.append(ip_address) + return [] + else: + return context.fake + + data = { + 'DEFAULT': { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '24', + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin() + self.mock_object(instance.db, 'share_network_update') + self.mock_object(instance.db, 'network_allocation_create') + self.mock_object( + instance.db, 'network_allocations_get_by_ip_address', + mock.Mock(side_effect=fake_get_allocations_by_ip_address)) + + allocations = instance.allocate_network( + ctxt, fake_share_server, fake_share_network, count=2) + + self.assertEqual(2, len(allocations)) + instance.db.share_network_update.assert_called_once_with( + ctxt, fake_share_network['id'], + dict(segmentation_id=None, cidr=instance.net.cidr, ip_version=4)) + instance.db.network_allocations_get_by_ip_address.assert_has_calls( + [mock.call(ctxt, '10.0.0.2'), mock.call(ctxt, '10.0.0.3'), + mock.call(ctxt, '10.0.0.4'), mock.call(ctxt, '10.0.0.5')]) + instance.db.network_allocation_create.assert_has_calls([ + mock.call( + ctxt, + dict(share_server_id=fake_share_server['id'], + ip_address='10.0.0.3', status=constants.STATUS_ACTIVE)), + mock.call( + ctxt, + dict(share_server_id=fake_share_server['id'], + ip_address='10.0.0.5', status=constants.STATUS_ACTIVE)), + ]) + + def test_allocate_network_no_available_ipv4_addresses(self): + data = { + 'DEFAULT': { + 'standalone_network_plugin_gateway': '10.0.0.1', + 'standalone_network_plugin_mask': '30', + }, + } + with test_utils.create_temp_config_with_opts(data): + instance = plugin.StandaloneNetworkPlugin() + self.mock_object(instance.db, 'share_network_update') + self.mock_object(instance.db, 'network_allocation_create') + self.mock_object( + instance.db, 'network_allocations_get_by_ip_address', + mock.Mock(return_value=['not empty list'])) + + self.assertRaises( + exception.NetworkBadConfigurationException, + instance.allocate_network, + fake_context, fake_share_server, fake_share_network) + + instance.db.share_network_update.assert_called_once_with( + fake_context, fake_share_network['id'], + dict(segmentation_id=None, cidr=instance.net.cidr, ip_version=4)) + instance.db.network_allocations_get_by_ip_address.assert_has_calls( + [mock.call(fake_context, '10.0.0.2')]) diff --git a/manila/tests/test_utils.py b/manila/tests/test_utils.py index 877c1aad45..a3d1a1bf8b 100644 --- a/manila/tests/test_utils.py +++ b/manila/tests/test_utils.py @@ -23,6 +23,7 @@ import socket import tempfile import uuid +import ddt import mock from oslo_config import cfg from oslo_utils import timeutils @@ -583,3 +584,46 @@ class CidrToNetmaskTestCase(test.TestCase): def test_cidr_to_netmask_invalid_04(self): cidr = '10.0.0.555/33' self.assertRaises(exception.InvalidInput, utils.cidr_to_netmask, cidr) + + +@ddt.ddt +class IsValidIPVersion(test.TestCase): + """Test suite for function 'is_valid_ip_address'.""" + + @ddt.data('0.0.0.0', '255.255.255.255', '192.168.0.1') + def test_valid_v4(self, addr): + for vers in (4, '4'): + self.assertTrue(utils.is_valid_ip_address(addr, vers)) + + @ddt.data( + '2001:cdba:0000:0000:0000:0000:3257:9652', + '2001:cdba:0:0:0:0:3257:9652', + '2001:cdba::3257:9652') + def test_valid_v6(self, addr): + for vers in (6, '6'): + self.assertTrue(utils.is_valid_ip_address(addr, vers)) + + @ddt.data( + {'addr': '1.1.1.1', 'vers': 3}, + {'addr': '1.1.1.1', 'vers': 5}, + {'addr': '1.1.1.1', 'vers': 7}, + {'addr': '2001:cdba::3257:9652', 'vers': '3'}, + {'addr': '2001:cdba::3257:9652', 'vers': '5'}, + {'addr': '2001:cdba::3257:9652', 'vers': '7'}) + @ddt.unpack + def test_provided_invalid_version(self, addr, vers): + self.assertRaises( + exception.ManilaException, utils.is_valid_ip_address, addr, vers) + + def test_provided_none_version(self): + self.assertRaises(TypeError, utils.is_valid_ip_address, '', None) + + @ddt.data(None, 'fake', '1.1.1.1') + def test_provided_invalid_v6_address(self, addr): + for vers in (6, '6'): + self.assertFalse(utils.is_valid_ip_address(addr, vers)) + + @ddt.data(None, 'fake', '255.255.255.256', '2001:cdba::3257:9652') + def test_provided_invalid_v4_address(self, addr): + for vers in (4, '4'): + self.assertFalse(utils.is_valid_ip_address(addr, vers)) diff --git a/manila/utils.py b/manila/utils.py index 9eadb3f253..fbd6242f3a 100644 --- a/manila/utils.py +++ b/manila/utils.py @@ -494,6 +494,16 @@ def cidr_to_netmask(cidr): raise exception.InvalidInput(_("Invalid cidr supplied %s") % cidr) +def is_valid_ip_address(ip_address, ip_version): + if int(ip_version) == 4: + return netaddr.valid_ipv4(ip_address) + elif int(ip_version) == 6: + return netaddr.valid_ipv6(ip_address) + else: + raise exception.ManilaException( + _("Provided improper IP version '%s'.") % ip_version) + + class IsAMatcher(object): def __init__(self, expected_value=None): self.expected_value = expected_value diff --git a/requirements.txt b/requirements.txt index 11c5aacf2c..9b1470fc20 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ greenlet>=0.3.2 httplib2>=0.7.5 iso8601>=0.1.9 lxml>=2.3 +netaddr>=0.7.12 oslo.config>=1.6.0 # Apache-2.0 oslo.context>=0.1.0 # Apache-2.0 oslo.db>=1.4.1 # Apache-2.0