#!/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 . # ANSIBLE_METADATA = {'status': ['preview'], 'supported_by': 'community', 'version': '1.0'} DOCUMENTATION = ''' --- module: nxos_feature version_added: "2.1" short_description: Manage features in NX-OS switches. description: - Offers ability to enable and disable features in NX-OS. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) options: feature: description: - Name of feature. required: true state: description: - Desired state of the feature. required: false default: 'enabled' choices: ['enabled','disabled'] ''' EXAMPLES = ''' - name: Ensure lacp is enabled nxos_feature: feature: lacp state: enabled host: "{{ inventory_hostname }}" - name: Ensure ospf is disabled nxos_feature: feature: ospf state: disabled host: "{{ inventory_hostname }}" - name: Ensure vpc is enabled nxos_feature: feature: vpc state: enabled host: "{{ inventory_hostname }}" ''' RETURN = ''' proposed: description: proposed feature state returned: always type: dict sample: {"state": "disabled"} existing: description: existing state of feature returned: always type: dict sample: {"state": "enabled"} end_state: description: feature state after executing module returned: always type: dict sample: {"state": "disabled"} updates: description: commands sent to the device returned: always type: list sample: ["no feature eigrp"] changed: description: check to see if a change was made on the device returned: always type: boolean sample: true feature: description: the feature that has been examined returned: always type: string sample: "vpc" ''' import json import collections # 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): """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. """ if 'xml' in response[0]: body = [] 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'): if module.params['transport'] == 'cli': command += ' | json' cmds = [command] response = execute_show(cmds, module) body = get_cli_body_ssh(command, response, module) elif module.params['transport'] == 'nxapi': cmds = [command] body = execute_show(cmds, module, command_type=command_type) return body def apply_key_map(key_map, table): new_dict = {} for key, value in table.items(): new_key = key_map.get(key) if new_key: value = table.get(key) if value: new_dict[new_key] = str(value) else: new_dict[new_key] = value return new_dict def get_available_features(feature, module): available_features = {} feature_regex = '(?P\S+)\s+\d+\s+(?P.*)' command = 'show feature' body = execute_show_command(command, module, command_type='cli_show_ascii') split_body = body[0].splitlines() for line in split_body: try: match_feature = re.match(feature_regex, line, re.DOTALL) feature_group = match_feature.groupdict() feature = feature_group['feature'] state = feature_group['state'] except AttributeError: feature = '' state = '' if feature and state: if 'enabled' in state: state = 'enabled' if feature not in available_features: available_features[feature] = state else: if (available_features[feature] == 'disabled' and state == 'enabled'): available_features[feature] = state return available_features def get_commands(proposed, existing, state, module): feature = validate_feature(module, mode='config') commands = [] feature_check = proposed == existing if not feature_check: if state == 'enabled': command = 'feature {0}'.format(feature) commands.append(command) elif state == 'disabled': command = "no feature {0}".format(feature) commands.append(command) return commands def validate_feature(module, mode='show'): '''Some features may need to be mapped due to inconsistency between how they appear from "show feature" output and how they are configured''' feature = module.params['feature'] feature_to_be_mapped = { 'show': { 'nv overlay': 'nve', 'vn-segment-vlan-based': 'vnseg_vlan', 'hsrp': 'hsrp_engine', 'fabric multicast': 'fabric_mcast', 'scp-server': 'scpServer', 'sftp-server': 'sftpServer', 'sla responder': 'sla_responder', 'sla sender': 'sla_sender', 'ssh': 'sshServer', 'tacacs+': 'tacacs', 'telnet': 'telnetServer', 'ethernet-link-oam': 'elo', 'port-security': 'eth_port_sec' }, 'config': { 'nve': 'nv overlay', 'vnseg_vlan': 'vn-segment-vlan-based', 'hsrp_engine': 'hsrp', 'fabric_mcast': 'fabric multicast', 'scpServer': 'scp-server', 'sftpServer': 'sftp-server', 'sla_sender': 'sla sender', 'sla_responder': 'sla responder', 'sshServer': 'ssh', 'tacacs': 'tacacs+', 'telnetServer': 'telnet', 'elo': 'ethernet-link-oam', 'eth_port_sec': 'port-security' } } if feature in feature_to_be_mapped[mode]: feature = feature_to_be_mapped[mode][feature] return feature def main(): argument_spec = dict( feature=dict(type='str', required=True), state=dict(choices=['enabled', 'disabled'], default='enabled', required=False), include_defaults=dict(default=False), config=dict(), save=dict(type='bool', default=False) ) module = get_network_module(argument_spec=argument_spec, supports_check_mode=True) feature = validate_feature(module) state = module.params['state'].lower() available_features = get_available_features(feature, module) if feature not in available_features: module.fail_json( msg='Invalid feature name.', features_currently_supported=available_features, invalid_feature=feature) else: existstate = available_features[feature] existing = dict(state=existstate) proposed = dict(state=state) changed = False end_state = existing cmds = get_commands(proposed, existing, state, module) if cmds: if module.check_mode: module.exit_json(changed=True, commands=cmds) else: execute_config_command(cmds, module) changed = True updated_features = get_available_features(feature, module) existstate = updated_features[feature] end_state = dict(state=existstate) if 'configure' in cmds: cmds.pop(0) results = {} results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state results['updates'] = cmds results['changed'] = changed results['feature'] = module.params['feature'] module.exit_json(**results) if __name__ == '__main__': main()