#!/usr/bin/env python # Copyright 2016 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. """switch_vlans module Used by the switch_vlans validation. """ import collections.abc as collectionsAbc import os.path import six from ansible.module_utils.basic import AnsibleModule # noqa from tripleo_validations import utils from yaml import safe_load as yaml_safe_load DOCUMENTATION = ''' --- module: switch_vlans short_description: Check configured VLANs against Ironic introspection data description: - Validate that the VLANs defined in TripleO nic config files are in the LLDP info received from network switches. The LLDP data is stored in Ironic introspection data per interface. - Used by the switch_vlans validation - Owned by the DF Networking options: path: required: true description: - The path of the base network environment file type: str template_files: required: true description: - A list of template files and contents type: list introspection_data: required: true description: - Introspection data for all nodes type: list author: "Bob Fournier" ''' EXAMPLES = ''' - hosts: undercloud tasks: - name: Check that switch vlans are present if used in nic-config files network_environment: path: environments/network-environment.yaml template_files: "{{ lookup('tht') }}" introspection_data: "{{ lookup('introspection_data', auth_url=auth_url.value, password=password.value) }}" ''' def open_network_environment_files(netenv_path, template_files): errors = [] try: network_data = yaml_safe_load(template_files[netenv_path]) except IOError as e: return ({}, {}, ["Can't open network environment file '{}': {}" .format(netenv_path, e)]) nic_configs = [] resource_registry = network_data.get('resource_registry', {}) for nic_name, relative_path in six.iteritems(resource_registry): if nic_name.endswith("Net::SoftwareConfig"): nic_config_path = os.path.normpath( os.path.join(os.path.dirname(netenv_path), relative_path)) try: nic_configs.append(( nic_name, nic_config_path, yaml_safe_load(template_files[nic_config_path]))) except IOError as e: errors.append( "Can't open the resource '{}' reference file '{}': {}" .format(nic_name, nic_config_path, e)) return (network_data, nic_configs, errors) def validate_switch_vlans(netenv_path, template_files, introspection_data): """Check if VLAN exists in introspection data for node :param netenv_path: path to network_environment file :param template_files: template files being checked :param introspection_data: introspection data for all node :returns warnings: List of warning messages errors: List of error messages """ network_data, nic_configs, errors =\ open_network_environment_files(netenv_path, template_files) warnings = [] vlans_in_templates = False # Store VLAN IDs from network-environment.yaml. vlaninfo = {} for item, data in six.iteritems(network_data.get('parameter_defaults', {})): if item.endswith('NetworkVlanID'): vlaninfo[item] = data # Get the VLANs which are actually used in nic configs for nic_config_name, nic_config_path, nic_config in nic_configs: resources = nic_config.get('resources') if not isinstance(nic_config, collectionsAbc.Mapping): return [], ["nic_config parameter must be a dictionary."] if not isinstance(resources, collectionsAbc.Mapping): return [], ["The nic_data must contain the 'resources' key " "and it must be a dictionary."] for name, resource in six.iteritems(resources): try: nested_path = [ ('properties', collectionsAbc.Mapping, 'dictionary'), ('config', collectionsAbc.Mapping, 'dictionary'), ('network_config', collectionsAbc.Iterable, 'list'), ] nw_config = utils.get_nested(resource, name, nested_path) except ValueError as e: errors.append('{}'.format(e)) continue # Not all resources contain a network config: if not nw_config: continue for elem in nw_config: # VLANs will be in bridge if elem['type'] == 'ovs_bridge' \ or elem['type'] == 'linux_bridge': for member in elem['members']: if member['type'] != 'vlan': continue vlans_in_templates = True vlan_id_str = member['vlan_id'] vlan_id = vlaninfo[vlan_id_str['get_param']] msg, result = vlan_exists_on_switch( vlan_id, introspection_data) warnings.extend(msg) if not msg and result is False: errors.append( "VLAN ID {} not on attached switch".format( vlan_id)) if not vlans_in_templates: warnings.append("No VLANs are used on templates files") return set(warnings), set(errors) def vlan_exists_on_switch(vlan_id, introspection_data): """Check if VLAN exists in introspection data :param vlan_id: VLAN id :param introspection_data: introspection data for all nodes :returns msg: Error or warning message result: boolean indicating if VLAN was found """ for node, data in introspection_data.items(): node_valid_lldp = False all_interfaces = data.get('all_interfaces', []) # Check lldp data on all interfaces for this vlan ID for interface in all_interfaces: lldp_proc = all_interfaces[interface].get('lldp_processed', {}) if lldp_proc: node_valid_lldp = True switch_vlans = lldp_proc.get('switch_port_vlans', []) if switch_vlans: if any(vlan['id'] == vlan_id for vlan in switch_vlans): return [], True # If no lldp data for node return warning, not possible to locate vlan if not node_valid_lldp: node_uuid = node.split("-", 1)[1] return ["LLDP data not available for node {}".format(node_uuid)],\ False return [], False # could not find VLAN ID def main(): module = AnsibleModule( argument_spec=yaml_safe_load(DOCUMENTATION)['options'] ) netenv_path = module.params.get('path') template_files = {name: content[1] for (name, content) in module.params.get('template_files')} introspection_data = {name: content for (name, content) in module.params.get('introspection_data')} warnings, errors = validate_switch_vlans(netenv_path, template_files, introspection_data) if errors: module.fail_json(msg="\n".join(errors)) elif warnings: module.exit_json(warnings="\n".join(warnings)) else: module.exit_json(msg="All VLANs configured on attached switches") if __name__ == '__main__': main()