Split configure_gateway_ext_port function

The function does three separate things today, and two of its
tasks are useful for other provider types such as MAAS.

Also fix create_additional_port_for_machines idempotency.

We previously added a run time assertion to fail early when
attempting to configure networking for an invalid bundle. The
check had the side effect of prohibiting subsequent runs on
already configured models.
This commit is contained in:
Frode Nordahl
2021-01-12 17:11:35 +01:00
parent 9961c9468b
commit ee0dd80cec
2 changed files with 274 additions and 131 deletions

View File

@@ -1262,34 +1262,68 @@ class TestOpenStackUtils(ut_utils.BaseTestCase):
self.get_application.side_effect = KeyError
self.assertFalse(openstack_utils.ngw_present())
def test_configure_gateway_ext_port(self):
# FIXME: this is not a complete unit test for the function as one did
# not exist at all I'm adding this to test one bit and we'll add more
# as we go.
def test_get_charm_networking_data(self):
self.patch_object(openstack_utils, 'deprecated_external_networking')
self.patch_object(openstack_utils, 'dvr_enabled')
self.patch_object(openstack_utils, 'ovn_present')
self.patch_object(openstack_utils, 'ngw_present')
self.patch_object(openstack_utils, 'get_ovs_uuids')
self.patch_object(openstack_utils, 'get_gateway_uuids')
self.patch_object(openstack_utils, 'get_admin_net')
self.patch_object(openstack_utils, 'get_ovn_uuids')
self.patch_object(openstack_utils.model, 'get_application')
self.dvr_enabled.return_value = False
self.ovn_present.return_value = False
self.ngw_present.return_value = True
self.get_admin_net.return_value = {'id': 'fakeid'}
self.ngw_present.return_value = False
self.get_ovs_uuids.return_value = []
self.get_gateway_uuids.return_value = []
self.get_ovn_uuids.return_value = []
self.get_application.side_effect = KeyError
novaclient = mock.MagicMock()
neutronclient = mock.MagicMock()
def _fake_empty_generator(empty=True):
if empty:
return
yield
self.get_gateway_uuids.side_effect = _fake_empty_generator
with self.assertRaises(RuntimeError):
openstack_utils.configure_gateway_ext_port(
novaclient, neutronclient)
# provide a uuid and check that we don't raise RuntimeError
self.get_gateway_uuids.side_effect = ['fake-uuid']
openstack_utils.configure_gateway_ext_port(
novaclient, neutronclient)
openstack_utils.get_charm_networking_data()
self.ngw_present.return_value = True
self.assertEquals(
openstack_utils.get_charm_networking_data(),
openstack_utils.CharmedOpenStackNetworkingData(
openstack_utils.OpenStackNetworkingTopology.ML2_OVS,
['neutron-gateway'],
mock.ANY,
'data-port',
{}))
self.dvr_enabled.return_value = True
self.assertEquals(
openstack_utils.get_charm_networking_data(),
openstack_utils.CharmedOpenStackNetworkingData(
openstack_utils.OpenStackNetworkingTopology.ML2_OVS_DVR,
['neutron-gateway', 'neutron-openvswitch'],
mock.ANY,
'data-port',
{}))
self.ngw_present.return_value = False
self.assertEquals(
openstack_utils.get_charm_networking_data(),
openstack_utils.CharmedOpenStackNetworkingData(
openstack_utils.OpenStackNetworkingTopology.ML2_OVS_DVR_SNAT,
['neutron-openvswitch'],
mock.ANY,
'data-port',
{}))
self.dvr_enabled.return_value = False
self.ovn_present.return_value = True
self.assertEquals(
openstack_utils.get_charm_networking_data(),
openstack_utils.CharmedOpenStackNetworkingData(
openstack_utils.OpenStackNetworkingTopology.ML2_OVN,
['ovn-chassis'],
mock.ANY,
'bridge-interface-mappings',
{'ovn-bridge-mappings': 'physnet1:br-ex'}))
self.get_application.side_effect = None
self.assertEquals(
openstack_utils.get_charm_networking_data(),
openstack_utils.CharmedOpenStackNetworkingData(
openstack_utils.OpenStackNetworkingTopology.ML2_OVN,
['ovn-chassis', 'ovn-dedicated-chassis'],
mock.ANY,
'bridge-interface-mappings',
{'ovn-bridge-mappings': 'physnet1:br-ex'}))

