OVN mac address ports ansible module

Add ansible module tripleo_ovn_mac_addresses. This
module will replace the following tripleo-heat-templates
resources:
  OS::TripleO::OVNMacAddressNetwork
  OS::TripleO::OVNMacAddressPort

Also update the tripleo_hieradata 'ovn_chassis_mac_map.j2'
template.

Implements: blueprint network-data-v2-ports
Change-Id: I0bc9c180b6a6cb70d32005e9528cf1b16c861ea8
This commit is contained in:
Harald Jensås 2021-03-19 05:12:33 +01:00
parent cca4da0bbc
commit d48af77c95
6 changed files with 478 additions and 12 deletions

View File

@ -0,0 +1,14 @@
==================================
Module - tripleo_ovn_mac_addresses
==================================
This module provides for the following ansible plugin:
* tripleo_ovn_mac_addresses
.. ansibleautoplugin::
:module: tripleo_ansible/ansible_plugins/modules/tripleo_ovn_mac_addresses.py
:documentation: true
:examples: true

View File

@ -18,6 +18,7 @@
import collections
import ipaddress
import jsonschema
import os
import yaml
RES_ID = 'physical_resource_id'
@ -415,6 +416,17 @@ def validate_json_schema(net_data):
return error_messages
def validate_playbook_dir(playbook_dir_path):
if not os.path.exists(playbook_dir_path):
raise Exception('ERROR: Playbook directory {} does not exist.'.format(
playbook_dir_path))
if not os.path.isdir(playbook_dir_path):
raise Exception(
'ERROR: Playbook directory {} is not a directory'.format(
playbook_dir_path))
def tags_to_dict(resource_tags):
tag_dict = dict()
for tag in resource_tags:

View File

