Add Neutron single network plugin

For share drivers that do support handling of share servers may be useful case
when used different share servers but with shared network. This new plugin
allows to use some specific network and subnet from Neutron for all share
servers within some back-end.

Add two configuration options ('neutron_net_id', 'neutron_subnet_id'), that can
be defined as all other opts for network plugins. For each back-end can be
defined it's own pair of these two options if this new plugin is enabled.

New configuration opts accept only IDs and provided subnet should be assigned
to provided network else error will be raised on start of manila-share service.

If provided share-network has values for net and subnet, they will be
redefined.

Implements blueprint neutron-single-network-plugin
Change-Id: Ia803339078dd25e75848c41d3007a166d94fc309
This commit is contained in:
Valeriy Ponomaryov 2015-01-27 17:31:35 +02:00
parent 13176ec1c9
commit 1d87257f18
6 changed files with 291 additions and 41 deletions

View File

@ -85,10 +85,9 @@ class API(base.Base):
def __init__(self, config_group_name=None):
super(API, self).__init__()
if config_group_name is None:
config_group_name = 'DEFAULT'
CONF.register_opts(neutron_opts, group=config_group_name)
self.configuration = getattr(CONF, config_group_name, CONF)
self.config_group_name = config_group_name or 'DEFAULT'
CONF.register_opts(neutron_opts, group=self.config_group_name)
self.configuration = getattr(CONF, self.config_group_name, CONF)
self.last_neutron_extension_sync = None
self.extensions = {}
self.client = self.get_client(context.get_admin_context())

View File

@ -1,4 +1,5 @@
# Copyright 2013 Openstack Foundation
# Copyright 2015 Mirantis, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -13,6 +14,8 @@
# License for the specific language governing permissions and limitations
# under the License.
from oslo_config import cfg
from manila.common import constants
from manila import exception
from manila import network
@ -20,6 +23,23 @@ from manila.network.neutron import api as neutron_api
from manila.network.neutron import constants as neutron_constants
from manila.openstack.common import log as logging
neutron_single_network_plugin_opts = [
cfg.StrOpt(
'neutron_net_id',
help="Default Neutron network that will be used for share server "
"creation in case it is not provided.",
deprecated_group='DEFAULT',
default=None),
cfg.StrOpt(
'neutron_subnet_id',
help="Default Neutron subnet that will be used for share server "
"creation in case it is not provided. Should be assigned to "
"network defined in opt 'neutron_net_id' if set.",
deprecated_group='DEFAULT',
default=None),
]
CONF = cfg.CONF
LOG = logging.getLogger(__name__)
@ -131,3 +151,45 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI):
self.db.share_network_update(context,
share_network['id'],
subnet_values)
class NeutronSingleNetworkPlugin(NeutronNetworkPlugin):
def __init__(self, *args, **kwargs):
super(NeutronSingleNetworkPlugin, self).__init__(*args, **kwargs)
CONF.register_opts(
neutron_single_network_plugin_opts,
group=self.neutron_api.config_group_name)
self.net = self.neutron_api.configuration.neutron_net_id
self.subnet = self.neutron_api.configuration.neutron_subnet_id
self._verify_net_and_subnet()
def allocate_network(self, context, share_server, share_network, **kwargs):
share_network = self._update_share_network_net_data(
context, share_network)
super(NeutronSingleNetworkPlugin, self).allocate_network(
context, share_server, share_network, **kwargs)
def _verify_net_and_subnet(self):
data = dict(net=self.net, subnet=self.subnet)
if self.net and self.subnet:
net = self.neutron_api.get_network(self.net)
if not (net.get('subnets') and data['subnet'] in net['subnets']):
raise exception.NetworkBadConfigurationException(
"Subnet '%(subnet)s' does not belong to "
"network '%(net)s'." % data)
else:
raise exception.NetworkBadConfigurationException(
"Neutron net and subnet are expected to be both set. "
"Got: net=%(net)s and subnet=%(subnet)s." % data)
def _update_share_network_net_data(self, context, share_network):
upd = dict()
if not share_network.get('neutron_net_id') == self.net:
upd['neutron_net_id'] = self.net
if not share_network.get('neutron_subnet_id') == self.subnet:
upd['neutron_subnet_id'] = self.subnet
if upd:
share_network = self.db.share_network_update(
context, share_network['id'], upd)
return share_network