View File

@@ -16,7 +16,10 @@
This module contains a number of functions for interacting with OpenStack.
"""
import collections
import copy
import datetime
import enum
import io
import itertools
import juju_wait
@@ -721,6 +724,211 @@ def add_interface_to_netplan(server_name, mac_address):
model.run_on_unit(unit_name, "sudo netplan apply")
class OpenStackNetworkingTopology(enum.Enum):
"""OpenStack Charms Network Topologies."""
ML2_OVS = 'ML2+OVS'
ML2_OVS_DVR = 'ML2+OVS+DVR'
ML2_OVS_DVR_SNAT = 'ML2+OVS+DVR, no dedicated GWs'
ML2_OVN = 'ML2+OVN'
CharmedOpenStackNetworkingData = collections.namedtuple(
'CharmedOpenStackNetworkingData',
[
'topology',
'application_names',
'unit_machine_ids',
'port_config_key',
'other_config',
])
def get_charm_networking_data(limit_gws=None):
"""Inspect Juju model, determine networking topology and return data.
:param limit_gws: Limit the number of gateways that get a port attached
:type limit_gws: Optional[int]
:rtype: CharmedOpenStackNetworkingData[
OpenStackNetworkingTopology,
List[str],
Iterator[str],
str,
Dict[str,str]]
:returns: Named Tuple with networking data, example:
CharmedOpenStackNetworkingData(
OpenStackNetworkingTopology.ML2_OVN,
['ovn-chassis', 'ovn-dedicated-chassis'],
['machine-id-1', 'machine-id-2'], # generator object
'bridge-interface-mappings',
{'ovn-bridge-mappings': 'physnet1:br-ex'})
:raises: RuntimeError
"""
# Initialize defaults, these will be amended to fit the reality of the
# model in the checks below.
topology = OpenStackNetworkingTopology.ML2_OVS
other_config = {}
port_config_key = (
'data-port' if not deprecated_external_networking() else 'ext-port')
unit_machine_ids = []
application_names = []
if dvr_enabled():
if ngw_present():
application_names = ['neutron-gateway', 'neutron-openvswitch']
topology = OpenStackNetworkingTopology.ML2_OVS_DVR
else:
application_names = ['neutron-openvswitch']
topology = OpenStackNetworkingTopology.ML2_OVS_DVR_SNAT
unit_machine_ids = itertools.islice(
itertools.chain(
get_ovs_uuids(),
get_gateway_uuids()),
limit_gws)
elif ngw_present():
unit_machine_ids = itertools.islice(
get_gateway_uuids(), limit_gws)
application_names = ['neutron-gateway']
elif ovn_present():
topology = OpenStackNetworkingTopology.ML2_OVN
unit_machine_ids = itertools.islice(get_ovn_uuids(), limit_gws)
application_names = ['ovn-chassis']
try:
ovn_dc_name = 'ovn-dedicated-chassis'
model.get_application(ovn_dc_name)
application_names.append(ovn_dc_name)
except KeyError:
# ovn-dedicated-chassis not in deployment
pass
port_config_key = 'bridge-interface-mappings'
other_config.update({'ovn-bridge-mappings': 'physnet1:br-ex'})
else:
raise RuntimeError('Unable to determine charm network topology.')
return CharmedOpenStackNetworkingData(
topology,
application_names,
unit_machine_ids,
port_config_key,
other_config)
def create_additional_port_for_machines(novaclient, neutronclient, net_id,
unit_machine_ids,
add_dataport_to_netplan=False):
"""Create additional port for machines for use with external networking.
:param novaclient: Undercloud Authenticated novaclient.
:type novaclient: novaclient.Client object
:param neutronclient: Undercloud Authenticated neutronclient.
:type neutronclient: neutronclient.Client object
:param net_id: Network ID to create ports on.
:type net_id: string
:param unit_machine_ids: Juju provider specific machine IDs for which we
should add ports on.
:type unit_machine_ids: Iterator[str]
:param add_dataport_to_netplan: Whether the newly created port should be
added to instance system configuration so
that it is brought up on instance reboot.
:type add_dataport_to_netplan: Optional[bool]
:returns: List of MAC addresses for created ports.
:rtype: List[str]
:raises: RuntimeError
"""
eligible_machines = 0
for uuid in unit_machine_ids:
eligible_machines += 1
server = novaclient.servers.get(uuid)
ext_port_name = "{}_ext-port".format(server.name)
for port in neutronclient.list_ports(device_id=server.id)['ports']:
if port['name'] == ext_port_name:
logging.warning(
'Instance {} already has additional port, skipping.'
.format(server.id))
break
else:
logging.info('Attaching additional port to instance ("{}"), '
'connected to net id: {}'
.format(uuid, net_id))
body_value = {
"port": {
"admin_state_up": True,
"name": ext_port_name,
"network_id": net_id,
"port_security_enabled": False,
}
}
port = neutronclient.create_port(body=body_value)
server.interface_attach(port_id=port['port']['id'],
net_id=None, fixed_ip=None)
if add_dataport_to_netplan:
mac_address = get_mac_from_port(port, neutronclient)
add_interface_to_netplan(server.name,
mac_address=mac_address)
if not eligible_machines:
# NOTE: unit_machine_ids may be an iterator so testing it for contents
# or length prior to iterating over it is futile.
raise RuntimeError('Unable to determine UUIDs for machines to attach '
'external networking to.')
# Retrieve the just created ports from Neutron so that we can provide our
# caller with their MAC addresses.
return [
port['mac_address']
for port in neutronclient.list_ports(network_id=net_id)['ports']
if 'ext-port' in port['name']
]
def configure_networking_charms(networking_data, macs, use_juju_wait=True):
"""Configure external networking for networking charms.
:param networking_data: Data on networking charm topology.
:type networking_data: CharmedOpenStackNetworkingData
:param macs: MAC addresses of ports for use with external networking.
:type macs: Iterator[str]
:param use_juju_wait: Whether to use juju wait to wait for the model to
settle once the gateway has been configured. Default is True
:type use_juju_wait: Optional[bool]
"""
br_mac_fmt = 'br-ex:{}' if not deprecated_external_networking() else '{}'
br_mac = [
br_mac_fmt.format(mac)
for mac in macs
]
config = copy.deepcopy(networking_data.other_config)
config.update({networking_data.port_config_key: ' '.join(sorted(br_mac))})
for application_name in networking_data.application_names:
logging.info('Setting {} on {}'.format(
config, application_name))
current_data_port = get_application_config_option(
application_name,
networking_data.port_config_key)
if current_data_port == config[networking_data.port_config_key]:
logging.info('Config already set to value')
return
model.set_application_config(
application_name,
configuration=config)
# NOTE(fnordahl): We are stuck with juju_wait until we figure out how
# to deal with all the non ['active', 'idle', 'Unit is ready.']
# workload/agent states and msgs that our mojo specs are exposed to.
if use_juju_wait:
juju_wait.wait(wait_for_workload=True)
else:
zaza.model.wait_for_agent_status()
# TODO: shouldn't access get_charm_config() here as it relies on
# ./tests/tests.yaml existing by default (regardless of the
# fatal=False) ... it's not great design.
test_config = zaza.charm_lifecycle.utils.get_charm_config(
fatal=False)
zaza.model.wait_for_application_states(
states=test_config.get('target_deploy_status', {}))
def configure_gateway_ext_port(novaclient, neutronclient, net_id=None,
add_dataport_to_netplan=False,
limit_gws=None,
@@ -739,123 +947,24 @@ def configure_gateway_ext_port(novaclient, neutronclient, net_id=None,
settle once the gateway has been configured. Default is True
:type use_juju_wait: boolean
"""
deprecated_extnet_mode = deprecated_external_networking()
port_config_key = 'data-port'
if deprecated_extnet_mode:
port_config_key = 'ext-port'
config = {}
if dvr_enabled():
uuids = itertools.islice(itertools.chain(get_ovs_uuids(),
get_gateway_uuids()),
limit_gws)
networking_data = get_charm_networking_data(limit_gws=limit_gws)
if networking_data.topology in (
OpenStackNetworkingTopology.ML2_OVS_DVR,
OpenStackNetworkingTopology.ML2_OVS_DVR_SNAT):
# If dvr, do not attempt to persist nic in netplan
# https://github.com/openstack-charmers/zaza-openstack-tests/issues/78
add_dataport_to_netplan = False
application_names = ['neutron-openvswitch']
try:
ngw = 'neutron-gateway'
model.get_application(ngw)
application_names.append(ngw)
except KeyError:
# neutron-gateway not in deployment
pass
elif ngw_present():
uuids = itertools.islice(get_gateway_uuids(), limit_gws)
application_names = ['neutron-gateway']
elif ovn_present():
uuids = itertools.islice(get_ovn_uuids(), limit_gws)
application_names = ['ovn-chassis']
try:
ovn_dc_name = 'ovn-dedicated-chassis'
model.get_application(ovn_dc_name)
application_names.append(ovn_dc_name)
except KeyError:
# ovn-dedicated-chassis not in deployment
pass
port_config_key = 'bridge-interface-mappings'
config.update({'ovn-bridge-mappings': 'physnet1:br-ex'})
add_dataport_to_netplan = True
else:
raise RuntimeError('Unable to determine charm network topology.')
if not net_id:
net_id = get_admin_net(neutronclient)['id']
ports_created = 0
for uuid in uuids:
server = novaclient.servers.get(uuid)
ext_port_name = "{}_ext-port".format(server.name)
for port in neutronclient.list_ports(device_id=server.id)['ports']:
if port['name'] == ext_port_name:
logging.warning(
'Neutron Gateway already has additional port')
break
else:
logging.info('Attaching additional port to instance ("{}"), '
'connected to net id: {}'
.format(uuid, net_id))
body_value = {
"port": {
"admin_state_up": True,
"name": ext_port_name,
"network_id": net_id,
"port_security_enabled": False,
}
}
port = neutronclient.create_port(body=body_value)
ports_created += 1
server.interface_attach(port_id=port['port']['id'],
net_id=None, fixed_ip=None)
if add_dataport_to_netplan:
mac_address = get_mac_from_port(port, neutronclient)
add_interface_to_netplan(server.name,
mac_address=mac_address)
if not ports_created:
# NOTE: uuids is an iterator so testing it for contents or length prior
# to iterating over it is futile.
raise RuntimeError('Unable to determine UUIDs for machines to attach '
'external networking to.')
macs = create_additional_port_for_machines(
novaclient, neutronclient, net_id, networking_data.unit_machine_ids,
add_dataport_to_netplan)
ext_br_macs = []
for port in neutronclient.list_ports(network_id=net_id)['ports']:
if 'ext-port' in port['name']:
if deprecated_extnet_mode:
ext_br_macs.append(port['mac_address'])
else:
ext_br_macs.append('br-ex:{}'.format(port['mac_address']))
ext_br_macs.sort()
ext_br_macs_str = ' '.join(ext_br_macs)
if ext_br_macs:
config.update({port_config_key: ext_br_macs_str})
for application_name in application_names:
logging.info('Setting {} on {}'.format(
config, application_name))
current_data_port = get_application_config_option(application_name,
port_config_key)
if current_data_port == ext_br_macs_str:
logging.info('Config already set to value')
return
model.set_application_config(
application_name,
configuration=config)
# NOTE(fnordahl): We are stuck with juju_wait until we figure out how
# to deal with all the non ['active', 'idle', 'Unit is ready.']
# workload/agent states and msgs that our mojo specs are exposed to.
if use_juju_wait:
juju_wait.wait(wait_for_workload=True)
else:
zaza.model.wait_for_agent_status()
# TODO: shouldn't access get_charm_config() here as it relies on
# ./tests/tests.yaml existing by default (regardless of the
# fatal=False) ... it's not great design.
test_config = zaza.charm_lifecycle.utils.get_charm_config(
fatal=False)
zaza.model.wait_for_application_states(
states=test_config.get('target_deploy_status', {}))
if macs:
configure_networking_charms(
networking_data, macs, use_juju_wait=use_juju_wait)
@tenacity.retry(wait=tenacity.wait_exponential(multiplier=1, max=60),