#!/usr/bin/python # # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # Ansible is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # DOCUMENTATION = ''' --- module: nxos_pim_interface version_added: "2.2" short_description: Manages PIM interface configuration. description: - Manages PIM interface configuration settings. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) notes: - When C(state=default), supported params will be reset to a default state. These include C(dr_prio), C(hello_auth_key), C(hello_interval), C(jp_policy_out), C(jp_policy_in), C(jp_type_in), C(jp_type_out), C(border), C(neighbor_policy), C(neighbor_type). - The C(hello_auth_key) param is not idempotent. - C(hello_auth_key) only supports clear text passwords. - When C(state=absent), pim interface configuration will be set to defaults and pim-sm will be disabled on the interface. - PIM must be enabled on the device to use this module. - This module is for Layer 3 interfaces. options: interface: description: - Full name of the interface such as Ethernet1/33. required: true sparse: description: - Enable/disable sparse-mode on the interface. required: false default: true choices: ['true', 'false'] hello_auth_key: description: - Authentication for hellos on this interface. required: false default: null hello_interval: description: - Hello interval in milliseconds for this interface. required: false default: null choices: ['true', 'false'] jp_policy_out: description: - Policy for join-prune messages (outbound). required: true default: null jp_policy_in: description: - Policy for join-prune messages (inbound). required: false default: null jp_type_out: description: - Type of policy mapped to C(jp_policy_out). required: false default: null choices: ['prefix', 'routemap'] jp_type_in: description: - Type of policy mapped to C(jp_policy_in). required: false default: null choices: ['prefix', 'routemap'] border: description: - Configures interface to be a boundary of a PIM domain. required: false default: null choices: ['true', 'false'] neighbor_policy: description: - Configures a neighbor policy for filtering adjacencies. required: false default: null neighbor_type: description: - Type of policy mapped to neighbor_policy. required: false default: null choices: ['prefix', 'routemap'] state: description: - Manages desired state of the resource. required: false default: present choices: ['present', 'default'] ''' EXAMPLES = ''' # ensure PIM is not running on the interface - nxos_pim_interface: interface: eth1/33 state: absent host: {{ inventory_hostname }} username: {{ un }} password: {{ pwd }} # ensure the interface has pim-sm enabled with the appropriate priority and hello interval - nxos_pim_interface: interface: eth1/33 dr_prio: 10 hello_interval: 40 state: present host: {{ inventory_hostname }} username: {{ un }} password: {{ pwd }} # ensure join-prune policies exist - nxos_pim_interface: interface: eth1/33 jp_policy_in: JPIN jp_policy_out: JPOUT jp_type_in: routemap jp_type_out: routemap host: {{ inventory_hostname }} username: {{ un }} password: {{ pwd }} # ensure defaults are in place - nxos_pim_interface: interface: eth1/33 state: default host: {{ inventory_hostname }} username: {{ un }} password: {{ pwd }} ''' RETURN = ''' proposed: description: k/v pairs of parameters passed into module returned: always type: dict sample: {"interface": "eth1/33", "neighbor_policy": "test", "neighbor_type": "routemap", "sparse": true} existing: description: - k/v pairs of existing configuration type: dict sample: {"border": false, "dr_prio": "1", "hello_interval": "30000", "isauth": false, "jp_bidir": false, "jp_policy_in": "JPIN", "jp_policy_out": "1", "jp_type_in": "routemap", "jp_type_out": null, "neighbor_policy": "test1", "neighbor_type": "prefix", "sparse": true} end_state: description: k/v pairs of configuration after module execution returned: always type: dict sample: {"border": false, "dr_prio": "1", "hello_interval": "30000", "isauth": false, "jp_bidir": false, "jp_policy_in": "JPIN", "jp_policy_out": "1", "jp_type_in": "routemap", "jp_type_out": null, "neighbor_policy": "test", "neighbor_type": "routemap", "sparse": true} updates: description: command sent to the device returned: always type: list sample: ["interface eth1/33", "ip pim neighbor-policy test", "ip pim neighbor-policy test"] changed: description: check to see if a change was made on the device returned: always type: boolean sample: true ''' import json import time # COMMON CODE FOR MIGRATION import re from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine from ansible.module_utils.shell import ShellError try: from ansible.module_utils.nxos import get_module except ImportError: from ansible.module_utils.nxos import NetworkModule def to_list(val): if isinstance(val, (list, tuple)): return list(val) elif val is not None: return [val] else: return list() class CustomNetworkConfig(NetworkConfig): def expand_section(self, configobj, S=None): if S is None: S = list() S.append(configobj) for child in configobj.children: if child in S: continue self.expand_section(child, S) return S def get_object(self, path): for item in self.items: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item def to_block(self, section): return '\n'.join([item.raw for item in section]) def get_section(self, path): try: section = self.get_section_objects(path) return self.to_block(section) except ValueError: return list() def get_section_objects(self, path): if not isinstance(path, list): path = [path] obj = self.get_object(path) if not obj: raise ValueError('path does not exist in config') return self.expand_section(obj) def add(self, lines, parents=None): """Adds one or lines of configuration """ ancestors = list() offset = 0 obj = None ## global config command if not parents: for line in to_list(lines): item = ConfigLine(line) item.raw = line if item not in self.items: self.items.append(item) else: for index, p in enumerate(parents): try: i = index + 1 obj = self.get_section_objects(parents[:i])[0] ancestors.append(obj) except ValueError: # add parent to config offset = index * self.indent obj = ConfigLine(p) obj.raw = p.rjust(len(p) + offset) if ancestors: obj.parents = list(ancestors) ancestors[-1].children.append(obj) self.items.append(obj) ancestors.append(obj) # add child objects for line in to_list(lines): # check if child already exists for child in ancestors[-1].children: if child.text == line: break else: offset = len(parents) * self.indent item = ConfigLine(line) item.raw = line.rjust(len(line) + offset) item.parents = ancestors ancestors[-1].children.append(item) self.items.append(item) def get_network_module(**kwargs): try: return get_module(**kwargs) except NameError: return NetworkModule(**kwargs) def get_config(module, include_defaults=False): config = module.params['config'] if not config: try: config = module.get_config() except AttributeError: defaults = module.params['include_defaults'] config = module.config.get_config(include_defaults=defaults) return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: try: module.configure(commands) except AttributeError: module.config(commands) if save_config: try: module.config.save_config() except AttributeError: module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands return result # END OF COMMON CODE def execute_config_command(commands, module): try: module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) except AttributeError: try: commands.insert(0, 'configure') module.cli.add_commands(commands, output='config') module.cli.run_commands() except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) def get_cli_body_ssh(command, response, module, text=False): """Get response for when transport=cli. This is kind of a hack and mainly needed because these modules were originally written for NX-API. And not every command supports "| json" when using cli/ssh. As such, we assume if | json returns an XML string, it is a valid command, but that the resource doesn't exist yet. Instead, the output will be a raw string when issuing commands containing 'show run'. """ if 'xml' in response[0] or response[0] == '\n' or '^' in response[0]: body = [] elif 'show run' in command or text: body = response else: try: body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) return body def execute_show(cmds, module, command_type=None): command_type_map = { 'cli_show': 'json', 'cli_show_ascii': 'text' } try: if command_type: response = module.execute(cmds, command_type=command_type) else: response = module.execute(cmds) except ShellError: clie = get_exception() module.fail_json(msg='Error sending {0}'.format(cmds), error=str(clie)) except AttributeError: try: if command_type: command_type = command_type_map.get(command_type) module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() module.fail_json(msg='Error sending {0}'.format(cmds), error=str(clie)) return response def execute_show_command(command, module, command_type='cli_show', text=False): if module.params['transport'] == 'cli': if 'show run' not in command and text is False: command += ' | json' cmds = [command] response = execute_show(cmds, module) body = get_cli_body_ssh(command, response, module, text=text) elif module.params['transport'] == 'nxapi': cmds = [command] body = execute_show(cmds, module, command_type=command_type) return body def flatten_list(command_lists): flat_command_list = [] for command in command_lists: if isinstance(command, list): flat_command_list.extend(command) else: flat_command_list.append(command) return flat_command_list def local_existing(gexisting): jp_bidir = False isauth = False if gexisting: jp_bidir = gexisting.get('jp_bidir') isauth = gexisting.get('isauth') if jp_bidir and isauth: gexisting.pop('jp_bidir') gexisting.pop('isauth') gexisting['sparse'] = True return gexisting, jp_bidir, isauth def get_interface_type(interface): if interface.upper().startswith('ET'): return 'ethernet' elif interface.upper().startswith('VL'): return 'svi' elif interface.upper().startswith('LO'): return 'loopback' elif interface.upper().startswith('MG'): return 'management' elif interface.upper().startswith('MA'): return 'management' elif interface.upper().startswith('PO'): return 'portchannel' else: return 'unknown' def get_interface_mode(interface, intf_type, module): command = 'show interface {0}'.format(interface) mode = 'unknown' interface_table = {} body = execute_show_command(command, module) try: interface_table = body[0]['TABLE_interface']['ROW_interface'] except (KeyError, AttributeError, IndexError): return mode if intf_type in ['ethernet', 'portchannel']: mode = str(interface_table.get('eth_mode', 'layer3')) if mode in ['access', 'trunk']: mode = 'layer2' elif mode == 'routed': mode = 'layer3' elif intf_type in ['loopback', 'svi']: mode = 'layer3' return mode def get_pim_interface(module, interface): pim_interface = {} command = 'show ip pim interface {0}'.format(interface) body = execute_show_command(command, module, command_type='cli_show_ascii', text=True) if body: if 'not running' not in body[0]: body = execute_show_command(command, module) try: get_data = body[0]['TABLE_iod']['ROW_iod'] if isinstance(get_data.get('dr-priority'), unicode) or \ isinstance(get_data.get('dr-priority'), str): pim_interface['dr_prio'] = get_data.get('dr-priority') else: pim_interface['dr_prio'] = get_data.get('dr-priority')[0] hello_interval = get_data.get('hello-interval-sec') if hello_interval: hello_interval_msec = int(get_data.get('hello-interval-sec'))*1000 pim_interface['hello_interval'] = str(hello_interval_msec) border = get_data.get('is-border') if border == 'true': pim_interface['border'] = True elif border == 'false': pim_interface['border'] = False isauth = get_data.get('isauth-config') if isauth == 'true': pim_interface['isauth'] = True elif isauth == 'false': pim_interface['isauth'] = False pim_interface['neighbor_policy'] = get_data.get('nbr-policy-name') if pim_interface['neighbor_policy'] == 'none configured': pim_interface['neighbor_policy'] = None jp_in_policy = get_data.get('jp-in-policy-name') pim_interface['jp_policy_in'] = jp_in_policy if jp_in_policy == 'none configured': pim_interface['jp_policy_in'] = None if isinstance(get_data.get('jp-out-policy-name'), unicode) or \ isinstance(get_data.get('jp-out-policy-name'), str): pim_interface['jp_policy_out'] = get_data.get('jp-out-policy-name') else: pim_interface['jp_policy_out'] = get_data.get( 'jp-out-policy-name')[0] if pim_interface['jp_policy_out'] == 'none configured': pim_interface['jp_policy_out'] = None except (KeyError, AttributeError, TypeError, IndexError): return {} command = 'show run interface {0}'.format(interface) body = execute_show_command(command, module, command_type='cli_show_ascii') jp_configs = [] neigh = None if body: all_lines = body[0].splitlines() for each in all_lines: if 'jp-policy' in each: jp_configs.append(str(each.strip())) elif 'neighbor-policy' in each: neigh = str(each) pim_interface['neighbor_type'] = None neigh_type = None if neigh: if 'prefix-list' in neigh: neigh_type = 'prefix' else: neigh_type = 'routemap' pim_interface['neighbor_type'] = neigh_type len_existing = len(jp_configs) list_of_prefix_type = len([x for x in jp_configs if 'prefix-list' in x]) jp_type_in = None jp_type_out = None jp_bidir = False if len_existing == 1: # determine type last_word = jp_configs[0].split(' ')[-1] if last_word == 'in': if list_of_prefix_type: jp_type_in = 'prefix' else: jp_type_in = 'routemap' elif last_word == 'out': if list_of_prefix_type: jp_type_out = 'prefix' else: jp_type_out = 'routemap' else: jp_bidir = True if list_of_prefix_type: jp_type_in = 'prefix' jp_type_out = 'routemap' else: jp_type_in = 'routemap' jp_type_out = 'routemap' else: for each in jp_configs: last_word = each.split(' ')[-1] if last_word == 'in': if 'prefix-list' in each: jp_type_in = 'prefix' else: jp_type_in = 'routemap' elif last_word == 'out': if 'prefix-list' in each: jp_type_out = 'prefix' else: jp_type_out = 'routemap' pim_interface['jp_type_in'] = jp_type_in pim_interface['jp_type_out'] = jp_type_out pim_interface['jp_bidir'] = jp_bidir return pim_interface def fix_delta(delta, existing): if delta.get('sparse') is False and existing.get('sparse') is None: delta.pop('sparse') return delta def config_pim_interface(delta, existing, jp_bidir, isauth): command = None commands = [] delta = fix_delta(delta, existing) CMDS = { 'sparse': 'ip pim sparse-mode', 'dr_prio': 'ip pim dr-priority {0}', 'hello_interval': 'ip pim hello-interval {0}', 'hello_auth_key': 'ip pim hello-authentication ah-md5 {0}', 'border': 'ip pim border', 'jp_policy_out': 'ip pim jp-policy prefix-list {0} out', 'jp_policy_in': 'ip pim jp-policy prefix-list {0} in', 'jp_type_in': '', 'jp_type_out': '', 'neighbor_policy': 'ip pim neighbor-policy prefix-list {0}', 'neighbor_type': '' } if jp_bidir: if delta.get('jp_policy_in') or delta.get('jp_policy_out'): if existing.get('jp_type_in') == 'prefix': command = 'no ip pim jp-policy prefix-list {0}'.format( existing.get('jp_policy_in') ) else: command = 'no ip pim jp-policy {0}'.format( existing.get('jp_policy_in') ) if command: commands.append(command) for k, v in delta.iteritems(): if k in ['dr_prio', 'hello_interval', 'hello_auth_key', 'border', 'sparse']: if v: command = CMDS.get(k).format(v) elif k == 'hello_auth_key': if isauth: command = 'no ip pim hello-authentication ah-md5' else: command = 'no ' + CMDS.get(k).format(v) if command: commands.append(command) elif k in ['neighbor_policy', 'jp_policy_in', 'jp_policy_out', 'neighbor_type']: if k in ['neighbor_policy', 'neighbor_type']: temp = delta.get('neighbor_policy') or existing.get( 'neighbor_policy') if delta.get('neighbor_type') == 'prefix': command = CMDS.get(k).format(temp) elif delta.get('neighbor_type') == 'routemap': command = 'ip pim neighbor-policy {0}'.format(temp) elif existing.get('neighbor_type') == 'prefix': command = CMDS.get(k).format(temp) elif existing.get('neighbor_type') == 'routemap': command = 'ip pim neighbor-policy {0}'.format(temp) elif k in ['jp_policy_in', 'jp_type_in']: temp = delta.get('jp_policy_in') or existing.get( 'jp_policy_in') if delta.get('jp_type_in') == 'prefix': command = CMDS.get(k).format(temp) elif delta.get('jp_type_in') == 'routemap': command = 'ip pim jp-policy {0} in'.format(temp) elif existing.get('jp_type_in') == 'prefix': command = CMDS.get(k).format(temp) elif existing.get('jp_type_in') == 'routemap': command = 'ip pim jp-policy {0} in'.format(temp) elif k in ['jp_policy_out', 'jp_type_out']: temp = delta.get('jp_policy_out') or existing.get( 'jp_policy_out') if delta.get('jp_type_out') == 'prefix': command = CMDS.get(k).format(temp) elif delta.get('jp_type_out') == 'routemap': command = 'ip pim jp-policy {0} out'.format(temp) elif existing.get('jp_type_out') == 'prefix': command = CMDS.get(k).format(temp) elif existing.get('jp_type_out') == 'routemap': command = 'ip pim jp-policy {0} out'.format(temp) if command: commands.append(command) command = None return commands def get_pim_interface_defaults(): dr_prio = '1' border = False hello_interval = '30000' hello_auth_key = False args = dict(dr_prio=dr_prio, border=border, hello_interval=hello_interval, hello_auth_key=hello_auth_key) default = dict((param, value) for (param, value) in args.iteritems() if value is not None) return default def default_pim_interface_policies(existing, jp_bidir): commands = [] if jp_bidir: if existing.get('jp_policy_in') or existing.get('jp_policy_out'): if existing.get('jp_type_in') == 'prefix': command = 'no ip pim jp-policy prefix-list {0}'.format( existing.get('jp_policy_in') ) if command: commands.append(command) elif not jp_bidir: command = None for k, v in existing.iteritems(): if k == 'jp_policy_in': if existing.get('jp_policy_in'): if existing.get('jp_type_in') == 'prefix': command = 'no ip pim jp-policy prefix-list {0} in'.format( existing.get('jp_policy_in') ) else: command = 'no ip pim jp-policy {0} in'.format( existing.get('jp_policy_in') ) elif k == 'jp_policy_out': if existing.get('jp_policy_out'): if existing.get('jp_type_out') == 'prefix': command = 'no ip pim jp-policy prefix-list {0} out'.format( existing.get('jp_policy_out') ) else: command = 'no ip pim jp-policy {0} out'.format( existing.get('jp_policy_out') ) if command: commands.append(command) command = None if existing.get('neighbor_policy'): command = 'no ip pim neighbor-policy' commands.append(command) return commands def config_pim_interface_defaults(existing, jp_bidir, isauth): command = [] # returns a dict defaults = get_pim_interface_defaults() delta = dict(set(defaults.iteritems()).difference( existing.iteritems())) if delta: # returns a list command = config_pim_interface(delta, existing, jp_bidir, isauth) comm = default_pim_interface_policies(existing, jp_bidir) if comm: for each in comm: command.append(each) return command def main(): argument_spec=dict( interface=dict(required=True), sparse=dict(type='bool', default=True), dr_prio=dict(type='str'), hello_auth_key=dict(type='str'), hello_interval=dict(type='int'), jp_policy_out=dict(type='str'), jp_policy_in=dict(type='str'), jp_type_out=dict(choices=['prefix', 'routemap']), jp_type_in=dict(choices=['prefix', 'routemap']), border=dict(type='bool'), neighbor_policy=dict(type='str'), neighbor_type=dict(choices=['prefix', 'routemap']), state=dict(choices=['present', 'absent', 'default'], default='present'), ) module = get_network_module(argument_spec=argument_spec, supports_check_mode=True) state = module.params['state'] sparse = module.params['sparse'] interface = module.params['interface'] jp_type_in = module.params['jp_type_in'] jp_type_out = module.params['jp_type_out'] jp_policy_in = module.params['jp_policy_in'] jp_policy_out = module.params['jp_policy_out'] neighbor_policy = module.params['neighbor_policy'] neighbor_type = module.params['neighbor_type'] hello_interval = module.params['hello_interval'] intf_type = get_interface_type(interface) if get_interface_mode(interface, intf_type, module) == 'layer2': module.fail_json(msg='this module only works on Layer 3 interfaces.') if jp_policy_in: if not jp_type_in: module.fail_json(msg='jp_type_in required when using jp_policy_in.') if jp_policy_out: if not jp_type_out: module.fail_json(msg='jp_type_out required when using ' ' jp_policy_out.') if neighbor_policy: if not neighbor_type: module.fail_json(msg='neighbor_type required when using ' 'neighbor_policy.') get_existing = get_pim_interface(module, interface) existing, jp_bidir, isauth = local_existing(get_existing) end_state = existing changed = False commands = [] args = [ 'interface', 'sparse', 'dr_prio', 'hello_auth_key', 'hello_interval', 'jp_policy_out', 'jp_type_out', 'jp_type_in', 'jp_policy_in', 'border', 'neighbor_type', 'neighbor_policy' ] proposed = dict((k, v) for k, v in module.params.iteritems() if v is not None and k in args) ''' CANNOT_ABSENT = ['dr_prio', 'hello_interval', 'hello_auth_key', 'jp_policy_out', 'jp_policy_in', 'jp_type_out', 'jp_type_in', 'border', 'neighbor_type', 'neighbor_policy'] ''' if hello_interval: proposed['hello_interval'] = str(proposed['hello_interval'] * 1000) delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) if state == 'present': if delta: command = config_pim_interface(delta, existing, jp_bidir, isauth) if command: commands.append(command) elif state == 'default': defaults = config_pim_interface_defaults(existing, jp_bidir, isauth) if defaults: commands.append(defaults) elif state == 'absent': if existing.get('sparse') == True: delta['sparse'] = False # defaults is a list of commands defaults = config_pim_interface_defaults(existing, jp_bidir, isauth) if defaults: commands.append(defaults) command = config_pim_interface(delta, existing, jp_bidir, isauth) commands.append(command) if commands: commands.insert(0, ['interface {0}'.format(interface)]) cmds = flatten_list(commands) results = {} if cmds: if module.check_mode: module.exit_json(changed=True, commands=cmds) else: changed = True execute_config_command(cmds, module) time.sleep(1) get_existing = get_pim_interface(module, interface) end_state, jp_bidir, isauth = local_existing(get_existing) if 'configure' in cmds: cmds.pop(0) results['proposed'] = proposed results['existing'] = existing results['updates'] = cmds results['changed'] = changed results['end_state'] = end_state module.exit_json(**results) if __name__ == '__main__': main()