View File

@ -33,6 +33,7 @@ import manila.exception
import manila.network
import manila.network.linux.interface
import manila.network.neutron.api
import manila.network.neutron.neutron_network_plugin
import manila.openstack.common.eventlet_backdoor
import manila.openstack.common.log
import manila.openstack.common.policy
@ -78,6 +79,8 @@ _global_opt_lists = [
manila.network.linux.interface.OPTS,
manila.network.network_opts,
manila.network.neutron.api.neutron_opts,
manila.network.neutron.neutron_network_plugin.
neutron_single_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,

View File

@ -1,4 +1,5 @@
# Copyright 2013 OpenStack Foundation
# Copyright 2015 Mirantis, Inc.
# All Rights Reserved
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -13,15 +14,21 @@
# License for the specific language governing permissions and limitations
# under the License.
import ddt
import mock
from oslo_config import cfg
from manila.common import constants
from manila import context
from manila.db import api as db_api
from manila import exception
from manila.network.neutron import api as neutron_api
from manila.network.neutron import constants as neutron_constants
from manila.network.neutron import neutron_network_plugin as plugin
from manila import test
from manila.tests import utils as test_utils
CONF = cfg.CONF
fake_neutron_port = {
"status": "test_port_status",
@ -34,35 +41,39 @@ fake_neutron_port = {
"binding:capabilities": {"port_filter": True},
"mac_address": "test_mac",
"fixed_ips": [
{"subnet_id": "test_subnet_id",
"ip_address": "test_ip"}
{"subnet_id": "test_subnet_id", "ip_address": "test_ip"},
],
"id": "test_port_id",
"security_groups": ["fake_sec_group_id"],
"device_id": "fake_device_id"
"device_id": "fake_device_id",
}
fake_share_network = {'id': 'fake nw info id',
'neutron_subnet_id': 'fake subnet id',
'neutron_net_id': 'fake net id',
'project_id': 'fake project id',
'status': 'test_subnet_status',
'name': 'fake name',
'description': 'fake description',
'security_services': []}
fake_share_network = {
'id': 'fake nw info id',
'neutron_subnet_id': 'fake subnet id',
'neutron_net_id': 'fake net id',
'project_id': 'fake project id',
'status': 'test_subnet_status',
'name': 'fake name',
'description': 'fake description',
'security_services': [],
}
fake_share_server = {'id': 'fake nw info id',
'status': 'test_server_status',
'host': 'fake@host',
'network_allocations': [],
'shares': []}
fake_share_server = {
'id': 'fake nw info id',
'status': 'test_server_status',
'host': 'fake@host',
'network_allocations': [],
'shares': [],
}
fake_network_allocation = \
{'id': fake_neutron_port['id'],
'share_server_id': fake_share_server['id'],
'ip_address': fake_neutron_port['fixed_ips'][0]['ip_address'],
'mac_address': fake_neutron_port['mac_address'],
'status': constants.STATUS_ACTIVE}
fake_network_allocation = {
'id': fake_neutron_port['id'],
'share_server_id': fake_share_server['id'],
'ip_address': fake_neutron_port['fixed_ips'][0]['ip_address'],
'mac_address': fake_neutron_port['mac_address'],
'status': constants.STATUS_ACTIVE,
}
class NeutronNetworkPluginTest(test.TestCase):
@ -284,3 +295,141 @@ class NeutronNetworkPluginTest(test.TestCase):
self.plugin.neutron_api.list_extensions.assert_any_call()
self.assertFalse(result)
@ddt.ddt
class NeutronSingleNetworkPluginTest(test.TestCase):
def setUp(self):
super(NeutronSingleNetworkPluginTest, self).setUp()
self.context = 'fake_context'
def test_init_valid(self):
fake_net_id = 'fake_net_id'
fake_subnet_id = 'fake_subnet_id'
config_data = {
'DEFAULT': {
'neutron_net_id': fake_net_id,
'neutron_subnet_id': fake_subnet_id,
}
}
fake_net = {'subnets': ['fake1', 'fake2', fake_subnet_id]}
self.stubs.Set(
neutron_api.API, 'get_network', mock.Mock(return_value=fake_net))
with test_utils.create_temp_config_with_opts(config_data):
instance = plugin.NeutronSingleNetworkPlugin()
self.assertEqual(fake_net_id, instance.net)
self.assertEqual(fake_subnet_id, instance.subnet)
neutron_api.API.get_network.assert_called_once_with(fake_net_id)
@ddt.data(
{'net': None, 'subnet': None},
{'net': 'fake_net_id', 'subnet': None},
{'net': None, 'subnet': 'fake_subnet_id'})
@ddt.unpack
def test_init_invalid(self, net, subnet):
config_data = dict()
# Simulate absence of set values
if net:
config_data['neutron_net_id'] = net
if subnet:
config_data['neutron_subnet_id'] = subnet
config_data = dict(DEFAULT=config_data)
with test_utils.create_temp_config_with_opts(config_data):
self.assertRaises(
exception.NetworkBadConfigurationException,
plugin.NeutronSingleNetworkPlugin)
@ddt.data({}, {'subnets': []}, {'subnets': ['different_foo_subnet']})
def test_init_subnet_does_not_belong_to_net(self, fake_net):
fake_net_id = 'fake_net_id'
config_data = {
'DEFAULT': {
'neutron_net_id': fake_net_id,
'neutron_subnet_id': 'fake_subnet_id',
}
}
self.stubs.Set(
neutron_api.API, 'get_network', mock.Mock(return_value=fake_net))
with test_utils.create_temp_config_with_opts(config_data):
self.assertRaises(
exception.NetworkBadConfigurationException,
plugin.NeutronSingleNetworkPlugin)
neutron_api.API.get_network.assert_called_once_with(fake_net_id)
def _get_neutron_single_network_plugin_instance(self):
fake_subnet_id = 'fake_subnet_id'
config_data = {
'DEFAULT': {
'neutron_net_id': 'fake_net_id',
'neutron_subnet_id': fake_subnet_id,
}
}
fake_net = {'subnets': [fake_subnet_id]}
self.stubs.Set(
neutron_api.API, 'get_network', mock.Mock(return_value=fake_net))
with test_utils.create_temp_config_with_opts(config_data):
instance = plugin.NeutronSingleNetworkPlugin()
return instance
def test___update_share_network_net_data_same_values(self):
instance = self._get_neutron_single_network_plugin_instance()
share_network = {
'neutron_net_id': instance.net,
'neutron_subnet_id': instance.subnet,
}
result = instance._update_share_network_net_data(
self.context, share_network)
self.assertEqual(share_network, result)
@ddt.data(
{'n': 'foo', 's': 'bar'},
{'n': 'fake_net_id', 's': 'bar'},
{'n': 'foo', 's': 'fake_subnet_id'})
@ddt.unpack
def test___update_share_network_net_data_different_values(self, n, s):
instance = self._get_neutron_single_network_plugin_instance()
share_network = {
'id': 'fake_share_network_id',
'neutron_net_id': n,
'neutron_subnet_id': s,
}
self.stubs.Set(
instance.db, 'share_network_update',
mock.Mock(return_value=share_network))
result = instance._update_share_network_net_data(
self.context, share_network)
self.assertEqual(share_network, result)
instance.db.share_network_update.assert_called_once_with(
self.context, share_network['id'], mock.ANY)
@mock.patch.object(
plugin.NeutronNetworkPlugin, "allocate_network", mock.Mock())
def test_allocate_network(self):
instance = self._get_neutron_single_network_plugin_instance()
share_server = 'fake_share_server'
share_network = 'fake_share_network'
share_network_upd = 'updated_fake_share_network'
count = 2
device_owner = 'fake_device_owner'
self.stubs.Set(
instance, '_update_share_network_net_data',
mock.Mock(return_value=share_network_upd))
instance.allocate_network(
self.context, share_server, share_network, count=count,
device_owner=device_owner)
instance._update_share_network_net_data.assert_called_once_with(
self.context, share_network)
plugin.NeutronNetworkPlugin.allocate_network.assert_called_once_with(
self.context, share_server, share_network_upd, count=count,
device_owner=device_owner)

View File

@ -15,7 +15,6 @@
# under the License.
"""Unit tests for the Share driver module."""
import os
import time
import ddt
@ -26,6 +25,7 @@ from manila import network
from manila.share import configuration
from manila.share import driver
from manila import test
from manila.tests import utils as test_utils
from manila import utils
@ -59,18 +59,8 @@ class ShareDriverTestCase(test.TestCase):
execute_mixin._try_execute)
def test_verify_share_driver_mode_option_type(self):
opt_name = 'driver_handles_share_servers'
with utils.tempdir() as tmpdir:
tmpfilename = os.path.join(tmpdir, '%s.conf' % opt_name)
with open(tmpfilename, "w") as configfile:
configfile.write("""[DEFAULT]\n%s = True""" % opt_name)
# Add config file with updated opt
driver.CONF.default_config_files = [configfile.name]
# Reload config instance to use redefined opt
driver.CONF.reload_config_files()
data = {'DEFAULT': {'driver_handles_share_servers': 'True'}}
with test_utils.create_temp_config_with_opts(data):
share_driver = driver.ShareDriver([True, False])
self.assertEqual(True, share_driver.driver_handles_share_servers)

View File

@ -1,4 +1,5 @@
# Copyright 2011 OpenStack LLC
# Copyright 2015 Mirantic, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@ -15,11 +16,16 @@
import os
import manila.context
from oslo_config import cfg
from manila import context
from manila import utils
CONF = cfg.CONF
def get_test_admin_context():
return manila.context.get_admin_context()
return context.get_admin_context()
def is_manila_installed():
@ -27,3 +33,44 @@ def is_manila_installed():
return True
else:
return False
class create_temp_config_with_opts(object):
"""Creates temporary config file with provided opts and values.
usage:
data = {'FOO_GROUP': {'foo_opt': 'foo_value'}}
assert CONF.FOO_GROUP.foo_opt != 'foo_value'
with create_temp_config_with_opts(data):
assert CONF.FOO_GROUP.foo_opt == 'foo_value'
assert CONF.FOO_GROUP.foo_opt != 'foo_value'
:param data: dict -- expected dict with two layers, first is name of
config group and second is opts with values. Example:
{'DEFAULT': {'foo_opt': 'foo_v'}, 'BAR_GROUP': {'bar_opt': 'bar_v'}}
"""
def __init__(self, data):
self.data = data
def __enter__(self):
config_filename = 'fake_config'
with utils.tempdir() as tmpdir:
tmpfilename = os.path.join(tmpdir, '%s.conf' % config_filename)
with open(tmpfilename, "w") as configfile:
for group, opts in self.data.items():
configfile.write("""[%s]\n""" % group)
for opt, value in opts.items():
configfile.write(
"""%(k)s = %(v)s\n""" % {'k': opt, 'v': value})
configfile.write("""\n""")
# Add config file with updated opts
CONF.default_config_files = [configfile.name]
# Reload config instance to use redefined opts
CONF.reload_config_files()
return CONF
def __exit__(self, exc_type, exc_value, exc_traceback):
return False # do not suppress errors