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:
parent
cca4da0bbc
commit
d48af77c95
14
doc/source/modules/modules-tripleo_ovn_mac_addresses.rst
Normal file
14
doc/source/modules/modules-tripleo_ovn_mac_addresses.rst
Normal 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
|
@ -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:
|
||||
|
@ -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()
|
@ -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')
|
||||
|
@ -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 %}
|
||||
|
160
tripleo_ansible/tests/modules/test_tripleo_ovn_mac_addresses.py
Normal file
160
tripleo_ansible/tests/modules/test_tripleo_ovn_mac_addresses.py
Normal 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'])
|
Loading…
Reference in New Issue
Block a user