From 4278e5822db14a66b9f530fa7aa419a0533081ca Mon Sep 17 00:00:00 2001 From: David Ames Date: Wed, 18 Apr 2018 14:40:53 -0700 Subject: [PATCH 1/3] Neutron dynamic routing testing Add the testing required for neutron dynamic routing A.K.A dragent. Create the zaza.charm_tests.dragent module for testing neutron dynamic routing. Create the zaza.configure module for reusable configuration tools. Update utilities to simplify authenticating clients. --- setup.py | 1 + .../test_zaza_charm_lifecycle_deploy.py | 2 +- unit_tests/utilitites/__init__.py | 0 .../test_zaza_utilitites_local_utils.py | 76 ++++++++ .../test_zaza_utilitites_openstack_utils.py | 122 +++++++++--- zaza/charm_lifecycle/deploy.py | 2 +- zaza/charm_tests/dragent/__init__.py | 0 zaza/charm_tests/dragent/configure_dragent.py | 52 +++++ zaza/charm_tests/dragent/test_dragent.py | 99 ++++++++++ zaza/charm_tests/dragent/tests.py | 20 ++ zaza/configure/__init__.py | 0 zaza/configure/bgp_speaker.py | 91 +++++++++ zaza/configure/network.py | 178 ++++++++++++++++++ zaza/utilities/_local_utils.py | 17 +- zaza/utilities/openstack_utils.py | 46 ++++- 15 files changed, 667 insertions(+), 39 deletions(-) create mode 100644 unit_tests/utilitites/__init__.py create mode 100644 unit_tests/utilitites/test_zaza_utilitites_local_utils.py create mode 100644 zaza/charm_tests/dragent/__init__.py create mode 100644 zaza/charm_tests/dragent/configure_dragent.py create mode 100644 zaza/charm_tests/dragent/test_dragent.py create mode 100644 zaza/charm_tests/dragent/tests.py create mode 100644 zaza/configure/__init__.py create mode 100755 zaza/configure/bgp_speaker.py create mode 100755 zaza/configure/network.py diff --git a/setup.py b/setup.py index 95aaf6f..7d20665 100644 --- a/setup.py +++ b/setup.py @@ -8,6 +8,7 @@ from setuptools.command.test import test as TestCommand version = "0.0.1.dev1" install_require = [ + 'async_generator', 'hvac', 'jinja2', 'juju', diff --git a/unit_tests/test_zaza_charm_lifecycle_deploy.py b/unit_tests/test_zaza_charm_lifecycle_deploy.py index 9884e3c..111be4e 100644 --- a/unit_tests/test_zaza_charm_lifecycle_deploy.py +++ b/unit_tests/test_zaza_charm_lifecycle_deploy.py @@ -152,7 +152,7 @@ class TestCharmLifecycleDeploy(ut_utils.BaseTestCase): self.patch_object(lc_deploy.juju_wait, 'wait') lc_deploy.deploy('bun.yaml', 'newmodel') self.deploy_bundle.assert_called_once_with('bun.yaml', 'newmodel') - self.wait.assert_called_once_with() + self.wait.assert_called_once_with(wait_for_workload=True) def test_deploy_nowait(self): self.patch_object(lc_deploy, 'deploy_bundle') diff --git a/unit_tests/utilitites/__init__.py b/unit_tests/utilitites/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/unit_tests/utilitites/test_zaza_utilitites_local_utils.py b/unit_tests/utilitites/test_zaza_utilitites_local_utils.py new file mode 100644 index 0000000..742bdd4 --- /dev/null +++ b/unit_tests/utilitites/test_zaza_utilitites_local_utils.py @@ -0,0 +1,76 @@ +import mock +import unit_tests.utils as ut_utils +from zaza.utilities import _local_utils + + +class TestLocalUtils(ut_utils.BaseTestCase): + + def setUp(self): + super(TestLocalUtils, self).setUp() + + def test_get_yaml_config(self): + self.patch("builtins.open", + new_callable=mock.mock_open(), + name="_open") + _yaml = "data: somedata" + _yaml_dict = {"data": "somedata"} + _filename = "filename" + _fileobj = mock.MagicMock() + _fileobj.read.return_value = _yaml + self._open.return_value = _fileobj + + self.assertEqual(_local_utils.get_yaml_config(_filename), + _yaml_dict) + self._open.assert_called_once_with(_filename, "r") + + def test_get_network_env_vars(self): + self.patch_object(_local_utils.os.environ, "get") + _env = {"NET_ID": "netid", + "NAMESERVER": "10.0.0.10", + "GATEWAY": "10.0.0.1", + "CIDR_EXT": "10.0.0.0/24", + "CIDR_PRIV": "192.168.0.0/24"} + _result = {} + _result["net_id"] = _env["NET_ID"] + _result["external_dns"] = _env["NAMESERVER"] + _result["default_gateway"] = _env["GATEWAY"] + _result["external_net_cidr"] = _env["CIDR_EXT"] + _result["private_net_cidr"] = _env["CIDR_PRIV"] + + def _get_env(key): + return _env.get(key) + self.get.side_effect = _get_env + + self.assertEqual(_local_utils.get_network_env_vars(), + _result) + + def test_get_net_info(self): + self.patch_object(_local_utils.os.path, "exists") + self.patch_object(_local_utils, "get_yaml_config") + self.patch_object(_local_utils, "get_network_env_vars") + net_topology = "topo" + _data = {net_topology: {"network": "DATA"}} + self.get_yaml_config.return_value = _data + + # YAML file does not exist + self.exists.return_value = False + with self.assertRaises(Exception): + _local_utils.get_net_info(net_topology) + + # No environmental variables + self.exists.return_value = True + self.assertEqual( + _local_utils.get_net_info(net_topology, ignore_env_vars=True), + _data[net_topology]) + self.get_yaml_config.assert_called_once_with("network.yaml") + self.get_network_env_vars.assert_not_called() + + # Update with environmental variables + _more_data = {"network": "NEW", + "other": "DATA"} + self.get_network_env_vars.return_value = _more_data + _data[net_topology].update(_more_data) + self.assertEqual( + _local_utils.get_net_info(net_topology), + _data[net_topology]) + self.get_network_env_vars.assert_called_once_with() diff --git a/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py b/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py index 29e1685..7434b0e 100644 --- a/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py +++ b/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py @@ -1,3 +1,4 @@ +import copy import mock import unit_tests.utils as ut_utils from zaza.utilities import openstack_utils @@ -7,63 +8,130 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): def setUp(self): super(TestOpenStackUtils, self).setUp() - self.port_name = 'port_name' - self.net_uuid = 'uuid' + self.port_name = "port_name" + self.net_uuid = "net_uuid" + self.project_id = "project_uuid" self.port = { - 'port': {'id': 'port_id', - 'name': self.port_name, - 'network_id': self.net_uuid}} - - self.ports = {'ports': [self.port['port']]} + "port": {"id": "port_id", + "name": self.port_name, + "network_id": self.net_uuid}} + self.ports = {"ports": [self.port["port"]]} self.floatingip = { - 'floatingip': {'id': 'floatingip_id', - 'floating_network_id': self.net_uuid, - 'port_id': 'port_id'}} - - self.floatingips = {'floatingips': [self.floatingip['floatingip']]} - + "floatingip": {"id": "floatingip_id", + "floating_network_id": self.net_uuid, + "port_id": "port_id"}} + self.floatingips = {"floatingips": [self.floatingip["floatingip"]]} + self.address_scope_name = "address_scope_name" + self.address_scope = { + "address_scope": {"id": "address_scope_id", + "name": self.address_scope_name, + "shared": True, + "ip_version": 4, + "tenant_id": self.project_id}} + self.address_scopes = { + "address_scopes": [self.address_scope["address_scope"]]} self.neutronclient = mock.MagicMock() self.neutronclient.list_ports.return_value = self.ports self.neutronclient.create_port.return_value = self.port self.neutronclient.list_floatingips.return_value = self.floatingips self.neutronclient.create_floatingip.return_value = self.floatingip - self.ext_net = 'ext_net' - self.private_net = 'private_net' + + self.neutronclient.list_address_scopes.return_value = ( + self.address_scopes) + self.neutronclient.create_address_scope.return_value = ( + self.address_scope) + self.ext_net = "ext_net" + self.private_net = "private_net" def test_create_port(self): - self.patch_object(openstack_utils, 'get_net_uuid') + self.patch_object(openstack_utils, "get_net_uuid") self.get_net_uuid.return_value = self.net_uuid # Already exists port = openstack_utils.create_port( self.neutronclient, self.port_name, self.private_net) - self.assertEqual(port, self.port['port']) + self.assertEqual(port, self.port["port"]) self.neutronclient.create_port.assert_not_called() # Does not yet exist - self.neutronclient.list_ports.return_value = {'ports': []} - self.port['port'].pop('id') + self.neutronclient.list_ports.return_value = {"ports": []} + self.port["port"].pop("id") port = openstack_utils.create_port( self.neutronclient, self.port_name, self.private_net) - self.assertEqual(port, self.port['port']) + self.assertEqual(port, self.port["port"]) self.neutronclient.create_port.assert_called_once_with(self.port) def test_create_floating_ip(self): - self.patch_object(openstack_utils, 'get_net_uuid') + self.patch_object(openstack_utils, "get_net_uuid") self.get_net_uuid.return_value = self.net_uuid # Already exists floatingip = openstack_utils.create_floating_ip( - self.neutronclient, self.ext_net, port=self.port['port']) - self.assertEqual(floatingip, self.floatingip['floatingip']) + self.neutronclient, self.ext_net, port=self.port["port"]) + self.assertEqual(floatingip, self.floatingip["floatingip"]) self.neutronclient.create_floatingip.assert_not_called() # Does not yet exist - self.neutronclient.list_floatingips.return_value = {'floatingips': []} - self.floatingip['floatingip'].pop('id') + self.neutronclient.list_floatingips.return_value = {"floatingips": []} + self.floatingip["floatingip"].pop("id") floatingip = openstack_utils.create_floating_ip( - self.neutronclient, self.private_net, port=self.port['port']) - self.assertEqual(floatingip, self.floatingip['floatingip']) + self.neutronclient, self.private_net, port=self.port["port"]) + self.assertEqual(floatingip, self.floatingip["floatingip"]) self.neutronclient.create_floatingip.assert_called_once_with( self.floatingip) + + def test_create_address_scope(self): + self.patch_object(openstack_utils, "get_net_uuid") + self.get_net_uuid.return_value = self.net_uuid + + # Already exists + address_scope = openstack_utils.create_address_scope( + self.neutronclient, self.project_id, self.address_scope_name) + self.assertEqual(address_scope, self.address_scope["address_scope"]) + self.neutronclient.create_address_scope.assert_not_called() + + # Does not yet exist + self.neutronclient.list_address_scopes.return_value = { + "address_scopes": []} + address_scope_msg = copy.deepcopy(self.address_scope) + address_scope_msg["address_scope"].pop("id") + address_scope = openstack_utils.create_address_scope( + self.neutronclient, self.project_id, self.address_scope_name) + self.assertEqual(address_scope, self.address_scope["address_scope"]) + self.neutronclient.create_address_scope.assert_called_once_with( + address_scope_msg) + + def test_get_keystone_scope(self): + self.patch_object(openstack_utils, "get_current_os_versions") + + # <= Liberty + self.get_current_os_versions.return_value = {"keystone": "liberty"} + self.assertEqual(openstack_utils.get_keystone_scope(), "DOMAIN") + # > Liberty + self.get_current_os_versions.return_value = {"keystone": "mitaka"} + self.assertEqual(openstack_utils.get_keystone_scope(), "PROJECT") + + def test_get_overcloud_keystone_session(self): + self.patch_object(openstack_utils, "get_keystone_session") + self.patch_object(openstack_utils, "get_keystone_scope") + self.patch_object(openstack_utils, "get_overcloud_auth") + _auth = "FAKE_AUTH" + _scope = "PROJECT" + self.get_keystone_scope.return_value = _scope + self.get_overcloud_auth.return_value = _auth + + openstack_utils.get_overcloud_keystone_session() + self.get_keystone_session.assert_called_once_with(_auth, scope=_scope) + + def test_get_undercloud_keystone_session(self): + self.patch_object(openstack_utils, "get_keystone_session") + self.patch_object(openstack_utils, "get_keystone_scope") + self.patch_object(openstack_utils, "get_undercloud_auth") + _auth = "FAKE_AUTH" + _scope = "PROJECT" + self.get_keystone_scope.return_value = _scope + self.get_undercloud_auth.return_value = _auth + + openstack_utils.get_undercloud_keystone_session() + self.get_keystone_session.assert_called_once_with(_auth, scope=_scope) diff --git a/zaza/charm_lifecycle/deploy.py b/zaza/charm_lifecycle/deploy.py index c87aad3..ab49bcb 100755 --- a/zaza/charm_lifecycle/deploy.py +++ b/zaza/charm_lifecycle/deploy.py @@ -224,7 +224,7 @@ def deploy(bundle, model, wait=True): if wait: logging.info("Waiting for environment to settle") utils.set_juju_model(model) - juju_wait.wait() + juju_wait.wait(wait_for_workload=True) def parse_args(args): diff --git a/zaza/charm_tests/dragent/__init__.py b/zaza/charm_tests/dragent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zaza/charm_tests/dragent/configure_dragent.py b/zaza/charm_tests/dragent/configure_dragent.py new file mode 100644 index 0000000..f22df00 --- /dev/null +++ b/zaza/charm_tests/dragent/configure_dragent.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 + +import os + +from zaza.configure import ( + network, + bgp_speaker, +) +from zaza.utilities import ( + _local_utils, + openstack_utils, +) + +DEFAULT_PEER_APPLICATION_NAME = "quagga" +# Find a default network.yaml in tests/bundles/network.yaml of the charm +DEFAULT_TOPOLOGY_FILE = os.path.join("tests", "bundles", "network.yaml") + + +def setup(net_topology="pool", net_topology_file="network.yaml"): + """Setup BGP networking + + :param net_topology: String name of network topology + :type net_topology: string + :param net_info: Network configuration dictionary + :type net_info: dict + :param keystone_session: Keystone session object for overcloud + :type keystone_session: keystoneauth1.session.Session object + :returns: None + :rtype: None + """ + + _local_utils.setup_logging() + + # Check for the network.yaml configuration + if (not os.path.exists(net_topology_file) and + os.path.exists(DEFAULT_TOPOLOGY_FILE)): + net_topology_file = DEFAULT_TOPOLOGY_FILE + + # Get network configuration from YAML and/or environment variables + net_info = _local_utils.get_net_info(net_topology, + net_topology_file=net_topology_file) + + # Get keystone session + keystone_session = openstack_utils.get_overcloud_keystone_session() + + # Confugre the network + network.setup_sdn( + net_topology, net_info, keystone_session=keystone_session) + # Configure BGP + bgp_speaker.setup_bgp_speaker( + peer_application_name=DEFAULT_PEER_APPLICATION_NAME, + keystone_session=keystone_session) diff --git a/zaza/charm_tests/dragent/test_dragent.py b/zaza/charm_tests/dragent/test_dragent.py new file mode 100644 index 0000000..b56e8c6 --- /dev/null +++ b/zaza/charm_tests/dragent/test_dragent.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import time +import sys + +from zaza import model +from zaza.charm_lifecycle import utils as lifecycle_utils +from zaza.utilities import ( + _local_utils, + openstack_utils, +) + + +def test_bgp_routes(peer_application_name="quagga", keystone_session=None): + """Test BGP routes + + :param peer_application_name: String name of BGP peer application + :type peer_application_name: string + :param keystone_session: Keystone session object for overcloud + :type keystone_session: keystoneauth1.session.Session object + :raises: AssertionError if expected BGP routes are not found + :returns: None + :rtype: None + """ + + # If a session has not been provided, acquire one + if not keystone_session: + keystone_session = openstack_utils.get_overcloud_keystone_session() + + # Get authenticated clients + neutron_client = openstack_utils.get_neutron_session_client( + keystone_session) + + # Get the peer unit + peer_unit = model.get_units( + lifecycle_utils.get_juju_model(), peer_application_name)[0] + + # Get expected advertised routes + private_cidr = neutron_client.list_subnets( + name="private_subnet")["subnets"][0]["cidr"] + floating_ip_cidr = "{}/32".format( + neutron_client.list_floatingips() + ["floatingips"][0]["floating_ip_address"]) + + # This test may run immediately after configuration. It may take time for + # routes to propogate via BGP. Do a binary backoff up to ~2 minutes long. + backoff = 2 + max_wait = 129 + logging.info("Checking routes on BGP peer {}".format(peer_unit.entity_id)) + while backoff < max_wait: + # Run show ip route bgp on BGP peer + routes = _local_utils.remote_run( + peer_unit.entity_id, remote_cmd='vtysh -c "show ip route bgp"') + try: + logging.debug(routes) + assert private_cidr in routes, ( + "Private subnet CIDR, {}, not advertised to BGP peer" + .format(private_cidr)) + logging.info("Private subnet CIDR, {}, found in routing table" + .format(private_cidr)) + break + except AssertionError: + logging.info("Binary backoff waiting {} seconds for bgp " + "routes on peer".format(backoff)) + time.sleep(backoff) + backoff = backoff * 2 + if backoff > max_wait: + raise + + assert floating_ip_cidr in routes, ("Floating IP, {}, not advertised " + "to BGP peer".format(floating_ip_cidr)) + logging.info("Floating IP CIDR, {}, found in routing table" + .format(floating_ip_cidr)) + + +def run_from_cli(): + """Run test for BGP routes from CLI + + :returns: None + :rtype: None + """ + + _local_utils.setup_logging() + parser = argparse.ArgumentParser() + parser.add_argument("--peer-application", "-a", + help="BGP Peer application name. Default: quagga", + default="quagga") + options = parser.parse_args() + + peer_application_name = _local_utils.parse_arg(options, + "peer_application") + + test_bgp_routes(peer_application_name) + + +if __name__ == "__main__": + sys.exit(run_from_cli()) diff --git a/zaza/charm_tests/dragent/tests.py b/zaza/charm_tests/dragent/tests.py new file mode 100644 index 0000000..baffcf8 --- /dev/null +++ b/zaza/charm_tests/dragent/tests.py @@ -0,0 +1,20 @@ +#!/usr/bin/env python3 + +import unittest + +from zaza.utilities import _local_utils +from zaza.charm_tests.dragent import test_dragent + + +class DRAgentTest(unittest.TestCase): + + @classmethod + def setUpClass(cls): + _local_utils.setup_logging() + + def test_bgp_routes(self): + test_dragent.test_bgp_routes(peer_application_name="quagga") + + +if __name__ == "__main__": + unittest.main() diff --git a/zaza/configure/__init__.py b/zaza/configure/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/zaza/configure/bgp_speaker.py b/zaza/configure/bgp_speaker.py new file mode 100755 index 0000000..acff045 --- /dev/null +++ b/zaza/configure/bgp_speaker.py @@ -0,0 +1,91 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import sys +from zaza.utilities import _local_utils +from zaza.utilities import openstack_utils + + +EXT_NET = "ext_net" +PRIVATE_NET = "private" +FIP_TEST = "FIP TEST" + + +def setup_bgp_speaker(peer_application_name, keystone_session=None): + """Setup BGP Speaker + + :param peer_application_name: String name of BGP peer application + :type peer_application_name: string + :param keystone_session: Keystone session object for overcloud + :type keystone_session: keystoneauth1.session.Session object + :returns: None + :rtype: None + """ + + # If a session has not been provided, acquire one + if not keystone_session: + keystone_session = openstack_utils.get_overcloud_keystone_session() + + # Get authenticated clients + neutron_client = openstack_utils.get_neutron_session_client( + keystone_session) + + # Create BGP speaker + logging.info("Setting up BGP speaker") + bgp_speaker = openstack_utils.create_bgp_speaker( + neutron_client, local_as=12345) + + # Add networks to bgp speaker + logging.info("Advertising BGP routes") + openstack_utils.add_network_to_bgp_speaker( + neutron_client, bgp_speaker, EXT_NET) + openstack_utils.add_network_to_bgp_speaker( + neutron_client, bgp_speaker, PRIVATE_NET) + logging.debug("Advertised routes: {}" + .format( + neutron_client.list_route_advertised_from_bgp_speaker( + bgp_speaker["id"]))) + + # Create peer + logging.info("Setting up BGP peer") + bgp_peer = openstack_utils.create_bgp_peer(neutron_client, + peer_application_name, + remote_as=10000) + # Add peer to bgp speaker + logging.info("Adding BGP peer to BGP speaker") + openstack_utils.add_peer_to_bgp_speaker( + neutron_client, bgp_speaker, bgp_peer) + + # Create Floating IP to advertise + logging.info("Creating floating IP to advertise") + port = openstack_utils.create_port(neutron_client, FIP_TEST, PRIVATE_NET) + floating_ip = openstack_utils.create_floating_ip(neutron_client, EXT_NET, + port=port) + logging.info( + "Advertised floating IP: {}".format( + floating_ip["floating_ip_address"])) + + +def run_from_cli(): + """Run BGP Speaker setup from CLI + + :returns: None + :rtype: None + """ + + _local_utils.setup_logging() + parser = argparse.ArgumentParser() + parser.add_argument("--peer-application", "-a", + help="BGP peer application name. Default: quagga", + default="quagga") + + options = parser.parse_args() + peer_application_name = _local_utils.parse_arg(options, + "peer_application") + + setup_bgp_speaker(peer_application_name) + + +if __name__ == "__main__": + sys.exit(run_from_cli()) diff --git a/zaza/configure/network.py b/zaza/configure/network.py new file mode 100755 index 0000000..8009955 --- /dev/null +++ b/zaza/configure/network.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python3 + +import argparse +import logging +import sys + +from zaza.utilities import _local_utils +from zaza.utilities import openstack_utils + + +def setup_sdn(net_topology, net_info, keystone_session=None): + """Setup Software Defined Network + + :param net_topology: String name of network topology + :type net_topology: string + :param net_info: Network configuration dictionary + :type net_info: dict + :param keystone_session: Keystone session object for overcloud + :type keystone_session: keystoneauth1.session.Session object + :returns: None + :rtype: None + """ + + # If a session has not been provided, acquire one + if not keystone_session: + keystone_session = openstack_utils.get_overcloud_keystone_session() + + # Get authenticated clients + keystone_client = openstack_utils.get_keystone_session_client( + keystone_session) + neutron_client = openstack_utils.get_neutron_session_client( + keystone_session) + + # Resolve the project name from the overcloud opentackrc into a project id + project_id = openstack_utils.get_project_id( + keystone_client, + "admin", + ) + # Network Setup + subnetpools = False + if net_info.get("subnetpool_prefix"): + subnetpools = True + + logging.info("Configuring overcloud network") + # Create the external network + ext_network = openstack_utils.create_external_network( + neutron_client, + project_id, + net_info.get("dvr_enabled", False), + net_info["external_net_name"]) + openstack_utils.create_external_subnet( + neutron_client, + project_id, + ext_network, + net_info["default_gateway"], + net_info["external_net_cidr"], + net_info["start_floating_ip"], + net_info["end_floating_ip"], + net_info["external_subnet_name"]) + # Should this be --enable_snat = False + provider_router = ( + openstack_utils.create_provider_router(neutron_client, project_id)) + openstack_utils.plug_extnet_into_router( + neutron_client, + provider_router, + ext_network) + ip_version = net_info.get("ip_version") or 4 + subnetpool = None + if subnetpools: + address_scope = openstack_utils.create_address_scope( + neutron_client, + project_id, + net_info.get("address_scope"), + ip_version=ip_version) + subnetpool = openstack_utils.create_subnetpool( + neutron_client, + project_id, + net_info.get("subnetpool_name"), + net_info.get("subnetpool_prefix"), + address_scope) + project_network = openstack_utils.create_project_network( + neutron_client, + project_id, + shared=False, + network_type=net_info["network_type"]) + project_subnet = openstack_utils.create_project_subnet( + neutron_client, + project_id, + project_network, + net_info.get("private_net_cidr"), + subnetpool=subnetpool, + ip_version=ip_version) + openstack_utils.update_subnet_dns( + neutron_client, + project_subnet, + net_info["external_dns"]) + openstack_utils.plug_subnet_into_router( + neutron_client, + net_info["router_name"], + project_network, + project_subnet) + openstack_utils.add_neutron_secgroup_rules(neutron_client, project_id) + + +def setup_gateway_ext_port(net_info, keystone_session=None): + """Setup external port on Neutron Gateway. + For OpenStack on OpenStack scenarios + + :param net_info: Network configuration dictionary + :type net_info: dict + :param keystone_session: Keystone session object for undercloud + :type keystone_session: keystoneauth1.session.Session object + :returns: None + :rtype: None + """ + + # If a session has not been provided, acquire one + if not keystone_session: + keystone_session = openstack_utils.get_undercloud_keystone_session() + + # Get authenticated clients + nova_client = openstack_utils.get_nova_session_client(keystone_session) + neutron_client = openstack_utils.get_neutron_session_client( + keystone_session) + + # Add an interface to the neutron-gateway units and tell juju to use it + # as the external port. + if "net_id" in net_info.keys(): + net_id = net_info["net_id"] + else: + net_id = None + + logging.info("Configuring network for OpenStack undercloud/provider") + openstack_utils.configure_gateway_ext_port( + nova_client, + neutron_client, + dvr_mode=net_info.get("dvr_enabled", False), + net_id=net_id) + + +def run_from_cli(): + """Run network configurations from CLI + + :returns: None + :rtype: None + """ + + _local_utils.setup_logging() + parser = argparse.ArgumentParser() + parser.add_argument("net_topology", + help="network topology type, default is GRE", + default="gre", nargs="?") + parser.add_argument("--ignore_env_vars", "-i", + help="do not override using environment variables", + action="store_true", + default=False) + parser.add_argument("--net_topology_file", "-f", + help="Network topology file location", + default="network.yaml") + # Handle CLI options + options = parser.parse_args() + net_topology = _local_utils.parse_arg(options, "net_topology") + net_topology_file = _local_utils.parse_arg(options, "net_topology_file") + ignore_env_vars = _local_utils.parse_arg(options, "ignore_env_vars") + + logging.info("Setting up %s network" % (net_topology)) + net_info = _local_utils.get_net_info( + net_topology, ignore_env_vars, net_topology_file) + + # Handle network for Openstack-on-Openstack scenarios + if _local_utils.get_provider_type() == "openstack": + setup_gateway_ext_port(net_info) + + setup_sdn(net_topology, net_info) + + +if __name__ == "__main__": + sys.exit(run_from_cli()) diff --git a/zaza/utilities/_local_utils.py b/zaza/utilities/_local_utils.py index 3a4a1b6..77ac5f3 100644 --- a/zaza/utilities/_local_utils.py +++ b/zaza/utilities/_local_utils.py @@ -112,7 +112,8 @@ def get_yaml_config(config_file): return yaml.load(open(config_file, 'r').read()) -def get_net_info(net_topology, ignore_env_vars=False): +def get_net_info(net_topology, ignore_env_vars=False, + net_topology_file="network.yaml"): """Get network info from network.yaml, override the values if specific environment variables are set. @@ -124,13 +125,17 @@ def get_net_info(net_topology, ignore_env_vars=False): :rtype: dict """ - net_info = get_yaml_config('network.yaml')[net_topology] + if os.path.exists(net_topology_file): + net_info = get_yaml_config(net_topology_file)[net_topology] + else: + raise Exception("Network topology file: {} not found." + .format(net_topology_file)) if not ignore_env_vars: - logging.info('Consuming network environment variables as overrides.') + logging.info("Consuming network environment variables as overrides.") net_info.update(get_network_env_vars()) - logging.info('Network info: {}'.format(dict_to_yaml(net_info))) + logging.info("Network info: {}".format(dict_to_yaml(net_info))) return net_info @@ -173,8 +178,8 @@ def remote_run(unit, remote_cmd, timeout=None, fatal=None): """ if fatal is None: fatal = True - result = model.run_on_unit(unit, - lifecycle_utils.get_juju_model(), + result = model.run_on_unit(lifecycle_utils.get_juju_model(), + unit, remote_cmd, timeout=timeout) if result: diff --git a/zaza/utilities/openstack_utils.py b/zaza/utilities/openstack_utils.py index c381e87..1691f92 100644 --- a/zaza/utilities/openstack_utils.py +++ b/zaza/utilities/openstack_utils.py @@ -139,6 +139,23 @@ def get_neutron_session_client(session): return neutronclient.Client(session=session) +def get_keystone_scope(): + """Return Keystone scope based on OpenStack release + + :returns: String keystone scope + :rtype: string + """ + + os_version = get_current_os_versions("keystone")["keystone"] + # Keystone policy.json shipped the charm with liberty requires a domain + # scoped token. Bug #1649106 + if os_version == "liberty": + scope = "DOMAIN" + else: + scope = "PROJECT" + return scope + + def get_keystone_session(opentackrc_creds, insecure=True, scope='PROJECT'): """Return keystone session @@ -160,6 +177,28 @@ def get_keystone_session(opentackrc_creds, insecure=True, scope='PROJECT'): return session.Session(auth=auth, verify=not insecure) +def get_overcloud_keystone_session(): + """Return Over cloud keystone session + + :returns keystone_session: keystoneauth1.session.Session object + :rtype: keystoneauth1.session.Session + """ + + return get_keystone_session(get_overcloud_auth(), + scope=get_keystone_scope()) + + +def get_undercloud_keystone_session(): + """Return Under cloud keystone session + + :returns keystone_session: keystoneauth1.session.Session object + :rtype: keystoneauth1.session.Session + """ + + return get_keystone_session(get_undercloud_auth(), + scope=get_keystone_scope()) + + def get_keystone_session_client(session): """Return keystoneclient authenticated by keystone session @@ -377,7 +416,7 @@ def configure_gateway_ext_port(novaclient, neutronclient, model.set_application_config( lifecycle_utils.get_juju_model(), application_name, configuration={config_key: ext_br_macs_str}) - juju_wait.wait() + juju_wait.wait(wait_for_workload=True) def create_project_network(neutron_client, project_id, net_name='private', @@ -444,10 +483,9 @@ def create_external_network(neutron_client, project_id, dvr_mode, 'name': net_name, 'router:external': True, 'tenant_id': project_id, + 'provider:physical_network': 'physnet1', + 'provider:network_type': 'flat', } - if not deprecated_external_networking(dvr_mode): - network_msg['provider:physical_network'] = 'physnet1' - network_msg['provider:network_type'] = 'flat' logging.info('Creating new external network definition: %s', net_name) From 6ad3745206086e3d4095cefc7cfbe3eb30959cdd Mon Sep 17 00:00:00 2001 From: David Ames Date: Thu, 19 Apr 2018 09:45:50 -0700 Subject: [PATCH 2/3] Separate overcloud and undercloud settings Changed the name of configure_dragent Removed the network.yaml file requirement for tests Made a bright line distinction between declared overcloud network settings and environment specific undercloud settings The tests will declare the overcloud settings and acquire the undercloud settings from environment variables. --- .../test_zaza_utilitites_local_utils.py | 48 +++--- .../test_zaza_utilitites_openstack_utils.py | 40 ++++- zaza/charm_tests/dragent/configure.py | 76 ++++++++++ zaza/charm_tests/dragent/configure_dragent.py | 52 ------- .../dragent/{test_dragent.py => test.py} | 0 zaza/charm_tests/dragent/tests.py | 6 +- zaza/configure/network.py | 140 ++++++++++++++---- zaza/utilities/_local_utils.py | 75 +++++----- zaza/utilities/openstack_utils.py | 4 - 9 files changed, 295 insertions(+), 146 deletions(-) create mode 100644 zaza/charm_tests/dragent/configure.py delete mode 100644 zaza/charm_tests/dragent/configure_dragent.py rename zaza/charm_tests/dragent/{test_dragent.py => test.py} (100%) diff --git a/unit_tests/utilitites/test_zaza_utilitites_local_utils.py b/unit_tests/utilitites/test_zaza_utilitites_local_utils.py index 742bdd4..24152a3 100644 --- a/unit_tests/utilitites/test_zaza_utilitites_local_utils.py +++ b/unit_tests/utilitites/test_zaza_utilitites_local_utils.py @@ -23,31 +23,43 @@ class TestLocalUtils(ut_utils.BaseTestCase): _yaml_dict) self._open.assert_called_once_with(_filename, "r") - def test_get_network_env_vars(self): + def test_get_undercloud_env_vars(self): self.patch_object(_local_utils.os.environ, "get") - _env = {"NET_ID": "netid", - "NAMESERVER": "10.0.0.10", - "GATEWAY": "10.0.0.1", - "CIDR_EXT": "10.0.0.0/24", - "CIDR_PRIV": "192.168.0.0/24"} - _result = {} - _result["net_id"] = _env["NET_ID"] - _result["external_dns"] = _env["NAMESERVER"] - _result["default_gateway"] = _env["GATEWAY"] - _result["external_net_cidr"] = _env["CIDR_EXT"] - _result["private_net_cidr"] = _env["CIDR_PRIV"] def _get_env(key): return _env.get(key) self.get.side_effect = _get_env - self.assertEqual(_local_utils.get_network_env_vars(), - _result) + # OSCI backward compatible env vars + _env = {"NET_ID": "netid", + "NAMESERVER": "10.0.0.10", + "GATEWAY": "10.0.0.1", + "CIDR_EXT": "10.0.0.0/24", + "FIP_RANGE": "10.0.200.0:10.0.200.254"} + _expected_result = {} + _expected_result["net_id"] = _env["NET_ID"] + _expected_result["external_dns"] = _env["NAMESERVER"] + _expected_result["default_gateway"] = _env["GATEWAY"] + _expected_result["external_net_cidr"] = _env["CIDR_EXT"] + _expected_result["start_floating_ip"] = _env["FIP_RANGE"].split(":")[0] + _expected_result["end_floating_ip"] = _env["FIP_RANGE"].split(":")[1] + self.assertEqual(_local_utils.get_undercloud_env_vars(), + _expected_result) + + # Overriding configure.network named variables + _override = {"start_floating_ip": "10.100.50.0", + "end_floating_ip": "10.100.50.254", + "default_gateway": "10.100.0.1", + "external_net_cidr": "10.100.0.0/16"} + _env.update(_override) + _expected_result.update(_override) + self.assertEqual(_local_utils.get_undercloud_env_vars(), + _expected_result) def test_get_net_info(self): self.patch_object(_local_utils.os.path, "exists") self.patch_object(_local_utils, "get_yaml_config") - self.patch_object(_local_utils, "get_network_env_vars") + self.patch_object(_local_utils, "get_undercloud_env_vars") net_topology = "topo" _data = {net_topology: {"network": "DATA"}} self.get_yaml_config.return_value = _data @@ -63,14 +75,14 @@ class TestLocalUtils(ut_utils.BaseTestCase): _local_utils.get_net_info(net_topology, ignore_env_vars=True), _data[net_topology]) self.get_yaml_config.assert_called_once_with("network.yaml") - self.get_network_env_vars.assert_not_called() + self.get_undercloud_env_vars.assert_not_called() # Update with environmental variables _more_data = {"network": "NEW", "other": "DATA"} - self.get_network_env_vars.return_value = _more_data + self.get_undercloud_env_vars.return_value = _more_data _data[net_topology].update(_more_data) self.assertEqual( _local_utils.get_net_info(net_topology), _data[net_topology]) - self.get_network_env_vars.assert_called_once_with() + self.get_undercloud_env_vars.assert_called_once_with() diff --git a/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py b/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py index 7434b0e..b3b9989 100644 --- a/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py +++ b/unit_tests/utilitites/test_zaza_utilitites_openstack_utils.py @@ -11,6 +11,8 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.port_name = "port_name" self.net_uuid = "net_uuid" self.project_id = "project_uuid" + self.ext_net = "ext_net" + self.private_net = "private_net" self.port = { "port": {"id": "port_id", "name": self.port_name, @@ -30,6 +32,18 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): "tenant_id": self.project_id}} self.address_scopes = { "address_scopes": [self.address_scope["address_scope"]]} + + self.network = { + "network": {"id": "network_id", + "name": self.ext_net, + "tenant_id": self.project_id, + "router:external": True, + "provider:physical_network": "physnet1", + "provider:network_type": "flat"}} + + self.networks = { + "networks": [self.network["network"]]} + self.neutronclient = mock.MagicMock() self.neutronclient.list_ports.return_value = self.ports self.neutronclient.create_port.return_value = self.port @@ -41,8 +55,9 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.address_scopes) self.neutronclient.create_address_scope.return_value = ( self.address_scope) - self.ext_net = "ext_net" - self.private_net = "private_net" + + self.neutronclient.list_networks.return_value = self.networks + self.neutronclient.create_network.return_value = self.network def test_create_port(self): self.patch_object(openstack_utils, "get_net_uuid") @@ -102,6 +117,27 @@ class TestOpenStackUtils(ut_utils.BaseTestCase): self.neutronclient.create_address_scope.assert_called_once_with( address_scope_msg) + def test_create_external_network(self): + self.patch_object(openstack_utils, "get_net_uuid") + self.get_net_uuid.return_value = self.net_uuid + + # Already exists + network = openstack_utils.create_external_network( + self.neutronclient, self.project_id, False) + self.assertEqual(network, self.network["network"]) + self.neutronclient.create_network.assert_not_called() + + # Does not yet exist + self.neutronclient.list_networks.return_value = { + "networks": []} + network_msg = copy.deepcopy(self.network) + network_msg["network"].pop("id") + network = openstack_utils.create_external_network( + self.neutronclient, self.project_id, False) + self.assertEqual(network, self.network["network"]) + self.neutronclient.create_network.assert_called_once_with( + network_msg) + def test_get_keystone_scope(self): self.patch_object(openstack_utils, "get_current_os_versions") diff --git a/zaza/charm_tests/dragent/configure.py b/zaza/charm_tests/dragent/configure.py new file mode 100644 index 0000000..ac53cdb --- /dev/null +++ b/zaza/charm_tests/dragent/configure.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +from zaza.configure import ( + network, + bgp_speaker, +) +from zaza.utilities import ( + _local_utils, + openstack_utils, +) + +DEFAULT_PEER_APPLICATION_NAME = "quagga" + +# The overcloud network configuration settings are declared. +# These are the network configuration settings under test. +OVERCLOUD_NETWORK_CONFIG = { + "network_type": "gre", + "router_name": "provider-router", + "ip_version": "4", + "address_scope": "public", + "external_net_name": "ext_net", + "external_subnet_name": "ext_net_subnet", + "prefix_len": "24", + "subnetpool_name": "pooled_subnets", + "subnetpool_prefix": "192.168.0.0/16", +} + +# The undercloud network configuration settings are substrate specific to +# the environment where the tests are being executed. These settings may be +# overridden by environment variables. See the doc string documentation for +# zaza.utilities._local_utils.get_overcloud_env_vars for the environment +# variables required to be exported and available to zaza. +# These are default settings provided as an example. +DEFAULT_UNDERCLOUD_NETWORK_CONFIG = { + "start_floating_ip": "10.5.150.0", + "end_floating_ip": "10.5.150.254", + "external_dns": "10.5.0.2", + "external_net_cidr": "10.5.0.0/16", + "default_gateway": "10.5.0.1", +} + + +def setup(): + """Setup BGP networking + + Configure the following: + The overcloud network using subnet pools + The overcloud BGP speaker + The BGP peer + Advertising of the FIPs via BGP + Advertising of the project network(s) via BGP + + :returns: None + :rtype: None + """ + + _local_utils.setup_logging() + + # Get network configuration settings + network_config = {} + # Declared overcloud settings + network_config.update(OVERCLOUD_NETWORK_CONFIG) + # Default undercloud settings + network_config.update(DEFAULT_UNDERCLOUD_NETWORK_CONFIG) + # Environment specific settings + network_config.update(_local_utils.get_undercloud_env_vars()) + + # Get keystone session + keystone_session = openstack_utils.get_overcloud_keystone_session() + + # Confugre the overcloud network + network.setup_sdn(network_config, keystone_session=keystone_session) + # Configure BGP + bgp_speaker.setup_bgp_speaker( + peer_application_name=DEFAULT_PEER_APPLICATION_NAME, + keystone_session=keystone_session) diff --git a/zaza/charm_tests/dragent/configure_dragent.py b/zaza/charm_tests/dragent/configure_dragent.py deleted file mode 100644 index f22df00..0000000 --- a/zaza/charm_tests/dragent/configure_dragent.py +++ /dev/null @@ -1,52 +0,0 @@ -#!/usr/bin/env python3 - -import os - -from zaza.configure import ( - network, - bgp_speaker, -) -from zaza.utilities import ( - _local_utils, - openstack_utils, -) - -DEFAULT_PEER_APPLICATION_NAME = "quagga" -# Find a default network.yaml in tests/bundles/network.yaml of the charm -DEFAULT_TOPOLOGY_FILE = os.path.join("tests", "bundles", "network.yaml") - - -def setup(net_topology="pool", net_topology_file="network.yaml"): - """Setup BGP networking - - :param net_topology: String name of network topology - :type net_topology: string - :param net_info: Network configuration dictionary - :type net_info: dict - :param keystone_session: Keystone session object for overcloud - :type keystone_session: keystoneauth1.session.Session object - :returns: None - :rtype: None - """ - - _local_utils.setup_logging() - - # Check for the network.yaml configuration - if (not os.path.exists(net_topology_file) and - os.path.exists(DEFAULT_TOPOLOGY_FILE)): - net_topology_file = DEFAULT_TOPOLOGY_FILE - - # Get network configuration from YAML and/or environment variables - net_info = _local_utils.get_net_info(net_topology, - net_topology_file=net_topology_file) - - # Get keystone session - keystone_session = openstack_utils.get_overcloud_keystone_session() - - # Confugre the network - network.setup_sdn( - net_topology, net_info, keystone_session=keystone_session) - # Configure BGP - bgp_speaker.setup_bgp_speaker( - peer_application_name=DEFAULT_PEER_APPLICATION_NAME, - keystone_session=keystone_session) diff --git a/zaza/charm_tests/dragent/test_dragent.py b/zaza/charm_tests/dragent/test.py similarity index 100% rename from zaza/charm_tests/dragent/test_dragent.py rename to zaza/charm_tests/dragent/test.py diff --git a/zaza/charm_tests/dragent/tests.py b/zaza/charm_tests/dragent/tests.py index baffcf8..46b3aa3 100644 --- a/zaza/charm_tests/dragent/tests.py +++ b/zaza/charm_tests/dragent/tests.py @@ -3,17 +3,19 @@ import unittest from zaza.utilities import _local_utils -from zaza.charm_tests.dragent import test_dragent +from zaza.charm_tests.dragent import test class DRAgentTest(unittest.TestCase): + BGP_PEER_APPLICATION = 'quagga' + @classmethod def setUpClass(cls): _local_utils.setup_logging() def test_bgp_routes(self): - test_dragent.test_bgp_routes(peer_application_name="quagga") + test.test_bgp_routes(peer_application_name=self.BGP_PEER_APPLICATION) if __name__ == "__main__": diff --git a/zaza/configure/network.py b/zaza/configure/network.py index 8009955..5105938 100755 --- a/zaza/configure/network.py +++ b/zaza/configure/network.py @@ -7,14 +7,76 @@ import sys from zaza.utilities import _local_utils from zaza.utilities import openstack_utils +"""Configure network -def setup_sdn(net_topology, net_info, keystone_session=None): +For these network configuration functions there are two distinct sets of +settings. There is the configuration of the overcloud's network, the network +under test, settings which may vary from test to test. Then there is the +configuration of the substrate specific undercloud which may vary from one test +environment to another. All of this information is required to setup a valid +test. For the purposes of using these functions please consider the following: + +The overcloud network configuration settings are declared by the test caller. +These are the network configuration settings under test and they may range from +a simple GRE setup, to a DVR configuration, to subnetpools with address scopes. +Each test caller may declare a slightly different set of configuration +settings. Here is a simple GRE example: + +EXAMPLE_OVERCLOUD_NETWORK_CONFIG = { + "network_type": "gre", + "router_name": "provider-router", + "private_net_cidr": "192.168.21.0/24", + "external_net_name": "ext_net", + "external_subnet_name": "ext_net_subnet", +} + +The undercloud network configuration settings are substrate specific to the +environment where the tests are being executed. They primarily focus on the +provider network settings. These settings may be overridden by environment +variables. See the doc string documentation for +zaza.utilities._local_utils.get_overcloud_env_vars for the environment +variables required to be exported and available to zaza. Here is an example of +undercloud settings: +EXAMPLE_DEFAULT_UNDERCLOUD_NETWORK_CONFIG = { + "start_floating_ip": "10.5.150.0", + "end_floating_ip": "10.5.150.254", + "external_dns": "10.5.0.2", + "external_net_cidr": "10.5.0.0/16", + "default_gateway": "10.5.0.1", +} + +The network configuration functions take in a dictionary parameter called +network_config and it is a combination of the above including environmental +overrides. The recommended use case is as follows: + +As a python module: + + import zaza + # Build network configuration settings + network_config = {} + # Declared overcloud settings for the network under test + network_config.update(EXAMPLE_OVERCLOUD_NETWORK_CONFIG) + # Default undercloud settings + network_config.update(EXAMPLE_DEFAULT_UNDERCLOUD_NETWORK_CONFIG) + # Environment specific settings + network_config.update( + zaza.utilities._local_utils.get_undercloud_env_vars()) + + # Configure the SDN network + zaza.configure.network.setup_sdn(network_config) + + +As a script from CLI with a YAML file of configuration: + + ./network toploogy_name -f network.yaml +""" + + +def setup_sdn(network_config, keystone_session=None): """Setup Software Defined Network - :param net_topology: String name of network topology - :type net_topology: string - :param net_info: Network configuration dictionary - :type net_info: dict + :param network_config: Network configuration settings dictionary + :type network_config: dict :param keystone_session: Keystone session object for overcloud :type keystone_session: keystoneauth1.session.Session object :returns: None @@ -31,14 +93,14 @@ def setup_sdn(net_topology, net_info, keystone_session=None): neutron_client = openstack_utils.get_neutron_session_client( keystone_session) - # Resolve the project name from the overcloud opentackrc into a project id + # Resolve the project name from the overcloud openrc into a project id project_id = openstack_utils.get_project_id( keystone_client, "admin", ) # Network Setup subnetpools = False - if net_info.get("subnetpool_prefix"): + if network_config.get("subnetpool_prefix"): subnetpools = True logging.info("Configuring overcloud network") @@ -46,68 +108,67 @@ def setup_sdn(net_topology, net_info, keystone_session=None): ext_network = openstack_utils.create_external_network( neutron_client, project_id, - net_info.get("dvr_enabled", False), - net_info["external_net_name"]) + network_config.get("dvr_enabled", False), + network_config["external_net_name"]) openstack_utils.create_external_subnet( neutron_client, project_id, ext_network, - net_info["default_gateway"], - net_info["external_net_cidr"], - net_info["start_floating_ip"], - net_info["end_floating_ip"], - net_info["external_subnet_name"]) - # Should this be --enable_snat = False + network_config["default_gateway"], + network_config["external_net_cidr"], + network_config["start_floating_ip"], + network_config["end_floating_ip"], + network_config["external_subnet_name"]) provider_router = ( openstack_utils.create_provider_router(neutron_client, project_id)) openstack_utils.plug_extnet_into_router( neutron_client, provider_router, ext_network) - ip_version = net_info.get("ip_version") or 4 + ip_version = network_config.get("ip_version") or 4 subnetpool = None if subnetpools: address_scope = openstack_utils.create_address_scope( neutron_client, project_id, - net_info.get("address_scope"), + network_config.get("address_scope"), ip_version=ip_version) subnetpool = openstack_utils.create_subnetpool( neutron_client, project_id, - net_info.get("subnetpool_name"), - net_info.get("subnetpool_prefix"), + network_config.get("subnetpool_name"), + network_config.get("subnetpool_prefix"), address_scope) project_network = openstack_utils.create_project_network( neutron_client, project_id, shared=False, - network_type=net_info["network_type"]) + network_type=network_config["network_type"]) project_subnet = openstack_utils.create_project_subnet( neutron_client, project_id, project_network, - net_info.get("private_net_cidr"), + network_config.get("private_net_cidr"), subnetpool=subnetpool, ip_version=ip_version) openstack_utils.update_subnet_dns( neutron_client, project_subnet, - net_info["external_dns"]) + network_config["external_dns"]) openstack_utils.plug_subnet_into_router( neutron_client, - net_info["router_name"], + network_config["router_name"], project_network, project_subnet) openstack_utils.add_neutron_secgroup_rules(neutron_client, project_id) -def setup_gateway_ext_port(net_info, keystone_session=None): +def setup_gateway_ext_port(network_config, keystone_session=None): """Setup external port on Neutron Gateway. For OpenStack on OpenStack scenarios - :param net_info: Network configuration dictionary - :type net_info: dict + :param network_config: Network configuration dictionary + :type network_config: dict :param keystone_session: Keystone session object for undercloud :type keystone_session: keystoneauth1.session.Session object :returns: None @@ -125,8 +186,8 @@ def setup_gateway_ext_port(net_info, keystone_session=None): # Add an interface to the neutron-gateway units and tell juju to use it # as the external port. - if "net_id" in net_info.keys(): - net_id = net_info["net_id"] + if "net_id" in network_config.keys(): + net_id = network_config["net_id"] else: net_id = None @@ -134,13 +195,28 @@ def setup_gateway_ext_port(net_info, keystone_session=None): openstack_utils.configure_gateway_ext_port( nova_client, neutron_client, - dvr_mode=net_info.get("dvr_enabled", False), + dvr_mode=network_config.get("dvr_enabled", False), net_id=net_id) def run_from_cli(): """Run network configurations from CLI + Use a YAML file of network configuration settings to configure the + overcloud network. YAML file of the form: + + topology_name: + network_type: gre + router_name: provider-router + private_net_cidr: 192.168.21.0/24 + external_dns: 10.5.0.2 + external_net_cidr: 10.5.0.0/16 + external_net_name: ext_net + external_subnet_name: ext_net_subnet + default_gateway: 10.5.0.1 + start_floating_ip: 10.5.150.0 + end_floating_ip: 10.5.200.254 + :returns: None :rtype: None """ @@ -164,14 +240,14 @@ def run_from_cli(): ignore_env_vars = _local_utils.parse_arg(options, "ignore_env_vars") logging.info("Setting up %s network" % (net_topology)) - net_info = _local_utils.get_net_info( + network_config = _local_utils.get_network_config( net_topology, ignore_env_vars, net_topology_file) # Handle network for Openstack-on-Openstack scenarios if _local_utils.get_provider_type() == "openstack": - setup_gateway_ext_port(net_info) + setup_gateway_ext_port(network_config) - setup_sdn(net_topology, net_info) + setup_sdn(network_config) if __name__ == "__main__": diff --git a/zaza/utilities/_local_utils.py b/zaza/utilities/_local_utils.py index 77ac5f3..82b9c82 100644 --- a/zaza/utilities/_local_utils.py +++ b/zaza/utilities/_local_utils.py @@ -18,65 +18,64 @@ from zaza import model from zaza.charm_lifecycle import utils as lifecycle_utils -# XXX Tech Debt Begins Here +def get_undercloud_env_vars(): + """ Get environment specific undercloud network configuration settings from + environment variables. -def get_network_env_vars(): - """Get environment variables with names which are consistent with - network.yaml keys; Also get network environment variables as commonly - used by openstack-charm-testing and ubuntu-openstack-ci automation. - Return a dictionary compatible with openstack-mojo-specs network.yaml - key structure. + For each testing substrate, specific undercloud network configuration + settings should be exported into the environment to enable testing on that + substrate. + + Note: *Overcloud* settings should be declared by the test caller and should + not be overridden here. + + Return a dictionary compatible with zaza.configure.network functions' + expected key structure. + + Example exported environment variables: + export default_gateway="172.17.107.1" + export external_net_cidr="172.17.107.0/24" + export external_dns="10.5.0.2" + export start_floating_ip="172.17.107.200" + export end_floating_ip="172.17.107.249" + + Example o-c-t & uosci non-standard environment variables: + export NET_ID="a705dd0f-5571-4818-8c30-4132cc494668" + export GATEWAY="172.17.107.1" + export CIDR_EXT="172.17.107.0/24" + export NAMESERVER="10.5.0.2" + export FIP_RANGE="172.17.107.200:172.17.107.249" :returns: Network environment variables :rtype: dict """ - # Example o-c-t & uosci environment variables: - # NET_ID="a705dd0f-5571-4818-8c30-4132cc494668" - # GATEWAY="172.17.107.1" - # CIDR_EXT="172.17.107.0/24" - # CIDR_PRIV="192.168.121.0/24" - # NAMESERVER="10.5.0.2" - # FIP_RANGE="172.17.107.200:172.17.107.249" - # AMULET_OS_VIP00="172.17.107.250" - # AMULET_OS_VIP01="172.17.107.251" - # AMULET_OS_VIP02="172.17.107.252" - # AMULET_OS_VIP03="172.17.107.253" + # Handle backward compatibile OSCI enviornment variables _vars = {} _vars['net_id'] = os.environ.get('NET_ID') _vars['external_dns'] = os.environ.get('NAMESERVER') _vars['default_gateway'] = os.environ.get('GATEWAY') _vars['external_net_cidr'] = os.environ.get('CIDR_EXT') - _vars['private_net_cidr'] = os.environ.get('CIDR_PRIV') + # Take FIP_RANGE and create start and end floating ips _fip_range = os.environ.get('FIP_RANGE') if _fip_range and ':' in _fip_range: _vars['start_floating_ip'] = os.environ.get('FIP_RANGE').split(':')[0] _vars['end_floating_ip'] = os.environ.get('FIP_RANGE').split(':')[1] - _vips = [os.environ.get('AMULET_OS_VIP00'), - os.environ.get('AMULET_OS_VIP01'), - os.environ.get('AMULET_OS_VIP02'), - os.environ.get('AMULET_OS_VIP03')] - - # Env var naming consistent with network.yaml takes priority - _keys = ['default_gateway' + # Env var naming consistent with zaza.configure.network functions takes + # priority. Override backward compatible settings. + _keys = ['default_gateway', 'start_floating_ip', 'end_floating_ip', 'external_dns', - 'external_net_cidr', - 'external_net_name', - 'external_subnet_name', - 'network_type', - 'private_net_cidr', - 'router_name'] + 'external_net_cidr'] for _key in _keys: _val = os.environ.get(_key) if _val: _vars[_key] = _val # Remove keys and items with a None value - _vars['vips'] = [_f for _f in _vips if _f] for k, v in list(_vars.items()): if not v: del _vars[k] @@ -115,7 +114,10 @@ def get_yaml_config(config_file): def get_net_info(net_topology, ignore_env_vars=False, net_topology_file="network.yaml"): """Get network info from network.yaml, override the values if specific - environment variables are set. + environment variables are set for the undercloud. + + This function may be used when running network configuration from CLI to + pass in network configuration settings from a YAML file. :param net_topology: Network topology name from network.yaml :type net_topology: string @@ -132,8 +134,9 @@ def get_net_info(net_topology, ignore_env_vars=False, .format(net_topology_file)) if not ignore_env_vars: - logging.info("Consuming network environment variables as overrides.") - net_info.update(get_network_env_vars()) + logging.info("Consuming network environment variables as overrides " + "for the undercloud.") + net_info.update(get_undercloud_env_vars()) logging.info("Network info: {}".format(dict_to_yaml(net_info))) return net_info diff --git a/zaza/utilities/openstack_utils.py b/zaza/utilities/openstack_utils.py index 1691f92..5d6748f 100644 --- a/zaza/utilities/openstack_utils.py +++ b/zaza/utilities/openstack_utils.py @@ -401,10 +401,6 @@ def configure_gateway_ext_port(novaclient, neutronclient, else: application_name = 'neutron-gateway' - # XXX Trying to track down a failure with juju run neutron-gateway/0 in - # the post juju_set check. Try a sleep here to see if some network - # reconfigureing on the gateway is still in progress and that's - # causing the issue if ext_br_macs: logging.info('Setting {} on {} external port to {}'.format( config_key, application_name, ext_br_macs_str)) From aa6d170a6c4198f7620d78648da9001d7039667a Mon Sep 17 00:00:00 2001 From: David Ames Date: Fri, 20 Apr 2018 09:52:27 -0700 Subject: [PATCH 3/3] Use tenacity for binary back off Use the tenacity module to handle retry and binary back off of test BGP test assertions. --- setup.py | 1 + test-requirements.txt | 1 + zaza/charm_tests/dragent/test.py | 41 +++++++++++++------------------- 3 files changed, 18 insertions(+), 25 deletions(-) diff --git a/setup.py b/setup.py index 7d20665..a2a39f8 100644 --- a/setup.py +++ b/setup.py @@ -14,6 +14,7 @@ install_require = [ 'juju', 'juju-wait', 'PyYAML', + 'tenacity', ] tests_require = [ diff --git a/test-requirements.txt b/test-requirements.txt index 19c644c..ac542c0 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -24,6 +24,7 @@ python-keystoneclient python-neutronclient python-novaclient python-swiftclient +tenacity distro-info paramiko diff --git a/zaza/charm_tests/dragent/test.py b/zaza/charm_tests/dragent/test.py index b56e8c6..dd3f213 100644 --- a/zaza/charm_tests/dragent/test.py +++ b/zaza/charm_tests/dragent/test.py @@ -2,8 +2,8 @@ import argparse import logging -import time import sys +import tenacity from zaza import model from zaza.charm_lifecycle import utils as lifecycle_utils @@ -35,7 +35,7 @@ def test_bgp_routes(peer_application_name="quagga", keystone_session=None): # Get the peer unit peer_unit = model.get_units( - lifecycle_utils.get_juju_model(), peer_application_name)[0] + lifecycle_utils.get_juju_model(), peer_application_name)[0].entity_id # Get expected advertised routes private_cidr = neutron_client.list_subnets( @@ -45,32 +45,23 @@ def test_bgp_routes(peer_application_name="quagga", keystone_session=None): ["floatingips"][0]["floating_ip_address"]) # This test may run immediately after configuration. It may take time for - # routes to propogate via BGP. Do a binary backoff up to ~2 minutes long. - backoff = 2 - max_wait = 129 - logging.info("Checking routes on BGP peer {}".format(peer_unit.entity_id)) - while backoff < max_wait: + # routes to propogate via BGP. Do a binary backoff. + @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60), + reraise=True, stop=tenacity.stop_after_attempt(8)) + def _assert_cidr_in_peer_routing_table(peer_unit, cidr): + logging.debug("Checking for {} on BGP peer {}" + .format(cidr, peer_unit)) # Run show ip route bgp on BGP peer routes = _local_utils.remote_run( - peer_unit.entity_id, remote_cmd='vtysh -c "show ip route bgp"') - try: - logging.debug(routes) - assert private_cidr in routes, ( - "Private subnet CIDR, {}, not advertised to BGP peer" - .format(private_cidr)) - logging.info("Private subnet CIDR, {}, found in routing table" - .format(private_cidr)) - break - except AssertionError: - logging.info("Binary backoff waiting {} seconds for bgp " - "routes on peer".format(backoff)) - time.sleep(backoff) - backoff = backoff * 2 - if backoff > max_wait: - raise + peer_unit, remote_cmd='vtysh -c "show ip route bgp"') + logging.debug(routes) + assert cidr in routes, ( + "CIDR, {}, not found in BGP peer's routing table" .format(cidr)) - assert floating_ip_cidr in routes, ("Floating IP, {}, not advertised " - "to BGP peer".format(floating_ip_cidr)) + _assert_cidr_in_peer_routing_table(peer_unit, private_cidr) + logging.info("Private subnet CIDR, {}, found in routing table" + .format(private_cidr)) + _assert_cidr_in_peer_routing_table(peer_unit, floating_ip_cidr) logging.info("Floating IP CIDR, {}, found in routing table" .format(floating_ip_cidr))