#!/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 ''' 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, project_id): network = conn.network.find_network(NET_NAME) if network is None: network = conn.network.create_network(name=NET_NAME, description=NET_DESCRIPTION, project_id=project_id) 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, project_id): 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, project_id=project_id) 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'] or [] servers = module.params.get('server_resource_names') or [] 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) project_id = network_data_v2.get_project_id(conn) net_id = create_ovn_mac_address_network( result, conn, project_id) tags = ['tripleo_stack_name={}'.format(stack)] if role_name: tags.append('tripleo_role={}'.format(role_name)) # no limit on concurrency, create a worker for every server if concurrency < 1: concurrency = len(servers) if 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, project_id)) for job in futures.as_completed(jobs): e = job.exception() if e: exceptions.append(e) if exceptions: raise exceptions[0] try: remove_obsolete_ports(result, conn, net_id, tags, servers, physnets) except Exception: pass if static_mappings or servers: 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()