@ -0,0 +1,287 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# Copyright (c) 2021 OpenStack Foundation
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from concurrent import futures
import os
import yaml
try:
from ansible.module_utils import network_data_v2
except ImportError:
from tripleo_ansible.ansible_plugins.module_utils import network_data_v2
from ansible.module_utils.basic import AnsibleModule
from ansible.module_utils.openstack import openstack_full_argument_spec
from ansible.module_utils.openstack import openstack_module_kwargs
from ansible.module_utils.openstack import openstack_cloud_from_module
ANSIBLE_METADATA = {
'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'
}
DOCUMENTATION = '''
---
module: tripleo_ovn_mac_addresses
short_description: Manage OVN bridge Mac Addresses
version_added: "2.8"
description:
- "Create a OVN Mac Address network, and allocate bridge mac address ports"
options:
concurrency:
description:
- Maximum number of server resources to provision ports for at once.
Set to 0 to have no concurrency limit
type: int
default: 0
playbook_dir:
description:
- The path to the directory of the playbook that was passed to the
ansible-playbook command line.
type: str
stack_name:
description:
- Name of the overcloud stack
type: str
default: overcloud
role_name:
description:
- TripleO role name
type: str
ovn_bridge_mappings:
description:
- OVN bridge mappings
type: list
server_resource_names:
description:
- List of server resources
type: list
ovn_static_bridge_mac_mappings:
description:
- Static OVN Bridge MAC address mappings. Unique OVN bridge mac addresses
is dynamically allocated by creating neutron ports. When neutron isn't
available, for instance in the standalone deployment, use this
parameter to provide static OVN bridge mac addresses.
type: dict
default: {}
author:
- Harald Jensås <hjensas@redhat.com>
'''
RETURN = '''
'''
EXAMPLES = '''
- name: Create OVN Mac address ports
tripleo_ovn_mac_addresses:
stack_name: overcloud
role_name: Controller
bridge_mappings:
- datacentre:br-ex
server_resource_names:
- controller-0
- controller-1
- controller-2
- name: Create OVN Mac address ports (static)
tripleo_ovn_mac_addresses:
stack_name: overcloud
role_name: Controller
bridge_mappings:
- datacentre:br-ex
server_resource_names:
- controller-0
- compute-0
ovn_static_bridge_mac_mappings:
controller-0:
datacenter: 00:00:5E:00:53:00
provider: 00:00:5E:00:53:01
compute-0:
datacenter: 00:00:5E:00:54:00
provider: 00:00:5E:00:54:01
'''
NET_NAME = 'ovn_mac_addr_net'
NET_DESCRIPTION = 'Network used to allocate MAC addresses for OVN chassis.'
def create_ovn_mac_address_network(result, conn):
network = conn.network.find_network(NET_NAME)
if network is None:
network = conn.network.create_network(name=NET_NAME,
description=NET_DESCRIPTION)
result['changed'] = True
return network.id
def port_exists(conn, net_id, tags, name):
try:
next(conn.network.ports(network_id=net_id, name=name, tags=tags))
except StopIteration:
return False
return True
def create_ovn_mac_address_ports(result, conn, net_id, tags, physnets,
server):
for physnet in physnets:
name = '_'.join([server, 'ovn_physnet', physnet])
if port_exists(conn, net_id, tags, name):
continue
port = conn.network.create_port(network_id=net_id, name=name,
dns_name=server)
conn.network.set_tags(
port, tags + ['tripleo_ovn_physnet={}'.format(physnet)])
result['changed'] = True
def remove_obsolete_ports(result, conn, net_id, tags, servers, physnets):
ports = conn.network.ports(network_id=net_id, tags=tags)
for port in ports:
tags = network_data_v2.tags_to_dict(port.tags)
if (port.dns_name not in servers
or tags['tripleo_ovn_physnet'] not in physnets):
conn.network.delete_port(port)
result['changed'] = True
def validate_ovn_bridge_mac_addr_var_file(ovn_bridge_mac_addr_var_file):
if not os.path.isfile(ovn_bridge_mac_addr_var_file):
raise Exception(
'ERROR: OVN bridge MAC address var file {} is not a file'.format(
ovn_bridge_mac_addr_var_file))
def write_vars_file(conn, playbook_dir, net_id, tags, static_mappings):
playbook_dir_path = os.path.abspath(playbook_dir)
network_data_v2.validate_playbook_dir(playbook_dir)
ovn_bridge_mac_addr_var_file = os.path.join(
playbook_dir_path, 'ovn_bridge_mac_address_vars.yaml')
if not os.path.exists(ovn_bridge_mac_addr_var_file):
data = dict()
else:
validate_ovn_bridge_mac_addr_var_file(ovn_bridge_mac_addr_var_file)
with open(ovn_bridge_mac_addr_var_file, 'r') as f:
data = yaml.safe_load(f.read())
if not static_mappings:
ports = conn.network.ports(network_id=net_id, tags=tags)
for port in ports:
tag_dict = network_data_v2.tags_to_dict(port.tags)
hostname = port.dns_name
physnet = tag_dict.get('tripleo_ovn_physnet')
if hostname and physnet:
host = data.setdefault(hostname, dict())
host[physnet] = port.mac_address
else:
data = static_mappings
with open(ovn_bridge_mac_addr_var_file, 'w') as f:
f.write(yaml.safe_dump(data, default_flow_style=False))
def run_module():
result = dict(
success=False,
changed=False,
error="",
)
argument_spec = openstack_full_argument_spec(
**yaml.safe_load(DOCUMENTATION)['options']
)
module = AnsibleModule(
argument_spec,
supports_check_mode=False,
**openstack_module_kwargs()
)
stack = module.params.get('stack_name', 'overcloud')
role_name = module.params['role_name']
bridge_mappings = module.params['ovn_bridge_mappings']
servers = module.params['server_resource_names']
playbook_dir = module.params['playbook_dir']
concurrency = module.params.get('concurrency', 0)
static_mappings = module.params.get(
'ovn_static_bridge_mac_mappings', {})
physnets = [x.split(':')[0] for x in bridge_mappings]
conn = tags = net_id = None
try:
if not static_mappings:
_, conn = openstack_cloud_from_module(module)
net_id = create_ovn_mac_address_network(result, conn)
tags = ['tripleo_stack_name={}'.format(stack),
'tripleo_role={}'.format(role_name)]
# no limit on concurrency, create a worker for every server
if concurrency < 1:
concurrency = len(servers)
jobs = []
exceptions = []
with futures.ThreadPoolExecutor(max_workers=concurrency) as p:
for server in servers:
jobs.append(p.submit(create_ovn_mac_address_ports,
result, conn, net_id, tags,
physnets, server))
for job in futures.as_completed(jobs):
e = job.exception()
if e:
exceptions.append(e)
if exceptions:
raise exceptions[0]
remove_obsolete_ports(result, conn, net_id, tags, servers,
physnets)
write_vars_file(conn, playbook_dir, net_id, tags, static_mappings)
result['success'] = True
module.exit_json(**result)
except Exception as err:
result['error'] = str(err)
result['msg'] = ('ERROR: Failed creating OVN MAC address resources!'
' {}'.format(err))
module.fail_json(**result)
def main():
run_module()
if __name__ == '__main__':
main()

