diff --git a/rally-jobs/rally-neutron.yaml b/rally-jobs/rally-neutron.yaml index 9d4c405577..5f361aefbb 100644 --- a/rally-jobs/rally-neutron.yaml +++ b/rally-jobs/rally-neutron.yaml @@ -106,6 +106,9 @@ tenants: 1 users_per_tenant: 1 network: {} + lbaas: + pool: {} + lbaas_version: 1 quotas: neutron: network: -1 diff --git a/rally/plugins/openstack/context/keystone/users.py b/rally/plugins/openstack/context/keystone/users.py index 4fad0c2f42..3e419ae9bd 100644 --- a/rally/plugins/openstack/context/keystone/users.py +++ b/rally/plugins/openstack/context/keystone/users.py @@ -111,7 +111,8 @@ class UserGenerator(context.Context): if consts.Service.NEUTRON not in clients.services().values(): return - use_sg, msg = network.wrap(clients).supports_security_group() + use_sg, msg = network.wrap(clients).supports_extension( + "security-group") if not use_sg: LOG.debug("Security group context is disabled: %s" % msg) return diff --git a/rally/plugins/openstack/context/network/allow_ssh.py b/rally/plugins/openstack/context/network/allow_ssh.py index 3fe4aaf0e8..f9514dc00a 100644 --- a/rally/plugins/openstack/context/network/allow_ssh.py +++ b/rally/plugins/openstack/context/network/allow_ssh.py @@ -96,7 +96,7 @@ class AllowSSH(context.Context): net_wrapper = network.wrap( osclients.Clients(admin_or_user["endpoint"]), self.config) - use_sg, msg = net_wrapper.supports_security_group() + use_sg, msg = net_wrapper.supports_extension("security-group") if not use_sg: LOG.info(_("Security group context is disabled: %s") % msg) return diff --git a/rally/plugins/openstack/context/neutron/__init__.py b/rally/plugins/openstack/context/neutron/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/rally/plugins/openstack/context/neutron/lbaas.py b/rally/plugins/openstack/context/neutron/lbaas.py new file mode 100644 index 0000000000..192170a06e --- /dev/null +++ b/rally/plugins/openstack/context/neutron/lbaas.py @@ -0,0 +1,92 @@ +# 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 six + +from rally.common.i18n import _ +from rally.common import log as logging +from rally.common import utils +from rally import consts +from rally import osclients +from rally.plugins.openstack.wrappers import network as network_wrapper +from rally.task import context + + +LOG = logging.getLogger(__name__) + + +@context.context(name="lbaas", order=360) +class Lbaas(context.Context): + CONFIG_SCHEMA = { + "type": "object", + "$schema": consts.JSON_SCHEMA, + "properties": { + "pool": { + "type": "object" + }, + "lbaas_version": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": False + } + + DEFAULT_CONFIG = { + "pool": { + "lb_method": "ROUND_ROBIN", + "protocol": "HTTP" + }, + "lbaas_version": 1 + } + + @utils.log_task_wrapper(LOG.info, _("Enter context: `lbaas`")) + def setup(self): + net_wrapper = network_wrapper.wrap( + osclients.Clients(self.context["admin"]["endpoint"]), + self.config) + + use_lb, msg = net_wrapper.supports_extension("lbaas") + if not use_lb: + LOG.info(msg) + return + + # Creates a lb-pool for every subnet created in network context. + for user, tenant_id in (utils.iterate_per_tenants( + self.context.get("users", []))): + for network in self.context["tenants"][tenant_id]["networks"]: + for subnet in network.get("subnets", []): + if self.config["lbaas_version"] == 1: + network.setdefault("lb_pools", []).append( + net_wrapper.create_v1_pool( + subnet, + **self.config["pool"])) + else: + raise NotImplementedError( + "Context for LBaaS version %s not implemented." + % self.config["lbaas_version"]) + + @utils.log_task_wrapper(LOG.info, _("Exit context: `lbaas`")) + def cleanup(self): + net_wrapper = network_wrapper.wrap( + osclients.Clients(self.context["admin"]["endpoint"]), + self.config) + for tenant_id, tenant_ctx in six.iteritems(self.context["tenants"]): + for network in tenant_ctx.get("networks", []): + for pool in network.get("lb_pools", []): + with logging.ExceptionLogger( + LOG, + _("Failed to delete pool %(pool)s for tenant " + "%(tenant)s") % {"pool": pool["pool"]["id"], + "tenant": tenant_id}): + if self.config["lbaas_version"] == 1: + net_wrapper.delete_v1_pool(pool["pool"]["id"]) diff --git a/rally/plugins/openstack/wrappers/network.py b/rally/plugins/openstack/wrappers/network.py index edf9bab574..4a49958557 100644 --- a/rally/plugins/openstack/wrappers/network.py +++ b/rally/plugins/openstack/wrappers/network.py @@ -95,8 +95,8 @@ class NetworkWrapper(object): """Delete floating IP.""" @abc.abstractmethod - def supports_security_group(self): - """Checks whether security group is supported.""" + def supports_extension(self): + """Checks whether a network extension is supported.""" class NovaNetworkWrapper(NetworkWrapper): @@ -174,18 +174,25 @@ class NovaNetworkWrapper(NetworkWrapper): fip_id, update_resource=lambda i: self._get_floating_ip(i, do_raise=True)) - def supports_security_group(self): - """Check whether security group is supported + def supports_extension(self, extension): + """Check whether a Nova-network extension is supported - :return: result tuple. Always (True, "") for nova-network. + :param extension: str Nova network extension + :returns: result tuple. Always (True, "") for secgroups in nova-network :rtype: (bool, string) """ - return True, "" + # TODO(rkiran): Add other extensions whenever necessary + if extension == "security-group": + return True, "" + + return False, _("Nova driver does not support %s") % (extension) class NeutronWrapper(NetworkWrapper): SERVICE_IMPL = consts.Service.NEUTRON SUBNET_IP_VERSION = 4 + LB_METHOD = "ROUND_ROBIN" + LB_PROTOCOL = "HTTP" @property def external_networks(self): @@ -227,6 +234,23 @@ class NeutronWrapper(NetworkWrapper): "network_id": net["id"], "enable_snat": True} return self.client.create_router({"router": kwargs})["router"] + def create_v1_pool(self, subnet_id, **kwargs): + """Create LB Pool (v1). + + :param subnet_id: str, neutron subnet-id + :param **kwargs: extra options + :returns: neutron lb-pool dict + """ + pool_args = { + "pool": { + "name": utils.generate_random_name("rally_pool_"), + "subnet_id": subnet_id, + "lb_method": kwargs.get("lb_method", self.LB_METHOD), + "protocol": kwargs.get("protocol", self.LB_PROTOCOL) + } + } + return self.client.create_pool(pool_args) + def _generate_cidr(self): # TODO(amaretskiy): Generate CIDRs unique for network, not cluster return generate_cidr(start_cidr=self.start_cidr) @@ -280,6 +304,13 @@ class NeutronWrapper(NetworkWrapper): "router_id": router and router["id"] or None, "tenant_id": tenant_id} + def delete_v1_pool(self, pool_id): + """Delete LB Pool (v1) + + :param pool_id: str, Lb-Pool-id + """ + self.client.delete_pool(pool_id) + def delete_network(self, network): net_dhcps = self.client.list_dhcp_agent_hosting_networks( network["id"])["agents"] @@ -366,19 +397,18 @@ class NeutronWrapper(NetworkWrapper): """ self.client.delete_floatingip(fip_id) - def supports_security_group(self): - """Check whether security group is supported + def supports_extension(self, extension): + """Check whether a neutron extension is supported + :param extension: str, neutron extension :return: result tuple :rtype: (bool, string) """ extensions = self.client.list_extensions().get("extensions", []) - use_sg = any(ext.get("alias") == "security-group" - for ext in extensions) - if use_sg: + if any(ext.get("alias") == extension for ext in extensions): return True, "" - return False, _("neutron driver does not support security groups") + return False, _("Neutron driver does not support %s") % (extension) def wrap(clients, config=None): diff --git a/tests/unit/plugins/openstack/context/keystone/test_users.py b/tests/unit/plugins/openstack/context/keystone/test_users.py index d3fb3c6408..6a8a6848f8 100644 --- a/tests/unit/plugins/openstack/context/keystone/test_users.py +++ b/tests/unit/plugins/openstack/context/keystone/test_users.py @@ -65,7 +65,7 @@ class UserGeneratorTestCase(test.TestCase): @mock.patch("%s.network.wrap" % CTX) def test__remove_default_security_group_neutron_no_sg(self, mock_wrap): net_wrapper = mock.Mock(SERVICE_IMPL=consts.Service.NEUTRON) - net_wrapper.supports_security_group.return_value = (False, None) + net_wrapper.supports_extension.return_value = (False, None) mock_wrap.return_value = net_wrapper user_generator = users.UserGenerator(self.context) @@ -80,7 +80,8 @@ class UserGeneratorTestCase(test.TestCase): user_generator._remove_default_security_group() mock_wrap.assert_called_once_with(admin_clients) - net_wrapper.supports_security_group.assert_called_once_with() + net_wrapper.supports_extension.assert_called_once_with( + "security-group") @mock.patch("rally.common.utils.iterate_per_tenants") @mock.patch("%s.network" % CTX) @@ -90,7 +91,7 @@ class UserGeneratorTestCase(test.TestCase): self, mock_check_service_status, mock_network, mock_iterate_per_tenants): net_wrapper = mock.Mock(SERVICE_IMPL=consts.Service.NEUTRON) - net_wrapper.supports_security_group.return_value = (True, None) + net_wrapper.supports_extension.return_value = (True, None) mock_network.wrap.return_value = net_wrapper user_generator = users.UserGenerator(self.context) diff --git a/tests/unit/plugins/openstack/context/network/test_allow_ssh.py b/tests/unit/plugins/openstack/context/network/test_allow_ssh.py index d2c76c370e..67e648c755 100644 --- a/tests/unit/plugins/openstack/context/network/test_allow_ssh.py +++ b/tests/unit/plugins/openstack/context/network/test_allow_ssh.py @@ -100,7 +100,7 @@ class AllowSSHContextTestCase(test.TestCase): self, mock_network_wrap, mock__prepare_open_secgroup, mock_clients): mock_network_wrapper = mock.MagicMock() - mock_network_wrapper.supports_security_group.return_value = ( + mock_network_wrapper.supports_extension.return_value = ( True, "") mock_network_wrap.return_value = mock_network_wrapper mock__prepare_open_secgroup.return_value = { @@ -131,7 +131,7 @@ class AllowSSHContextTestCase(test.TestCase): def test_secgroup_setup_with_secgroup_unsupported( self, mock_network_wrap, mock_clients): mock_network_wrapper = mock.MagicMock() - mock_network_wrapper.supports_security_group.return_value = ( + mock_network_wrapper.supports_extension.return_value = ( False, "Not supported") mock_network_wrap.return_value = mock_network_wrapper mock_clients.return_value = mock.MagicMock() diff --git a/tests/unit/plugins/openstack/context/neutron/__init__.py b/tests/unit/plugins/openstack/context/neutron/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/tests/unit/plugins/openstack/context/neutron/test_lbaas.py b/tests/unit/plugins/openstack/context/neutron/test_lbaas.py new file mode 100644 index 0000000000..edd3ae4a7c --- /dev/null +++ b/tests/unit/plugins/openstack/context/neutron/test_lbaas.py @@ -0,0 +1,153 @@ +# 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 rally.plugins.openstack.context.neutron import lbaas as lbaas_context +from tests.unit import test + +NET = "rally.plugins.openstack.wrappers.network." + + +class LbaasTestCase(test.TestCase): + def get_context(self, **kwargs): + foo_tenant = {"networks": [{"id": "foo_net", + "subnets": ["foo_subnet"]}]} + bar_tenant = {"networks": [{"id": "bar_net", + "subnets": ["bar_subnet"]}]} + return {"task": {"uuid": "foo_task"}, + "admin": {"endpoint": "foo_admin"}, + "users": [{"id": "foo_user", "tenant_id": "foo_tenant"}, + {"id": "bar_user", "tenant_id": "bar_tenant"}], + "config": {"lbaas": kwargs}, + "tenants": {"foo_tenant": foo_tenant, + "bar_tenant": bar_tenant}} + + @mock.patch("rally.osclients.Clients") + @mock.patch(NET + "wrap", return_value="foo_service") + def test__init__default(self, mock_wrap, mock_clients): + context = lbaas_context.Lbaas(self.get_context()) + self.assertEqual( + context.config["pool"]["lb_method"], + lbaas_context.Lbaas.DEFAULT_CONFIG["pool"]["lb_method"]) + self.assertEqual( + context.config["pool"]["protocol"], + lbaas_context.Lbaas.DEFAULT_CONFIG["pool"]["protocol"]) + self.assertEqual( + context.config["lbaas_version"], + lbaas_context.Lbaas.DEFAULT_CONFIG["lbaas_version"]) + + @mock.patch("rally.osclients.Clients") + @mock.patch(NET + "wrap", return_value="foo_service") + def test__init__explicit(self, mock_wrap, mock_clients): + context = lbaas_context.Lbaas( + self.get_context(pool={"lb_method": "LEAST_CONNECTIONS"})) + self.assertEqual(context.config["pool"]["lb_method"], + "LEAST_CONNECTIONS") + + @mock.patch(NET + "wrap") + @mock.patch("rally.plugins.openstack.context.neutron.lbaas.utils") + @mock.patch("rally.osclients.Clients") + def test_setup_with_lbaas(self, mock_clients, mock_utils, mock_wrap): + mock_utils.iterate_per_tenants.return_value = [ + ("foo_user", "foo_tenant"), + ("bar_user", "bar_tenant")] + foo_net = {"id": "foo_net", + "subnets": ["foo_subnet"], + "lb_pools": [{"pool": {"id": "foo_pool"}}]} + bar_net = {"id": "bar_net", + "subnets": ["bar_subnet"], + "lb_pools": [{"pool": {"id": "bar_pool"}}]} + expected_net = [bar_net, foo_net] + mock_create = mock.Mock( + side_effect=lambda t, + **kw: {"pool": {"id": str(t.split("_")[0]) + "_pool"}}) + actual_net = [] + mock_wrap.return_value = mock.Mock(create_v1_pool=mock_create) + net_wrapper = mock_wrap(mock_clients.return_value) + net_wrapper.supports_extension.return_value = (True, None) + fake_args = {"lbaas_version": 1} + lb_context = lbaas_context.Lbaas(self.get_context(**fake_args)) + lb_context.setup() + mock_utils.iterate_per_tenants.called_once_with( + lb_context.context["users"]) + net_wrapper.supports_extension.called_once_with("lbaas") + for tenant_id, tenant_ctx in ( + sorted(lb_context.context["tenants"].items())): + for network in tenant_ctx["networks"]: + actual_net.append(network) + self.assertEqual(expected_net, actual_net) + + @mock.patch(NET + "wrap") + @mock.patch("rally.plugins.openstack.context.neutron.lbaas.utils") + @mock.patch("rally.osclients.Clients") + def test_setup_with_no_lbaas(self, mock_clients, mock_utils, mock_wrap): + mock_utils.iterate_per_tenants.return_value = [ + ("bar_user", "bar_tenant")] + mock_create = mock.Mock(side_effect=lambda t, **kw: t + "-net") + mock_wrap.return_value = mock.Mock(create_v1_pool=mock_create) + fake_args = {"lbaas_version": 1} + lb_context = lbaas_context.Lbaas(self.get_context(**fake_args)) + net_wrapper = mock_wrap(mock_clients.return_value) + net_wrapper.supports_extension.return_value = (False, None) + lb_context.setup() + mock_utils.iterate_per_tenants.called_once_with( + lb_context.context["users"]) + net_wrapper.supports_extension.assert_called_once_with("lbaas") + assert not net_wrapper.create_v1_pool.called + + @mock.patch(NET + "wrap") + @mock.patch("rally.plugins.openstack.context.neutron.lbaas.utils") + @mock.patch("rally.osclients.Clients") + def test_setup_with_lbaas_version_not_one(self, mock_clients, + mock_utils, mock_wrap): + mock_utils.iterate_per_tenants.return_value = [ + ("bar_user", "bar_tenant")] + mock_create = mock.Mock(side_effect=lambda t, **kw: t + "-net") + mock_wrap.return_value = mock.Mock(create_v1_pool=mock_create) + fake_args = {"lbaas_version": 2} + lb_context = lbaas_context.Lbaas(self.get_context(**fake_args)) + net_wrapper = mock_wrap(mock_clients.return_value) + net_wrapper.supports_extension.return_value = (True, None) + self.assertRaises(NotImplementedError, lb_context.setup) + + @mock.patch("rally.osclients.Clients") + @mock.patch(NET + "wrap") + def test_cleanup(self, mock_wrap, mock_clients): + net_wrapper = mock_wrap(mock_clients.return_value) + lb_context = lbaas_context.Lbaas(self.get_context()) + expected_pools = [] + for tenant_id, tenant_ctx in lb_context.context["tenants"].items(): + resultant_pool = {"pool": { + "id": str(tenant_id.split("_")[0]) + "_pool"}} + expected_pools.append(resultant_pool) + for network in ( + lb_context.context["tenants"][tenant_id]["networks"]): + network.setdefault("lb_pools", []).append(resultant_pool) + lb_context.cleanup() + net_wrapper.delete_v1_pool.assert_has_calls( + [mock.call(pool["pool"]["id"]) for pool in expected_pools]) + + @mock.patch("rally.osclients.Clients") + @mock.patch(NET + "wrap") + def test_cleanup_lbaas_version_not_one(self, mock_wrap, mock_clients): + fakeargs = {"lbaas_version": 2} + net_wrapper = mock_wrap(mock_clients.return_value) + lb_context = lbaas_context.Lbaas(self.get_context(**fakeargs)) + for tenant_id, tenant_ctx in lb_context.context["tenants"].items(): + resultant_pool = {"pool": { + "id": str(tenant_id.split("_")[0]) + "_pool"}} + for network in ( + lb_context.context["tenants"][tenant_id]["networks"]): + network.setdefault("lb_pools", []).append(resultant_pool) + lb_context.cleanup() + assert not net_wrapper.delete_v1_pool.called diff --git a/tests/unit/plugins/openstack/wrappers/test_network.py b/tests/unit/plugins/openstack/wrappers/test_network.py index d094fe6215..deae8b3ec7 100644 --- a/tests/unit/plugins/openstack/wrappers/test_network.py +++ b/tests/unit/plugins/openstack/wrappers/test_network.py @@ -142,9 +142,10 @@ class NovaNetworkWrapperTestCase(test.TestCase): [mock.call("fip_id", do_raise=True)] * 4, wrap._get_floating_ip.mock_calls) - def test_supports_secgroup(self): + def test_supports_extension(self): wrap = self.get_wrapper() - self.assertTrue(wrap.supports_security_group()[0]) + self.assertFalse(wrap.supports_extension("extension")[0]) + self.assertTrue(wrap.supports_extension("security-group")[0]) class NeutronWrapperTestCase(test.TestCase): @@ -210,6 +211,24 @@ class NeutronWrapperTestCase(test.TestCase): self.assertRaises(network.NetworkWrapperException, wrap.get_network, name="foo_name") + @mock.patch("rally.common.utils.generate_random_name") + def test_create_v1_pool(self, mock_generate_random_name): + mock_generate_random_name.return_value = "foo_name" + subnet = "subnet_id" + service = self.get_wrapper() + expected_pool = {"pool": { + "id": "pool_id", + "name": "foo_name", + "subnet_id": subnet}} + service.client.create_pool.return_value = expected_pool + resultant_pool = service.create_v1_pool(subnet) + service.client.create_pool.assert_called_once_with({ + "pool": {"lb_method": "ROUND_ROBIN", + "subnet_id": subnet, + "protocol": "HTTP", + "name": "foo_name"}}) + self.assertEqual(resultant_pool, expected_pool) + @mock.patch("rally.common.utils.generate_random_name") def test_create_network(self, mock_generate_random_name): mock_generate_random_name.return_value = "foo_name" @@ -345,6 +364,12 @@ class NeutronWrapperTestCase(test.TestCase): self.assertEqual(service.client.delete_subnet.mock_calls, []) service.client.delete_network.assert_called_once_with("foo_id") + def test_delete_v1_pool(self): + service = self.get_wrapper() + pool = {"pool": {"id": "pool-id"}} + service.delete_v1_pool(pool["pool"]["id"]) + service.client.delete_pool.called_once_with([mock.call("pool-id")]) + def test_delete_network_with_dhcp_and_router_and_ports_and_subnets(self): service = self.get_wrapper() agents = ["foo_agent", "bar_agent"] @@ -356,7 +381,8 @@ class NeutronWrapperTestCase(test.TestCase): {"ports": [{"id": port_id} for port_id in ports]}) service.client.delete_network.return_value = "foo_deleted" result = service.delete_network( - {"id": "foo_id", "router_id": "foo_router", "subnets": subnets}) + {"id": "foo_id", "router_id": "foo_router", "subnets": subnets, + "lb_pools": []}) self.assertEqual(result, "foo_deleted") self.assertEqual( service.client.remove_network_from_dhcp_agent.mock_calls, @@ -460,18 +486,18 @@ class NeutronWrapperTestCase(test.TestCase): {"port": {"network_id": "foo_net", "name": "random_name", "foo": "bar"}}) - def test_supports_security_group(self): + def test_supports_extension(self): wrap = self.get_wrapper() wrap.client.list_extensions.return_value = ( - {"extensions": [{"alias": "security-group"}]}) - self.assertTrue(wrap.supports_security_group()[0]) + {"extensions": [{"alias": "extension"}]}) + self.assertTrue(wrap.supports_extension("extension")[0]) wrap.client.list_extensions.return_value = ( - {"extensions": [{"alias": "dummy-group"}]}) - self.assertFalse(wrap.supports_security_group()[0]) + {"extensions": [{"alias": "extension"}]}) + self.assertFalse(wrap.supports_extension("dummy-group")[0]) wrap.client.list_extensions.return_value = {} - self.assertFalse(wrap.supports_security_group()[0]) + self.assertFalse(wrap.supports_extension("extension")[0]) class FunctionsTestCase(test.TestCase):