View File

@ -184,17 +184,6 @@ def find_ctlplane_vip(conn, stack=None, service=None):
return port
def validate_playbook_dir(playbook_dir_path):
if not os.path.exists(playbook_dir_path):
raise Exception('ERROR: Playbook directory {} does not exist.'.format(
playbook_dir_path))
if not os.path.isdir(playbook_dir_path):
raise Exception(
'ERROR: Playbook directory {} is not a directory'.format(
playbook_dir_path))
def validate_service_vip_vars_file(service_vip_var_file):
if not os.path.isfile(service_vip_var_file):
raise Exception(
@ -208,7 +197,7 @@ def write_vars_file(port, service, playbook_dir):
ips = ips[0]
playbook_dir_path = os.path.abspath(playbook_dir)
validate_playbook_dir(playbook_dir)
network_data_v2.validate_playbook_dir(playbook_dir)
service_vip_var_file = os.path.join(playbook_dir_path,
'service_vip_vars.yaml')

View File

@ -1 +1,5 @@
{% if ovn_bridge_mac_address_vars is defined %}
{{ ovn_bridge_mac_address_vars[inventory_hostname] | default({}) | to_nice_json }}
{% else %}
{{ hostvars[inventory_hostname]['ovn_chassis_mac_map'] | default({}) | to_nice_json }}
{% endif %}

View File

@ -0,0 +1,160 @@
# Copyright 2021 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
#
import copy
import mock
import openstack
try:
from ansible.module_utils import network_data_v2 as n_utils
except ImportError:
from tripleo_ansible.ansible_plugins.module_utils import network_data_v2 as n_utils # noqa
from tripleo_ansible.ansible_plugins.modules import (
tripleo_ovn_mac_addresses as plugin)
from tripleo_ansible.tests import base as tests_base
from tripleo_ansible.tests import stubs
FAKE_NETWORK = stubs.FakeNeutronNetwork(
name=plugin.NET_NAME,
id='fake_ovn_mac_addr_net_id',
description=plugin.NET_DESCRIPTION
)
FAKE_PORT = stubs.FakeNeutronPort(
name='server-01_ovn_physnet_network01',
dns_name='server-01',
tags=['tripleo_ovn_physnet=network01', 'tripleo_stack_name=stack',
'tripleo_role_name=Compute']
)
@mock.patch.object(openstack.connection, 'Connection', autospec=True)
class TestTripleoOVNMacAddresses(tests_base.TestCase):
def setUp(self):
super(TestTripleoOVNMacAddresses, self).setUp()
# Helper function to convert array to generator
self.a2g = lambda x: (n for n in x)
def test_create_ovn_mac_address_network(self, mock_conn):
result = dict(changed=False)
mock_conn.network.find_network.return_value = None
mock_conn.network.create_network.return_value = FAKE_NETWORK
net_id = plugin.create_ovn_mac_address_network(result, mock_conn)
mock_conn.network.create_network.assert_called_with(
name=plugin.NET_NAME,
description=plugin.NET_DESCRIPTION)
self.assertTrue(result['changed'])
self.assertEqual(FAKE_NETWORK.id, net_id)
def test_create_ovn_mac_address_network_already_exists(self, mock_conn):
result = dict(changed=False)
mock_conn.network.find_network.return_value = FAKE_NETWORK
net_id = plugin.create_ovn_mac_address_network(result, mock_conn)
mock_conn.network.create_network.assert_not_called()
self.assertFalse(result['changed'])
self.assertEqual(FAKE_NETWORK.id, net_id)
def test_port_exists_port_not_found(self, mock_conn):
net_id = FAKE_NETWORK.id
tags = ['tripleo_stack_name=stack', 'tripleo_role_name=Compute']
name = 'server-01_ovn_physnet_network01'
mock_conn.network.ports.return_value = self.a2g([])
self.assertFalse(plugin.port_exists(mock_conn, net_id, tags, name))
mock_conn.network.ports.assert_called_with(network_id=net_id,
name=name, tags=tags)
def test_port_exists_port_found(self, mock_conn):
net_id = FAKE_NETWORK.id
tags = ['tripleo_stack_name=stack', 'tripleo_role_name=Compute']
name = 'server-01_ovn_physnet_network01'
mock_conn.network.ports.return_value = self.a2g([FAKE_PORT])
self.assertTrue(plugin.port_exists(mock_conn, net_id, tags, name))
mock_conn.network.ports.assert_called_with(network_id=net_id,
name=name, tags=tags)
@mock.patch.object(plugin, 'port_exists', autospec=True)
def test_create_ovn_mac_address_ports(self, mock_port_exists, mock_conn):
result = dict(changed=False)
tags = ['tripleo_stack_name=overcloud',
'tripleo_role=Controller']
physnets = ['net-a', 'net-b']
server = 'controller-0'
mock_port_exists.return_value = False
plugin.create_ovn_mac_address_ports(result, mock_conn,
FAKE_NETWORK.id, tags,
physnets, server)
mock_conn.network.create_port.assert_has_calls(
[mock.call(network_id=FAKE_NETWORK.id,
name=server + '_ovn_physnet_net-a',
dns_name=server),
mock.call(network_id=FAKE_NETWORK.id,
name=server + '_ovn_physnet_net-b',
dns_name=server)])
mock_conn.network.set_tags.assert_has_calls(
[mock.call(mock.ANY, tags + ['tripleo_ovn_physnet=net-a']),
mock.call(mock.ANY, tags + ['tripleo_ovn_physnet=net-b'])])
@mock.patch.object(plugin, 'port_exists', autospec=True)
def test_create_ovn_mac_address_ports_exists(self, mock_port_exists,
mock_conn):
result = dict(changed=False)
tags = ['tripleo_stack_name=overcloud',
'tripleo_role=Controller']
physnets = ['net-a', 'net-b']
server = 'controller-0.example.com'
mock_port_exists.return_value = True
plugin.create_ovn_mac_address_ports(result, mock_conn,
FAKE_NETWORK.id, tags,
physnets, server)
mock_conn.network.create_port.assert_not_called()
mock_conn.network.set_tags.assert_not_called()
def test_delete_ports_for_removed_nodes(self, mock_conn):
result = dict(changed=False)
servers = ['server-01', 'server-a', 'server-b']
physnets = ['network01', 'net-a', 'net-b']
mock_conn.network.ports.return_value = self.a2g([FAKE_PORT])
plugin.remove_obsolete_ports(result, mock_conn, 'net_id',
['fake_tags'], servers, physnets)
mock_conn.network.delete_port.assert_not_called()
self.assertFalse(result['changed'])
# Verify port is deleted if server was deleted, (scale down)
mock_conn.reset_mock()
mock_conn.network.ports.return_value = self.a2g([FAKE_PORT])
servers = ['server-a', 'server-b']
plugin.remove_obsolete_ports(result, mock_conn, 'net_id',
['fake_tags'], servers, physnets)
mock_conn.network.delete_port.assert_called_with(FAKE_PORT)
self.assertTrue(result['changed'])
# Verify port is deleted if physnet no longer in bridge mappings
mock_conn.reset_mock()
result = dict(changed=False)
mock_conn.network.ports.return_value = self.a2g([FAKE_PORT])
servers = ['server-01', 'server-a', 'server-b']
physnets = ['net-a', 'net-b']
plugin.remove_obsolete_ports(result, mock_conn, 'net_id',
['fake_tags'], servers, physnets)
mock_conn.network.delete_port.assert_called_with(FAKE_PORT)
self.assertTrue(result['changed'])