From 22772e75cf1d9c72b6e60eb6aac2ede83f9bef92 Mon Sep 17 00:00:00 2001 From: Jason Edelman Date: Fri, 26 Aug 2016 13:27:26 -0400 Subject: [PATCH 001/770] removed feature check for nxos_interface --- network/nxos/nxos_interface.py | 36 ---------------------------------- 1 file changed, 36 deletions(-) diff --git a/network/nxos/nxos_interface.py b/network/nxos/nxos_interface.py index 46b9f4ef8e4..412895e19b3 100644 --- a/network/nxos/nxos_interface.py +++ b/network/nxos/nxos_interface.py @@ -157,33 +157,6 @@ def is_default_interface(interface, module): return 'DNE' -def get_available_features(feature, module): - available_features = {} - command = 'show feature' - body = execute_show_command(command, module) - - try: - body = body[0]['TABLE_cfcFeatureCtrlTable']['ROW_cfcFeatureCtrlTable'] - except (TypeError, IndexError): - return available_features - - for each_feature in body: - feature = each_feature['cfcFeatureCtrlName2'] - state = each_feature['cfcFeatureCtrlOpStatus2'] - - if 'enabled' in state: - state = 'enabled' - - if feature not in available_features.keys(): - available_features[feature] = state - else: - if (available_features[feature] == 'disabled' and - state == 'enabled'): - available_features[feature] = state - - return available_features - - def get_interface_type(interface): """Gets the type of interface @@ -626,15 +599,6 @@ def main(): if normalized_interface == 'Vlan1' and state == 'absent': module.fail_json(msg='ERROR: CANNOT REMOVE VLAN 1!') - if intf_type == 'svi': - feature = 'interface-vlan' - available_features = get_available_features(feature, module) - svi_state = available_features[feature] - if svi_state == 'disabled': - module.fail_json( - msg='SVI (interface-vlan) feature needs to be enabled first', - ) - if intf_type == 'unknown': module.fail_json( msg='unknown interface type found-1', From 6152328abbd0749f0b02e7138ddabf6b339f6da4 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Mon, 29 Aug 2016 21:10:29 -0700 Subject: [PATCH 002/770] Added Command and Config modules to support Dell Networking OS10 device --- network/dnos10_command.py | 208 ++++++++++++++++++++++++++++++ network/dnos10_config.py | 260 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 468 insertions(+) create mode 100644 network/dnos10_command.py create mode 100644 network/dnos10_config.py diff --git a/network/dnos10_command.py b/network/dnos10_command.py new file mode 100644 index 00000000000..fbee7da3c5f --- /dev/null +++ b/network/dnos10_command.py @@ -0,0 +1,208 @@ +#!/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: dnos10_command +version_added: "2.2" +author: "Senthil Kumar Ganesan (@skg_net) +short_description: Run commands on remote devices running Dell OS10 +description: + - Sends arbitrary commands to an Dell OS10 node and returns the results + read from the device. This module includes an + argument that will cause the module to wait for a specific condition + before returning or timing out if the condition is not met. + - This module does not support running commands in configuration mode. + Please use M(dnos10_config) to configure Dell OS10 devices. +extends_documentation_fragment: dnos10 +options: + commands: + description: + - List of commands to send to the remote dnos10 device over the + configured provider. The resulting output from the command + is returned. If the I(wait_for) argument is provided, the + module is not returned until the condition is satisfied or + the number of retries has expired. + required: true + wait_for: + description: + - List of conditions to evaluate against the output of the + command. The task will wait for each condition to be true + before moving forward. If the conditional is not true + within the configured number of retries, the task fails. + See examples. + required: false + default: null + aliases: ['waitfor'] + retries: + description: + - Specifies the number of retries a command should by tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the + I(wait_for) conditions. + required: false + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + required: false + default: 1 +""" + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: admin + password: admin + transport: cli + +tasks: + - name: run show version on remote devices + dnos10_command: + commands: show version + provider "{{ cli }}" + + - name: run show version and check to see if output contains OS10 + dnos10_command: + commands: show version + wait_for: result[0] contains OS10 + provider "{{ cli }}" + + - name: run multiple commands on remote nodes + dnos10_command: + commands: + - show version + - show interface + provider "{{ cli }}" + + - name: run multiple commands and evaluate the output + dnos10_command: + commands: + - show version + - show interface + wait_for: + - result[0] contains OS10 + - result[1] contains Ethernet + provider: "{{ cli }}" +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always + type: list + sample: ['...', '...'] + +stdout_lines: + description: The value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] + +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] + +warnings: + description: The list of warnings (if any) generated by module based on arguments + returned: always + type: list + sample: ['...', '...'] +""" +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner, FailedConditionsError +from ansible.module_utils.dnos10 import NetworkModule, NetworkError + +def to_lines(stdout): + for item in stdout: + if isinstance(item, basestring): + item = str(item).split('\n') + yield item + +def main(): + spec = dict( + commands=dict(type='list', required=True), + wait_for=dict(type='list', aliases=['waitfor']), + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + module = NetworkModule(argument_spec=spec, + connect_on_load=False, + supports_check_mode=True) + + commands = module.params['commands'] + conditionals = module.params['wait_for'] or list() + + warnings = list() + + runner = CommandRunner(module) + + for cmd in commands: + if module.check_mode and not cmd.startswith('show'): + warnings.append('only show commands are supported when using ' + 'check mode, not executing `%s`' % cmd) + else: + if cmd.startswith('conf'): + module.fail_json(msg='dnos10_command does not support running ' + 'config mode commands. Please use ' + 'dnos10_config instead') + runner.add_command(cmd) + + for item in conditionals: + runner.add_conditional(item) + + runner.retries = module.params['retries'] + runner.interval = module.params['interval'] + + try: + runner.run() + except FailedConditionsError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) + + result = dict(changed=False) + + result['stdout'] = list() + for cmd in commands: + try: + output = runner.get_command(cmd) + except ValueError: + output = 'command not executed due to check_mode, see warnings' + result['stdout'].append(output) + + + result['warnings'] = warnings + result['stdout_lines'] = list(to_lines(result['stdout'])) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() diff --git a/network/dnos10_config.py b/network/dnos10_config.py new file mode 100644 index 00000000000..7004b76b113 --- /dev/null +++ b/network/dnos10_config.py @@ -0,0 +1,260 @@ +#!/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: dnos10_config +version_added: "2.2" +author: "Senthil Kumar Ganesan (@skg_net) +short_description: Manage Dell OS10 configuration sections +description: + - Dell OS10 configurations use a simple block indent file sytanx + for segementing configuration into sections. This module provides + an implementation for working with Dell OS10 configuration sections in + a deterministic way. +extends_documentation_fragment: dnos10 +options: + lines: + description: + - The ordered set of commands that should be configured in the + section. The commands must be the exact same commands as found + in the device running-config. Be sure to note the configuration + command syntax as some commands are automatically modified by the + device config parser. + required: false + default: null + aliases: ['commands'] + parents: + description: + - The ordered set of parents that uniquely identify the section + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top + level or global commands. + required: false + default: null + src: + description: + - Specifies the source path to the file that contains the configuration + or configuration template to load. The path to the source file can + either be the full path on the Ansible control host or a relative + path from the playbook or role root dir. This argument is mutually + exclusive with O(lines). + required: false + default: null + before: + description: + - The ordered set of commands to push on to the command stack if + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing + any changes without affecting how the set of commands are matched + against the system + required: false + default: null + after: + description: + - The ordered set of commands to append to the end of the command + stack if a changed needs to be made. Just like with I(before) this + allows the playbook designer to append a set of commands to be + executed after the command set. + required: false + default: null + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. If + match is set to I(line), commands are matched line by line. If + match is set to I(strict), command lines are matched with respect + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. + required: false + default: line + choices: ['line', 'strict', 'exact', 'none'] + replace: + description: + - Instructs the module on the way to perform the configuration + on the device. If the replace argument is set to I(line) then + the modified lines are pushed to the device in configuration + mode. If the replace argument is set to I(block) then the entire + command block is pushed to the device in configuration mode if any + line is not correct + required: false + default: line + choices: ['line', 'block'] + update_config: + description: + - This arugment will either cause or prevent the changed commands + from being sent to the remote device. The set to true, the + remote Dell OS10 device will be configured with the updated commands + and when set to false, the remote device will not be updated. + required: false + default: yes + choices: ['yes', 'no'] + backup_config: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no + choices: ['yes', 'no'] +""" + +EXAMPLES = """ +- dnos10_config: + lines: ['hostname {{ inventory_hostname }}'] + force: yes + +- dnos10_config: + lines: + - 10 permit ip host 1.1.1.1 any log + - 20 permit ip host 2.2.2.2 any log + - 30 permit ip host 3.3.3.3 any log + - 40 permit ip host 4.4.4.4 any log + - 50 permit ip host 5.5.5.5 any log + parents: ['ip access-list extended test'] + before: ['no ip access-list extended test'] + match: exact + +- dnos10_config: + lines: + - 10 permit ip host 1.1.1.1 any log + - 20 permit ip host 2.2.2.2 any log + - 30 permit ip host 3.3.3.3 any log + - 40 permit ip host 4.4.4.4 any log + parents: ['ip access-list extended test'] + before: ['no ip access-list extended test'] + replace: block + +- dnos10_config: + commands: "{{lookup('file', 'datcenter1.txt')}}" + parents: ['ip access-list test'] + before: ['no ip access-list test'] + replace: block + +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + retured: when not check_mode + type: list + sample: ['...', '...'] +""" +from ansible.module_utils.netcfg import NetworkConfig, dumps, ConfigLine +from ansible.module_utils.dnos10 import NetworkModule, dnos10_argument_spec +from ansible.module_utils.dnos10 import get_config, get_sublevel_config + +def get_candidate(module): + candidate = NetworkConfig(indent=1) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) + return candidate + + +def main(): + + argument_spec = dict( + lines=dict(aliases=['commands'], type='list'), + parents=dict(type='list'), + + src=dict(type='path'), + + before=dict(type='list'), + after=dict(type='list'), + + match=dict(default='line', + choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + + update_config=dict(type='bool', default=True), + backup_config=dict(type='bool', default=False) + ) + argument_spec.update(dnos10_argument_spec) + + mutually_exclusive = [('lines', 'src')] + + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + module.check_mode = not module.params['update_config'] + + parents = module.params['parents'] or list() + + match = module.params['match'] + replace = module.params['replace'] + before = module.params['before'] + result = dict(changed=False, saved=False) + + candidate = get_candidate(module) + + if module.params['match'] != 'none': + config = get_config(module) + if parents: + contents = get_sublevel_config(config, module) + config = NetworkConfig(contents=contents, indent=1) + configobjs = candidate.difference(config, match=match, replace=replace) + + else: + configobjs = candidate.items + + if module.params['backup_config']: + result['__backup__'] = module.cli('show running-config')[0] + + commands = list() + if configobjs: + commands = dumps(configobjs, 'commands') + commands = commands.split('\n') + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + if not module.check_mode: + response = module.config.load_config(commands) + result['responses'] = response + + if module.params['save_config']: + module.config.save_config() + result['saved'] = True + + result['changed'] = True + + result['updates'] = commands + result['connected'] = module.connected + + module.exit_json(**result) + +if __name__ == '__main__': + main() From b7480a34d5d2e9c946894c7834d704482be93193 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Mon, 29 Aug 2016 21:57:06 -0700 Subject: [PATCH 003/770] Moved the modules to Dell folder --- network/{ => dell}/dnos10_command.py | 2 +- network/{ => dell}/dnos10_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename network/{ => dell}/dnos10_command.py (99%) rename network/{ => dell}/dnos10_config.py (99%) diff --git a/network/dnos10_command.py b/network/dell/dnos10_command.py similarity index 99% rename from network/dnos10_command.py rename to network/dell/dnos10_command.py index fbee7da3c5f..aa682711b5b 100644 --- a/network/dnos10_command.py +++ b/network/dell/dnos10_command.py @@ -20,7 +20,7 @@ --- module: dnos10_command version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg_net) +author: "Senthil Kumar Ganesan (@skg_net)" short_description: Run commands on remote devices running Dell OS10 description: - Sends arbitrary commands to an Dell OS10 node and returns the results diff --git a/network/dnos10_config.py b/network/dell/dnos10_config.py similarity index 99% rename from network/dnos10_config.py rename to network/dell/dnos10_config.py index 7004b76b113..3d7a0477d5a 100644 --- a/network/dnos10_config.py +++ b/network/dell/dnos10_config.py @@ -20,7 +20,7 @@ --- module: dnos10_config version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg_net) +author: "Senthil Kumar Ganesan (@skg_net)" short_description: Manage Dell OS10 configuration sections description: - Dell OS10 configurations use a simple block indent file sytanx From 405d3bfc5b75ff9328a36332a7b412e6c4284757 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 30 Aug 2016 13:30:38 +0200 Subject: [PATCH 004/770] Adding nxos_bgp.py --- network/nxos/nxos_bgp.py | 1194 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1194 insertions(+) create mode 100644 network/nxos/nxos_bgp.py diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py new file mode 100644 index 00000000000..132174ee6e9 --- /dev/null +++ b/network/nxos/nxos_bgp.py @@ -0,0 +1,1194 @@ +#!/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_bgp +version_added: "2.2" +short_description: Manages BGP configuration +description: + - Manages BGP configurations on NX-OS switches +author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - State 'absent' removes the whole BGP ASN configuration when VRF is + 'default' or the whole VRF instance within the BGP process when using + a different VRF. + - 'default', when supported, restores params default value. + - Configuring global parmas is only permitted if VRF is 'default'. +options: + asn: + description: + - BGP autonomous system number. Valid values are String, + Integer in ASPLAIN or ASDOT notation. + required: true + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing the global bgp. + required: false + default: null + bestpath_always_compare_med: + description: + - Enable/Disable MED comparison on paths from different autonomous systems. + required: false + choices: ['true','false', 'default'] + default: null + bestpath_aspath_multipath_relax: + description: + - Enable/Disable load sharing across the providers with + different (but equal-length) AS paths. + required: false + choices: ['true','false', 'default'] + default: null + bestpath_compare_routerid: + description: + - Enable/Disable comparison of router IDs for identical eBGP paths. + required: false + choices: ['true','false', 'default'] + default: null + bestpath_cost_community_ignore: + description: + - Enable/Disable Ignores the cost community for BGP best-path + calculations. + required: false + choices: ['true','false', 'default'] + default: null + bestpath_med_confed: + description: + - Enable/Disable enforcement of bestpath to do a MED comparison + only between paths originated within a confederation. + required: false + choices: ['true','false', 'default'] + default: null + bestpath_med_missing_as_worst: + description: + - Enable/Disable assigns the value of infinity to received routes that + do not carry the MED attribute, making these routes the least desirable. + required: false + choices: ['true','false', 'default'] + default: null + bestpath_med_non_deterministic: + description: + - Enable/Disable deterministic selection of the best MED path from among + the paths from the same autonomous system. + required: false + choices: ['true','false', 'default'] + default: null + cluster_id: + description: + - Route Reflector Cluster-ID. + required: false + default: null + confederation_id: + description: + - Routing domain confederation AS. + required: false + default: null + confederation_peers: + description: + - AS confederation parameters. + required: false + default: null + disable_policy_batching: + description: + - Enable/Disable the batching evaluation of prefix advertisements to all peers. + required: false + choices: ['true','false', 'default'] + default: null + disable_policy_batching_ipv4_prefix_list: + description: + - Enable/Disable the batching evaluation of prefix advertisements to all + peers with prefix list. + required: false + default: null + disable_policy_batching_ipv6_prefix_list: + description: + - Enable/Disable the batching evaluation of prefix advertisements to all peers with prefix list. + required: false + enforce_first_as: + description: + - Enable/Disable enforces the neighbor autonomous system to be the first AS number + listed in the AS path attribute for eBGP. On NX-OS, this property is only supported + in the global BGP context. + required: false + choices: ['true','false', 'default'] + default: null + event_history_cli: + description: + - Enable/Disable cli event history buffer. + required: false + choices: ['size_small', 'size_medium', 'size_large', 'size_disable', 'default'] + default: null + event_history_detail: + description: + - Enable/Disable detail event history buffer. + required: false + choices: ['size_small', 'size_medium', 'size_large', 'size_disable', 'default'] + default: null + event_history_events: + description: + - Enable/Disable event history buffer. + required: false + choices: ['size_small', 'size_medium', 'size_large', 'size_disable', 'default'] + default: null + event_history_periodic: + description: + - Enable/Disable periodic event history buffer. + required: false + choices: ['size_small', 'size_medium', 'size_large', 'size_disable', 'default'] + fast_external_fallover: + description: + - Enable/Disable immediately reset the session if the link to a + directly connected BGP peer goes down. Only supported in the global BGP context. + required: false + choices: ['true','false', 'default'] + default: null + flush_routes: + description: + - Enable/Disable flush routes in RIB upon controlled restart. + On NX-OS, this property is only supported in the global BGP context. + required: false + choices: ['true','false', 'default'] + default: null + graceful_restart: + description: + - Enable/Disable graceful restart. + required: false + choices: ['true','false', 'default'] + default: null + graceful_restart_helper: + description: + - Enable/Disable graceful restart helper mode. + required: false + choices: ['true','false', 'default'] + default: null + graceful_restart_timers_restart: + description: + - Set maximum time for a restart sent to the BGP peer. + required: false + choices: ['true','false', 'default'] + default: null + graceful_restart_timers_stalepath_time: + description: + - Set maximum time that BGP keeps the stale routes from the restarting BGP peer. + choices: ['true','false', 'default'] + default: null + isolate: + description: + - Enable/Disable isolate this router from BGP perspective. + required: false + choices: ['true','false', 'default'] + default: null + local_as: + description: + - Local AS number to be used within a VRF instance. + required: false + choices: string + default: null + log_neighbor_changes: + description: + - Enable/Disable message logging for neighbor up/down event. + required: false + choices: ['true','false', 'default'] + default: null + maxas_limit: + description: + - Specify Maximum number of AS numbers allowed in the AS-path attribute + Valid values are between 1 and 512. + required: false + default: null + neighbor_down_fib_accelerate: + description: + - Enable/Disable handle BGP neighbor down event, due to various reasons. + required: false + choices: ['true','false', 'default'] + default: null + reconnect_interval: + description: + - The BGP reconnection interval for dropped sessions. 1 - 60. + required: false + default: null + router_id: + description: + - Router Identifier (ID) of the BGP router VRF instance. + required: false + default: null + shutdown: + description: + - Administratively shutdown the BGP protocol. + required: false + choices: ['true','false', 'default'] + default: null + suppress_fib_pending: + description: + - Enable/Disable advertise only routes programmed in hardware to peers. + required: false + choices: ['true','false', 'default'] + default: null + timer_bestpath_limit: + description: + - Specify timeout for the first best path after a restart, in seconds. + required: false + default: null + timer_bestpath_limit_always: + description: + - Enable/Disable update-delay-always option. + required: false + choices: ['true','false', 'default'] + default: null + timer_bgp_hold: + description: + - Set bgp hold timer + required: false + default: null + timer_bgp_keepalive: + description: + - Set bgp keepalive timer. + required: false + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' + + +EXAMPLES = ''' +# configure a simple asn +- nxos_bgp: + asn=65535 + vrf=default + state=present + transport=cli +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import itertools + +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + string = item.text if ignore_whitespace is True else item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + +def get_config(module): + config = module.params['running_config'] + if not config: + config = module.get_config() + 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_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +import re + +WARNINGS = [] +BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 'True', 1, True] +BOOLEANS_FALSE = ['no', 'off', '0', 'false', 'False', 0, False] +ACCEPTED = BOOLEANS_TRUE + BOOLEANS_FALSE + ['default'] +BOOL_PARAMS = [ + 'bestpath_always_compare_med', + 'bestpath_aspath_multipath_relax', + 'bestpath_compare_neighborid', + 'bestpath_compare_routerid', + 'bestpath_cost_community_ignore', + 'bestpath_med_confed', + 'bestpath_med_missing_as_worst', + 'bestpath_med_non_deterministic', + 'disable_policy_batching', + 'enforce_first_as', + 'fast_external_fallover', + 'flush_routes', + 'graceful_restart', + 'graceful_restart_helper', + 'isolate', + 'log_neighbor_changes', + 'neighbor_down_fib_accelerate', + 'shutdown', + 'suppress_fib_pending' +] +GLOBAL_PARAMS = [ + 'disable_policy_batching', + 'disable_policy_batching_ipv4_prefix_list', + 'disable_policy_batching_ipv6_prefix_list', + 'enforce_first_as', + 'event_history_cli', + 'event_history_detail', + 'event_history_events', + 'event_history_periodic', + 'fast_external_fallover', + 'flush_routes', + 'isolate', + 'shutdown' +] +PARAM_TO_DEFAULT_KEYMAP = { + 'timer_bgp_keepalive': '60', + 'timer_bgp_hold': '180', + 'graceful_restart': True, + 'graceful_restart_timers_restart': '120', + 'graceful_restart_timers_stalepath_time': '300', + 'reconnect_interval': '60', + 'suppress_fib_pending': True, + 'fast_external_fallover': True, + 'enforce_first_as': True, + 'event_history_periodic': True, + 'event_history_cli': True, + 'event_history_events': True +} +PARAM_TO_COMMAND_KEYMAP = { + 'asn': 'router bgp', + 'bestpath_always_compare_med': 'bestpath always-compare-med', + 'bestpath_aspath_multipath_relax': 'bestpath as-path multipath-relax', + 'bestpath_compare_neighborid': 'bestpath compare-neighborid', + 'bestpath_compare_routerid': 'bestpath compare-routerid', + 'bestpath_cost_community_ignore': 'bestpath cost-community ignore', + 'bestpath_med_confed': 'bestpath med confed', + 'bestpath_med_missing_as_worst': 'bestpath med missing-as-worst', + 'bestpath_med_non_deterministic': 'bestpath med non-deterministic', + 'cluster_id': 'cluster-id', + 'confederation_id': 'confederation identifier', + 'confederation_peers': 'confederation peers', + 'disable_policy_batching': 'disable-policy-batching', + 'disable_policy_batching_ipv4_prefix_list': 'disable-policy-batching ipv4 prefix-list', + 'disable_policy_batching_ipv6_prefix_list': 'disable-policy-batching ipv6 prefix-list', + 'enforce_first_as': 'enforce-first-as', + 'event_history_cli': 'event-history cli', + 'event_history_detail': 'event-history detail', + 'event_history_events': 'event-history events', + 'event_history_periodic': 'event-history periodic', + 'fast_external_fallover': 'fast-external-fallover', + 'flush_routes': 'flush-routes', + 'graceful_restart': 'graceful-restart', + 'graceful_restart_helper': 'graceful-restart-helper', + 'graceful_restart_timers_restart': 'graceful-restart restart-time', + 'graceful_restart_timers_stalepath_time': 'graceful-restart stalepath-time', + 'isolate': 'isolate', + 'local_as': 'local-as', + 'log_neighbor_changes': 'log-neighbor-changes', + 'maxas_limit': 'maxas-limit', + 'neighbor_down_fib_accelerate': 'neighbor-down fib-accelerate', + 'reconnect_interval': 'reconnect-interval', + 'router_id': 'router-id', + 'shutdown': 'shutdown', + 'suppress_fib_pending': 'suppress-fib-pending', + 'timer_bestpath_limit': 'timers bestpath-limit', + 'timer_bgp_hold': 'timers bgp', + 'timer_bgp_keepalive': 'timers bgp', + 'vrf': 'vrf' +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_custom_value(config, arg): + if arg.startswith('event_history'): + REGEX_SIZE = re.compile(r'(?:{0} size\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + + if 'no {0}'.format(PARAM_TO_COMMAND_KEYMAP[arg]) in config: + pass + elif PARAM_TO_COMMAND_KEYMAP[arg] in config: + try: + value = REGEX_SIZE.search(config).group('value') + except AttributeError: + if REGEX.search(config): + value = True + + elif arg == 'confederation_peers': + REGEX = re.compile(r'(?:confederation peers\s)(?P.*)$', re.M) + value = '' + if 'confederation peers' in config: + value = REGEX.search(config).group('value').split() + + elif arg == 'timer_bgp_keepalive': + REGEX = re.compile(r'(?:timers bgp\s)(?P.*)$', re.M) + value = '' + if 'timers bgp' in config: + parsed = REGEX.search(config).group('value').split() + value = parsed[0] + + elif arg == 'timer_bgp_hold': + REGEX = re.compile(r'(?:timers bgp\s)(?P.*)$', re.M) + value = '' + if 'timers bgp' in config: + parsed = REGEX.search(config).group('value').split() + if len(parsed) == 2: + value = parsed[1] + + return value + + +def get_value(arg, config): + custom = [ + 'event_history_cli', + 'event_history_events', + 'event_history_periodic', + 'event_history_detail', + 'confederation_peers', + 'timer_bgp_hold', + 'timer_bgp_keepalive' + ] + + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + elif arg in custom: + value = get_custom_value(config, arg) + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_existing(module, args): + existing = {} + netcfg = get_config(module) + + try: + asn_regex = '.*router\sbgp\s(?P\d+).*' + match_asn = re.match(asn_regex, str(netcfg), re.DOTALL) + existing_asn_group = match_asn.groupdict() + existing_asn = existing_asn_group['existing_asn'] + except AttributeError: + existing_asn = '' + + if existing_asn: + bgp_parent = 'router bgp {0}'.format(existing_asn) + if module.params['vrf'] != 'default': + parents = [bgp_parent, 'vrf {0}'.format(module.params['vrf'])] + else: + parents = bgp_parent + + config = netcfg.get_section(parents) + if config: + for arg in args: + if arg != 'asn': + if module.params['vrf'] != 'default': + if arg not in GLOBAL_PARAMS: + existing[arg] = get_value(arg, config) + else: + existing[arg] = get_value(arg, config) + + existing['asn'] = existing_asn + if module.params['vrf'] == 'default': + existing['vrf'] = 'default' + else: + if (module.params['state'] == 'present' and + module.params['vrf'] != 'default'): + msg = ("VRF {0} doesn't exist. ".format(module.params['vrf'])) + WARNINGS.append(msg) + else: + if (module.params['state'] == 'present' and + module.params['vrf'] != 'default'): + msg = ("VRF {0} doesn't exist. ".format(module.params['vrf'])) + WARNINGS.append(msg) + + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + elif value is False: + commands.append('no {0}'.format(key)) + elif value == 'default': + if key in PARAM_TO_DEFAULT_KEYMAP.keys(): + commands.append('{0} {1}'.format(key, PARAM_TO_DEFAULT_KEYMAP[key])) + elif existing_commands.get(key): + existing_value = existing_commands.get(key) + if key == 'confederation peers': + commands.append('no {0} {1}'.format(key, ' '.join(existing_value))) + else: + commands.append('no {0} {1}'.format(key, existing_value)) + else: + if key.replace(' ', '_').replace('-', '_') in BOOL_PARAMS: + commands.append('no {0}'.format(key)) + else: + if key == 'confederation peers': + existing_confederation_peers = existing.get('confederation_peers') + + if existing_confederation_peers: + if not isinstance(existing_confederation_peers, list): + existing_confederation_peers = [existing_confederation_peers] + else: + existing_confederation_peers = [] + + values = value.split() + for each_value in values: + if each_value not in existing_confederation_peers: + existing_confederation_peers.append(each_value) + peer_string = ' '.join(existing_confederation_peers) + commands.append('{0} {1}'.format(key, peer_string)) + elif key.startswith('timers bgp'): + command = 'timers bgp {0} {1}'.format( + proposed['timer_bgp_keepalive'], + proposed['timer_bgp_hold']) + if command not in commands: + commands.append(command) + else: + if value.startswith('size'): + value = value.replace('_', ' ') + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + commands = fix_commands(commands) + parents = ['router bgp {0}'.format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + candidate.add(commands, parents=parents) + else: + if len(proposed.keys()) == 0: + if module.params['vrf'] != 'default': + commands.append('vrf {0}'.format(module.params['vrf'])) + parents = ['router bgp {0}'.format(module.params['asn'])] + else: + commands.append('router bgp {0}'.format(module.params['asn'])) + parents = [] + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = [] + if module.params['vrf'] == 'default': + commands.append('no router bgp {0}'.format(module.params['asn'])) + else: + if existing.get('vrf') == module.params['vrf']: + commands.append('no vrf {0}'.format(module.params['vrf'])) + parents = ['router bgp {0}'.format(module.params['asn'])] + + candidate.add(commands, parents=parents) + + +def fix_commands(commands): + local_as_command = '' + confederation_id_command = '' + confederation_peers_command = '' + + for command in commands: + if 'local-as' in command: + local_as_command = command + elif 'confederation identifier' in command: + confederation_id_command = command + elif 'confederation peers' in command: + confederation_peers_command = command + + if local_as_command and confederation_id_command: + commands.pop(commands.index(local_as_command)) + commands.pop(commands.index(confederation_id_command)) + commands.append(local_as_command) + commands.append(confederation_id_command) + + elif confederation_peers_command and confederation_id_command: + commands.pop(commands.index(confederation_peers_command)) + commands.pop(commands.index(confederation_id_command)) + commands.append(confederation_id_command) + commands.append(confederation_peers_command) + + return commands + + +def main(): + argument_spec = dict( + asn=dict(required=True, type='str'), + vrf=dict(required=False, type='str', default='default'), + bestpath_always_compare_med=dict(required=False, choices=ACCEPTED), + bestpath_aspath_multipath_relax=dict(required=False, choices=ACCEPTED), + bestpath_compare_neighborid=dict(required=False, choices=ACCEPTED), + bestpath_compare_routerid=dict(required=False, choices=ACCEPTED), + bestpath_cost_community_ignore=dict(required=False, choices=ACCEPTED), + bestpath_med_confed=dict(required=False, choices=ACCEPTED), + bestpath_med_missing_as_worst=dict(required=False, choices=ACCEPTED), + bestpath_med_non_deterministic=dict(required=False, choices=ACCEPTED), + cluster_id=dict(required=False, type='str'), + confederation_id=dict(required=False, type='str'), + confederation_peers=dict(required=False, type='str'), + disable_policy_batching=dict(required=False, choices=ACCEPTED), + disable_policy_batching_ipv4_prefix_list=dict(required=False, type='str'), + disable_policy_batching_ipv6_prefix_list=dict(required=False, type='str'), + enforce_first_as=dict(required=False, choices=ACCEPTED), + event_history_cli=dict(required=False, choices=['true', 'false', 'default', 'size_small', 'size_medium', 'size_large', 'size_disable']), + event_history_detail=dict(required=False, choices=['true', 'false', 'default', 'size_small', 'size_medium', 'size_large', 'size_disable']), + event_history_events=dict(required=False, choices=['true', 'false', 'default' 'size_small', 'size_medium', 'size_large', 'size_disable']), + event_history_periodic=dict(required=False, choices=['true', 'false', 'default', 'size_small', 'size_medium', 'size_large', 'size_disable']), + fast_external_fallover=dict(required=False, choices=ACCEPTED), + flush_routes=dict(required=False, choices=ACCEPTED), + graceful_restart=dict(required=False, choices=ACCEPTED), + graceful_restart_helper=dict(required=False, choices=ACCEPTED), + graceful_restart_timers_restart=dict(required=False, type='str'), + graceful_restart_timers_stalepath_time=dict(required=False, type='str'), + isolate=dict(required=False, choices=ACCEPTED), + local_as=dict(required=False, type='str'), + log_neighbor_changes=dict(required=False, choices=ACCEPTED), + maxas_limit=dict(required=False, type='str'), + neighbor_down_fib_accelerate=dict(required=False, choices=ACCEPTED), + reconnect_interval=dict(required=False, type='str'), + router_id=dict(required=False, type='str'), + shutdown=dict(required=False, choices=ACCEPTED), + suppress_fib_pending=dict(required=False, choices=ACCEPTED), + timer_bestpath_limit=dict(required=False, type='str'), + timer_bgp_hold=dict(required=False, type='str'), + timer_bgp_keepalive=dict(required=False, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + required_together=[['timer_bgp_hold', + 'timer_bgp_keepalive']], + supports_check_mode=True) + + state = module.params['state'] + args = [ + "asn", + "bestpath_always_compare_med", + "bestpath_aspath_multipath_relax", + "bestpath_compare_neighborid", + "bestpath_compare_routerid", + "bestpath_cost_community_ignore", + "bestpath_med_confed", + "bestpath_med_missing_as_worst", + "bestpath_med_non_deterministic", + "cluster_id", + "confederation_id", + "confederation_peers", + "disable_policy_batching", + "disable_policy_batching_ipv4_prefix_list", + "disable_policy_batching_ipv6_prefix_list", + "enforce_first_as", + "event_history_cli", + "event_history_detail", + "event_history_events", + "event_history_periodic", + "fast_external_fallover", + "flush_routes", + "graceful_restart", + "graceful_restart_helper", + "graceful_restart_timers_restart", + "graceful_restart_timers_stalepath_time", + "isolate", + "local_as", + "log_neighbor_changes", + "maxas_limit", + "neighbor_down_fib_accelerate", + "reconnect_interval", + "router_id", + "shutdown", + "suppress_fib_pending", + "timer_bestpath_limit", + "timer_bgp_hold", + "timer_bgp_keepalive", + "vrf" + ] + + if module.params['vrf'] != 'default': + for param, inserted_value in module.params.iteritems(): + if param in GLOBAL_PARAMS and inserted_value: + module.fail_json(msg='Global params can be modifed only' + ' under "default" VRF.', + vrf=module.params['vrf'], + global_param=param) + + existing = invoke('get_existing', module, args) + + if existing.get('asn'): + if (existing.get('asn') != module.params['asn'] and + state == 'present'): + module.fail_json(msg='Another BGP ASN already exists.', + proposed_asn=module.params['asn'], + existing_asn=existing.get('asn')) + + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'asn' and key != 'vrf': + if value.lower() == 'true': + value = True + elif value.lower() == 'false': + value = False + elif value.lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + if key in BOOL_PARAMS: + value = False + else: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if (state == 'present' or (state == 'absent' and + existing.get('asn') == module.params['asn'])): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.nxos import * +if __name__ == '__main__': + main() From 228269f093b28eabef5730c8ed6da0861c110346 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 30 Aug 2016 14:02:54 +0200 Subject: [PATCH 005/770] Fixing DOC --- network/nxos/nxos_bgp.py | 83 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 76 insertions(+), 7 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 132174ee6e9..bcc0f6f1dd6 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -23,14 +23,16 @@ short_description: Manages BGP configuration description: - Manages BGP configurations on NX-OS switches -author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - State 'absent' removes the whole BGP ASN configuration when VRF is - 'default' or the whole VRF instance within the BGP process when using + - I(state)=absent removes the whole BGP ASN configuration when VRF is + C(default) or the whole VRF instance within the BGP process when using a different VRF. - - 'default', when supported, restores params default value. - - Configuring global parmas is only permitted if VRF is 'default'. + - C(default) when supported restores params default value. + - Configuring global parmas is only permitted if VRF is C(default). options: asn: description: @@ -280,9 +282,76 @@ # configure a simple asn - nxos_bgp: asn=65535 - vrf=default + vrf=test + router_id=1.1.1.1 state=present - transport=cli + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"} +existing: + description: k/v pairs of existing BGP configuration + type: dict + sample: {"asn": "65535", "bestpath_always_compare_med": false, + "bestpath_aspath_multipath_relax": false, + "bestpath_compare_neighborid": false, + "bestpath_compare_routerid": false, + "bestpath_cost_community_ignore": false, + "bestpath_med_confed": false, + "bestpath_med_missing_as_worst": false, + "bestpath_med_non_deterministic": false, "cluster_id": "", + "confederation_id": "", "confederation_peers": "", + "graceful_restart": true, "graceful_restart_helper": false, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", "local_as": "", + "log_neighbor_changes": false, "maxas_limit": "", + "neighbor_down_fib_accelerate": false, "reconnect_interval": "60", + "router_id": "11.11.11.11", "suppress_fib_pending": false, + "timer_bestpath_limit": "", "timer_bgp_hold": "180", + "timer_bgp_keepalive": "60", "vrf": "test"} +end_state: + description: k/v pairs of BGP configuration after module execution + returned: always + type: dict + sample: {"asn": "65535", "bestpath_always_compare_med": false, + "bestpath_aspath_multipath_relax": false, + "bestpath_compare_neighborid": false, + "bestpath_compare_routerid": false, + "bestpath_cost_community_ignore": false, + "bestpath_med_confed": false, + "bestpath_med_missing_as_worst": false, + "bestpath_med_non_deterministic": false, "cluster_id": "", + "confederation_id": "", "confederation_peers": "", + "graceful_restart": true, "graceful_restart_helper": false, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", "local_as": "", + "log_neighbor_changes": false, "maxas_limit": "", + "neighbor_down_fib_accelerate": false, "reconnect_interval": "60", + "router_id": "1.1.1.1", "suppress_fib_pending": false, + "timer_bestpath_limit": "", "timer_bgp_hold": "180", + "timer_bgp_keepalive": "60", "vrf": "test"} +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "present" +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true ''' # COMMON CODE FOR MIGRATION From 17dfe863ec2c948f18c038e5bdce6f5437308972 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 30 Aug 2016 14:08:53 +0200 Subject: [PATCH 006/770] Fixing DOC --- network/nxos/nxos_bgp.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index bcc0f6f1dd6..5cdf85d8e54 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -200,7 +200,6 @@ description: - Local AS number to be used within a VRF instance. required: false - choices: string default: null log_neighbor_changes: description: From 1083a9e7eaf9e3fff8f46c47720b484c1620f8df Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 30 Aug 2016 14:22:26 +0200 Subject: [PATCH 007/770] Fixing conditional format --- network/nxos/nxos_bgp.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 5cdf85d8e54..682cf9fcb3e 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -654,7 +654,10 @@ def replace(self, replace, text=None, regex=None, parents=None, for item in self.items: for regexp in patterns: - string = item.text if ignore_whitespace is True else item.raw + if ignore_whitespace is True: + string = item.text + else: + string = item.raw if regexp.search(item.text): if item.text != replace: if parents == [p.text for p in item.parents]: From 02c47f5b0cd2e6051be12b42f48d4bd9fd2c6a54 Mon Sep 17 00:00:00 2001 From: Rick Mendes Date: Tue, 30 Aug 2016 06:16:42 -0700 Subject: [PATCH 008/770] Fixes #3144 (#4305) --- cloud/amazon/ec2_eip.py | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_eip.py b/cloud/amazon/ec2_eip.py index be5691d07d1..eb3f17a9569 100644 --- a/cloud/amazon/ec2_eip.py +++ b/cloud/amazon/ec2_eip.py @@ -17,9 +17,10 @@ DOCUMENTATION = ''' --- module: ec2_eip -short_description: associate an EC2 elastic IP with an instance. +short_description: manages EC2 elastic IP (EIP) addresses. description: - - This module associates AWS EC2 elastic IP addresses with instances + - This module can allocate or release an EIP. + - This module can associate/disassociate an EIP with instances or network interfaces. version_added: "1.4" options: device_id: @@ -30,13 +31,15 @@ version_added: "2.0" public_ip: description: - - The elastic IP address to associate with the instance. - - If absent, allocate a new address + - The IP address of a previously allocated EIP. + - If present and device is specified, the EIP is associated with the device. + - If absent and device is specified, the EIP is disassociated from the device. required: false + aliases: [ ip ] state: description: - - If present, associate the IP with the instance. - - If absent, disassociate the IP with the instance. + - If present, allocate an EIP or associate an existing EIP with a device. + - If absent, disassociate the EIP from the device and optionally release it. required: false choices: ['present', 'absent'] default: present @@ -48,7 +51,7 @@ version_added: "1.4" reuse_existing_ip_allowed: description: - - Reuse an EIP that is not associated to an instance (when available), instead of allocating a new one. + - Reuse an EIP that is not associated to a device (when available), instead of allocating a new one. required: false default: false version_added: "1.6" @@ -64,8 +67,8 @@ author: "Rick Mendes (@rickmendes) " notes: - This module will return C(public_ip) on success, which will contain the - public IP address associated with the instance. - - There may be a delay between the time the Elastic IP is assigned and when + public IP address associated with the device. + - There may be a delay between the time the EIP is assigned and when the cloud instance is reachable via the new address. Use wait_for and pause to delay further playbook execution until the instance is reachable, if necessary. @@ -93,7 +96,7 @@ - name: another way of allocating an elastic IP without associating it to anything ec2_eip: state='present' - name: provision new instances with ec2 - ec2: keypair=mykey instance_type=c1.medium image=emi-40603AD1 wait=yes''' + ec2: keypair=mykey instance_type=c1.medium image=ami-40603AD1 wait=yes''' ''' group=webserver count=3 register: ec2 - name: associate new elastic IPs with each of the instances From 978716cf4c177f09f1e07d29d103fed27575846d Mon Sep 17 00:00:00 2001 From: "James S. Martin" Date: Tue, 30 Aug 2016 10:10:01 -0400 Subject: [PATCH 009/770] Shows messages for uncaught exceptions from called modules in async_wrapper output. (#4591) --- utilities/logic/async_wrapper.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 3a9b44d4cd0..05452b4e2cc 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -105,6 +105,7 @@ def _run_module(wrapped_cmd, jid, job_path): "failed" : 1, "cmd" : wrapped_cmd, "data" : outdata, # temporary notice only + "stderr": stderr, "msg" : traceback.format_exc() } result['ansible_job_id'] = jid From 0c37949941146e9b2a435009011ab5165f8da23d Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Tue, 30 Aug 2016 10:24:00 -0400 Subject: [PATCH 010/770] Remove spurious `changed` state on iam_policy module (#4381) Due to a mixup of the group/role/user and policy names, policies with the same name as the group/role/user they are attached to would never be updated after creation. To fix that, we needed two changes to the logic of policy comparison: - Compare the new policy name to *all* matching policies, not just the first in lexicographical order - Compare the new policy name to the matching ones, not to the IAM object the policy is attached to --- cloud/amazon/iam_policy.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/cloud/amazon/iam_policy.py b/cloud/amazon/iam_policy.py index ce0c06a8d51..e1cc6b305c9 100644 --- a/cloud/amazon/iam_policy.py +++ b/cloud/amazon/iam_policy.py @@ -139,7 +139,7 @@ def user_action(module, iam, name, policy_name, skip, pdoc, state): current_policies = [cp for cp in iam.get_all_user_policies(name). list_user_policies_result. policy_names] - pol = "" + matching_policies = [] for pol in current_policies: ''' urllib is needed here because boto returns url encoded strings instead @@ -147,13 +147,13 @@ def user_action(module, iam, name, policy_name, skip, pdoc, state): if urllib.unquote(iam.get_user_policy(name, pol). get_user_policy_result.policy_document) == pdoc: policy_match = True - break + matching_policies.append(pol) if state == 'present': # If policy document does not already exist (either it's changed # or the policy is not present) or if we're not skipping dupes then # make the put call. Note that the put call does a create or update. - if (not policy_match or not skip) and pol != name: + if not policy_match or (not skip and policy_name not in matching_policies): changed = True iam.put_user_policy(name, policy_name, pdoc) elif state == 'absent': @@ -191,18 +191,18 @@ def role_action(module, iam, name, policy_name, skip, pdoc, state): module.fail_json(msg=e.message) try: - pol = "" + matching_policies = [] for pol in current_policies: if urllib.unquote(iam.get_role_policy(name, pol). get_role_policy_result.policy_document) == pdoc: policy_match = True - break + matching_policies.append(pol) if state == 'present': # If policy document does not already exist (either it's changed # or the policy is not present) or if we're not skipping dupes then # make the put call. Note that the put call does a create or update. - if (not policy_match or not skip) and pol != name: + if not policy_match or (not skip and policy_name not in matching_policies): changed = True iam.put_role_policy(name, policy_name, pdoc) elif state == 'absent': @@ -236,20 +236,19 @@ def group_action(module, iam, name, policy_name, skip, pdoc, state): current_policies = [cp for cp in iam.get_all_group_policies(name). list_group_policies_result. policy_names] - pol = "" + matching_policies = [] for pol in current_policies: if urllib.unquote(iam.get_group_policy(name, pol). get_group_policy_result.policy_document) == pdoc: policy_match = True - if policy_match: - msg=("The policy document you specified already exists " - "under the name %s." % pol) - break + matching_policies.append(pol) + msg=("The policy document you specified already exists " + "under the name %s." % pol) if state == 'present': # If policy document does not already exist (either it's changed # or the policy is not present) or if we're not skipping dupes then # make the put call. Note that the put call does a create or update. - if (not policy_match or not skip) and pol != name: + if not policy_match or (not skip and policy_name not in matching_policies): changed = True iam.put_group_policy(name, policy_name, pdoc) elif state == 'absent': From 8f963a7028360d6a45f609fe201c2b60aa4c2f62 Mon Sep 17 00:00:00 2001 From: David J Peacock Date: Tue, 30 Aug 2016 10:55:16 -0400 Subject: [PATCH 011/770] Fix #4412: os_security_group_rule support numbered protocols (#4444) --- cloud/openstack/os_security_group_rule.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_security_group_rule.py b/cloud/openstack/os_security_group_rule.py index af4106ac44c..0bbb2427784 100644 --- a/cloud/openstack/os_security_group_rule.py +++ b/cloud/openstack/os_security_group_rule.py @@ -39,7 +39,7 @@ protocol: description: - IP protocol - choices: ['tcp', 'udp', 'icmp', None] + choices: ['tcp', 'udp', 'icmp', 112, None] default: None port_range_min: description: @@ -121,6 +121,12 @@ security_group: foo protocol: tcp remote_ip_prefix: 0.0.0.0/0 + +# Create a rule for VRRP with numbered protocol 112 +- os_security_group_rule: + security_group: loadbalancer_sg + protocol: 112 + remote_group: loadbalancer-node_sg ''' RETURN = ''' @@ -251,7 +257,7 @@ def main(): # NOTE(Shrews): None is an acceptable protocol value for # Neutron, but Nova will balk at this. protocol = dict(default=None, - choices=[None, 'tcp', 'udp', 'icmp']), + choices=[None, 'tcp', 'udp', 'icmp', 112]), port_range_min = dict(required=False, type='int'), port_range_max = dict(required=False, type='int'), remote_ip_prefix = dict(required=False, default=None), From 0d98760b4988a27dd76a757b72c25b1e9f339c87 Mon Sep 17 00:00:00 2001 From: Arun prasath Date: Tue, 30 Aug 2016 20:46:43 +0530 Subject: [PATCH 012/770] Fixes bug #4546 - Made password optional (#4574) --- cloud/openstack/os_user.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/cloud/openstack/os_user.py b/cloud/openstack/os_user.py index 1e148e3d946..19c984992ac 100644 --- a/cloud/openstack/os_user.py +++ b/cloud/openstack/os_user.py @@ -41,7 +41,6 @@ password: description: - Password for the user - - Required when I(state) is present required: false default: None email: @@ -149,9 +148,6 @@ def main(): module_kwargs = openstack_module_kwargs() module = AnsibleModule( argument_spec, - required_if=[ - ('state', 'present', ['password']) - ], **module_kwargs) if not HAS_SHADE: From c170107eef9be3b6e36fa5169ce1e4cc6f441651 Mon Sep 17 00:00:00 2001 From: Victor Volle Date: Tue, 30 Aug 2016 20:54:46 +0200 Subject: [PATCH 013/770] Digitalocean tags (replaces #4209) (#4218) * Fixes #4117: Add DigitalOcean Tag support * Add GPLv3 license header and RETURN documentation * ansible.module_utils.urls instead of "requests" --- cloud/digital_ocean/digital_ocean_tag.py | 256 +++++++++++++++++++++++ 1 file changed, 256 insertions(+) create mode 100644 cloud/digital_ocean/digital_ocean_tag.py diff --git a/cloud/digital_ocean/digital_ocean_tag.py b/cloud/digital_ocean/digital_ocean_tag.py new file mode 100644 index 00000000000..e0085e0b056 --- /dev/null +++ b/cloud/digital_ocean/digital_ocean_tag.py @@ -0,0 +1,256 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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: digital_ocean_tag +short_description: Create and remove tag(s) to DigitalOcean resource. +description: + - Create and remove tag(s) to DigitalOcean resource. +version_added: "2.2" +options: + name: + description: + - The name of the tag. The supported characters for names include + alphanumeric characters, dashes, and underscores. + resource_id: + description: + - The ID of the resource to operate on. + resource_type: + description: + - The type of resource to operate on. Currently only tagging of + droplets is supported. + default: droplet + choices: ['droplet'] + state: + description: + - Whether the tag should be present or absent on the resource. + default: present + choices: ['present', 'absent'] + api_token: + description: + - DigitalOcean api token. + +notes: + - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. + They both refer to the v2 token. + - As of Ansible 2.0, Version 2 of the DigitalOcean API is used. + +requirements: + - "python >= 2.6" +''' + + +EXAMPLES = ''' +- name: create a tag + digital_ocean_tag: + name: production + state: present + +- name: tag a resource; creating the tag if it does not exists + digital_ocean_tag: + name: "{{ item }}" + resource_id: YYY + state: present + with_items: + - staging + - dbserver + +- name: untag a resource + digital_ocean_tag: + name: staging + resource_id: YYY + state: absent + +# Deleting a tag also untags all the resources that have previously been +# tagged with it +- name: remove a tag + digital_ocean_tag: + name: dbserver + state: absent +''' + + +RETURN = ''' +data: + description: a DigitalOcean Tag resource + returned: success and no resource constraint + type: dict + sample: { + "tag": { + "name": "awesome", + "resources": { + "droplets": { + "count": 0, + "last_tagged": null + } + } + } + } +''' + +import json + + +class Response(object): + + def __init__(self, resp, info): + self.body = None + if resp: + self.body = resp.read() + self.info = info + + @property + def json(self): + if not self.body: + if "body" in self.info: + return json.loads(self.info["body"]) + return None + try: + return json.loads(self.body) + except ValueError as e: + return None + + @property + def status_code(self): + return self.info["status"] + + +class Rest(object): + + def __init__(self, module, headers): + self.module = module + self.headers = headers + self.baseurl = 'https://api.digitalocean.com/v2' + + def _url_builder(self, path): + if path[0] == '/': + path = path[1:] + return '%s/%s' % (self.baseurl, path) + + def send(self, method, path, data=None, headers=None): + url = self._url_builder(path) + data = self.module.jsonify(data) + + resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method) + + return Response(resp, info) + + def get(self, path, data=None, headers=None): + return self.send('GET', path, data, headers) + + def put(self, path, data=None, headers=None): + return self.send('PUT', path, data, headers) + + def post(self, path, data=None, headers=None): + return self.send('POST', path, data, headers) + + def delete(self, path, data=None, headers=None): + return self.send('DELETE', path, data, headers) + + +def core(module): + try: + api_token = module.params['api_token'] or \ + os.environ['DO_API_TOKEN'] or os.environ['DO_API_KEY'] + except KeyError as e: + module.fail_json(msg='Unable to load %s' % e.message) + + state = module.params['state'] + name = module.params['name'] + resource_id = module.params['resource_id'] + resource_type = module.params['resource_type'] + + rest = Rest(module, {'Authorization': 'Bearer {}'.format(api_token), + 'Content-type': 'application/json'}) + + if state in ('present'): + if name is None: + module.fail_json(msg='parameter `name` is missing') + + # Ensure Tag exists + response = rest.post("tags", data={'name': name}) + status_code = response.status_code + json = response.json + if status_code == 201: + changed = True + elif status_code == 422: + changed = False + else: + module.exit_json(changed=False, data=json) + + if resource_id is None: + # No resource defined, we're done. + if json is None: + module.exit_json(changed=changed, data=json) + else: + module.exit_json(changed=changed, data=json) + else: + # Tag a resource + url = "tags/{}/resources".format(name) + payload = { + 'resources': [{ + 'resource_id': resource_id, + 'resource_type': resource_type}]} + response = rest.post(url, data=payload) + if response.status_code == 204: + module.exit_json(changed=True) + else: + module.fail_json(msg="error tagging resource '{}': {}".format( + resource_id, response.json["message"])) + + elif state in ('absent'): + if name is None: + module.fail_json(msg='parameter `name` is missing') + + if resource_id: + url = "tags/{}/resources".format(name) + payload = { + 'resources': [{ + 'resource_id': resource_id, + 'resource_type': resource_type}]} + response = rest.delete(url, data=payload) + else: + url = "tags/{}".format(name) + response = rest.delete(url) + if response.status_code == 204: + module.exit_json(changed=True) + else: + module.exit_json(changed=False, data=response.json) + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(type='str', required=True), + resource_id=dict(aliases=['droplet_id'], type='int'), + resource_type=dict(choices=['droplet'], default='droplet'), + state=dict(choices=['present', 'absent'], default='present'), + api_token=dict(aliases=['API_TOKEN'], no_log=True), + ) + ) + + try: + core(module) + except Exception as e: + module.fail_json(msg=str(e)) + +# import module snippets +from ansible.module_utils.basic import * # noqa +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() From 0c25e968ee5153f049d3ba9a95f213a6a67cd476 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Tue, 30 Aug 2016 19:56:49 +0100 Subject: [PATCH 014/770] digital_ocean_tag name is a required field Spotted during final review of https://github.com/ansible/ansible-modules-core/pull/4218/files Simple to change, so fixing post merge --- cloud/digital_ocean/digital_ocean_tag.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/digital_ocean/digital_ocean_tag.py b/cloud/digital_ocean/digital_ocean_tag.py index e0085e0b056..2e06a1d5a0a 100644 --- a/cloud/digital_ocean/digital_ocean_tag.py +++ b/cloud/digital_ocean/digital_ocean_tag.py @@ -27,6 +27,7 @@ description: - The name of the tag. The supported characters for names include alphanumeric characters, dashes, and underscores. + required: true resource_id: description: - The ID of the resource to operate on. From 2538c70d468eb9972023a395d77269fb2ed2b59a Mon Sep 17 00:00:00 2001 From: Timothy Appnel Date: Tue, 30 Aug 2016 12:04:03 -0700 Subject: [PATCH 015/770] Fixes get_url examples in docs and applies native YAML syntax. (#4474) --- network/basics/get_url.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/network/basics/get_url.py b/network/basics/get_url.py index 6a85b161141..f2cc7c615c1 100644 --- a/network/basics/get_url.py +++ b/network/basics/get_url.py @@ -169,20 +169,39 @@ EXAMPLES=''' - name: download foo.conf - get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf mode=0440 + get_url: + url: http://example.com/path/file.conf + dest: /etc/foo.conf + mode: 0440 - name: download file and force basic auth - get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf force_basic_auth=yes + get_url: + url: http://example.com/path/file.conf + dest: /etc/foo.conf + force_basic_auth: yes - name: download file with custom HTTP headers - get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf headers='key:value,key:value' - -- name: download file with check - get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf checksum=sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c - get_url: url=http://example.com/path/file.conf dest=/etc/foo.conf checksum=md5:66dffb5228a211e61d6d7ef4a86f5758 + get_url: + url: http://example.com/path/file.conf + dest: /etc/foo.conf + headers: 'key:value,key:value' + +- name: download file with check (sha256) + get_url: + url: http://example.com/path/file.conf + dest: /etc/foo.conf + checksum: sha256:b5bb9d8014a0f9b1d61e21e796d78dccdf1352f23cd32812f4850b878ae4944c + +- name: download file with check (md5) + get_url: + url: http://example.com/path/file.conf + dest: /etc/foo.conf + checksum: md5:66dffb5228a211e61d6d7ef4a86f5758 - name: download file from a file path - get_url: url="file:///tmp/afile.txt" dest=/tmp/afilecopy.txt + get_url: + url: "file:///tmp/afile.txt" + dest: /tmp/afilecopy.txt ''' from ansible.module_utils.six.moves.urllib.parse import urlsplit From 6543bb4bdd7d4b28cdecd6a2f2e19590e229440c Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 15:05:18 -0400 Subject: [PATCH 016/770] update nxos_nxapi module with minor enhancements (#4573) * added new config argument * added states present and absent * update to use network shared modules Tested on NXOS 7.3(0)D1(1) --- network/nxos/nxos_nxapi.py | 470 ++++++++++++++++++++----------------- 1 file changed, 253 insertions(+), 217 deletions(-) diff --git a/network/nxos/nxos_nxapi.py b/network/nxos/nxos_nxapi.py index 88b0b9150d4..feec8a6d80a 100644 --- a/network/nxos/nxos_nxapi.py +++ b/network/nxos/nxos_nxapi.py @@ -19,221 +19,229 @@ DOCUMENTATION = """ --- -module: nxos_nxapi +module: nxos_nxapi version_added: "2.1" -author: "Chris Houseknecht (@chouseknecht)" +author: "Peter Sprygada (@privateip)" short_description: Manage NXAPI configuration on an NXOS device. description: - - Use to enable or disable NXAPI access, set the port and state - of http and https servers, and enable or disable the sandbox. - - When enabling NXAPI access the default is to enable HTTP on port - 80, enable HTTPS on port 443, and enable the web based UI sandbox. - Use the options below to override the default configuration. + - Configures the NXAPI feature on devices running Cisco NXOS. The + NXAPI feature is absent from the configuration by default. Since + this module manages the NXAPI feature it only supports the use + of the C(Cli) transport. extends_documentation_fragment: nxos options: - state: - description: - - Set to started or stopped. A state of started will - enable NXAPI access, and a state of stopped will - disable or shutdown all NXAPI access. - choices: - - started - - stopped - required: false - default: started - http_port: - description: - - Port on which the HTTP server will listen. - required: false - default: 80 - https_port: - description: - - Port on which the HTTPS server will listen. - required: false - default: 443 - http: - description: - - Enable/disable HTTP server. - required: false - default: true - choices: - - true - - false - aliases: - - enable_http - https: - description: - - Enable/disable HTTPS server. - required: false - choices: - - true - - false - default: true - aliases: - - enable_https - sandbox: - description: - - Enable/disable NXAPI web based UI for entering commands. - required: false - default: true - choices: - - true - - false - aliases: - - enable_sandbox + http_port: + description: + - Configure the port with which the HTTP server will listen on + for requests. By default, NXAPI will bind the HTTP service + to the standard HTTP port 80. This argument accepts valid + port values in the range of 1 to 65535. + required: false + default: 80 + http: + description: + - Controls the operating state of the HTTP protocol as one of the + underlying transports for NXAPI. By default, NXAPI will enable + the HTTP transport when the feature is first configured. To + disable the use of the HTTP transport, set the value of this + argument to False. + required: false + default: yes + choices: ['yes', 'no'] + aliases: ['enable_http'] + https_port: + description: + - Configure the port with which the HTTPS server will listen on + for requests. By default, NXAPI will bind the HTTPS service + to the standard HTTPS port 443. This argument accepts valid + port values in the range of 1 to 65535. + required: false + default: 443 + https: + description: + - Controls the operating state of the HTTPS protocol as one of the + underlying transports for NXAPI. By default, NXAPI will disable + the HTTPS transport when the feature is first configured. To + enable the use of the HTTPS transport, set the value of this + argument to True. + required: false + default: yes + choices: ['yes', 'no'] + aliases: ['enable_https'] + sandbox: + description: + - The NXAPI feature provides a web base UI for developers for + entering commands. This feature is initially disabled when + the NXAPI feature is configured for the first time. When the + C(sandbox) argument is set to True, the developer sandbox URL + will accept requests and when the value is set to False, the + sandbox URL is unavailable. + required: false + default: no + choices: ['yes', 'no'] + aliases: ['enable_sandbox'] + config: + description: + - The C(config) argument provides an optional argument to + specify the device running-config to used as the basis for + configuring the remote system. The C(config) argument accepts + a string value that represents the device configuration. + required: false + default: null + version_added: "2.2" + state: + description: + - The C(state) argument controls whether or not the NXAPI + feature is configured on the remote device. When the value + is C(present) the NXAPI feature configuration is present in + the device running-config. When the values is C(absent) the + feature configuration is removed from the running-config. + choices: ['present', 'absent'] + required: false + default: present """ EXAMPLES = """ - - name: Enable NXAPI access with default configuration - nxos_nxapi: - provider: {{ provider }} - - - name: Enable NXAPI with no HTTP, HTTPS at port 9443 and sandbox disabled - nxos_nxapi: - enable_http: false - https_port: 9443 - enable_sandbox: no - provider: {{ provider }} - - - name: shutdown NXAPI access - nxos_nxapi: - state: stopped - provider: {{ provider }} +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: admin + password: admin + +- name: Enable NXAPI access with default configuration + nxos_nxapi: + provider: {{ cli }} + +- name: Enable NXAPI with no HTTP, HTTPS at port 9443 and sandbox disabled + nxos_nxapi: + enable_http: false + https_port: 9443 + https: yes + enable_sandbox: no + provider: {{ cli }} + +- name: remove NXAPI configuration + nxos_nxapi: + state: absent + provider: {{ cli }} """ RETURN = """ -changed: - description: - - Indicates if commands were sent to the device. - returned: always - type: boolean - sample: false - -commands: - description: - - Set of commands to be executed on remote device. If run in check mode, - commands will not be executed. - returned: always - type: list - sample: [ - 'nxapi feature', - 'nxapi http port 8080' - ] - -_config: - description: - - Configuration found on the device prior ro any commands being executed. - returned: always - type: object - sample: {...} +updates: + description: + - Returns the list of commands that need to be pushed into the remote + device to satisfy the arguments + returned: always + type: list + sample: ['no feature nxapi'] """ - - -def http_commands(protocol, port, enable, config): - port_config = config.get('{0}_port'.format(protocol), None) - changed = False - commands = [] - if port_config is None and enable: - # enable - changed = True - commands.append('nxapi {0} port {1}'.format(protocol, port)) - elif port_config is not None: - if not enable: - # disable - commands.append('no nxapi {0}'.format(protocol)) - changed = True - elif port_config != port: - # update port - commands.append('nxapi {0} port {1}'.format(protocol, port)) - changed = True - return commands, changed - - -def execute_commands(module, commands): - if not module.params.get('check_mode'): - module.configure(commands) - - -def get_nxapi_state(module): - features = module.execute(['show feature | grep nxapi'])[0] - if re.search('disabled', features) is None: - return 'started' - return 'stopped' - - -def config_server(module): - - nxapi_state = get_nxapi_state(module) - - config = dict() - if nxapi_state == 'started': - config = module.from_json(module.execute(['show nxapi | json'])[0]) - - state = module.params.get('state') - result = dict(changed=False, _config=config, commands=[]) - commands = [] - - if config.get('nxapi_status', 'Disabled') == 'Disabled': - if state == 'started': - # enable nxapi and get the new default config - commands.append('feature nxapi') - result['_config'] = dict() - result['changed'] = True - if module.params.get('check_mode'): - # make an assumption about default state - config['http_port'] = 80 - config['sandbox_status'] = 'Disabled' - else: - # get the default config - execute_commands(module, commands) - config = module.from_json(module.execute(['show nxapi | json'])[0]) - else: - # nxapi already disabled - return result - elif config.get('nxapi_status', 'Disabled') == 'Enabled' and state == 'stopped': - # disable nxapi and exit - commands.append('no feature nxapi') +import re +import time + +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.nxos import NetworkModule, NetworkError +from ansible.module_utils.basic import get_exception + +PRIVATE_KEYS_RE = re.compile('__.+__') + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + +def present(module, commands): + commands.append('feature nxapi') + setters = set() + for key, value in module.argument_spec.iteritems(): + setter = value.get('setter') or 'set_%s' % key + if setter not in setters: + setters.add(setter) + if module.params[key] is not None: + invoke(setter, module, commands) + +def absent(module, commands): + commands.append('no feature nxapi') + +def set_http(module, commands): + port = module.params['http_port'] + if not 0 <= port <= 65535: + module.fail_json(msg='http_port must be between 1 and 65535') + elif module.params['http'] is True: + commands.append('nxapi http port %s' % port) + elif module.params['http'] is False: + commands.append('no nxapi http') + +def set_https(module, commands): + port = module.params['https_port'] + if not 0 <= port <= 65535: + module.fail_json(msg='https_port must be between 1 and 65535') + elif module.params['https'] is True: + commands.append('nxapi https port %s' % port) + elif module.params['https'] is False: + commands.append('no nxapi https') + +def set_sandbox(module, commands): + if module.params['sandbox'] is True: + commands.append('nxapi sandbox') + elif module.params['sandbox'] is False: + commands.append('no nxapi sandbox') + +def get_config(module): + contents = module.params['config'] + if not contents: + try: + contents = module.cli(['show running-config nxapi all'])[0] + except NetworkError: + contents = None + config = NetworkConfig(indent=2) + if contents: + config.load(contents) + return config + +def load_checkpoint(module, result): + try: + checkpoint = result['__checkpoint__'] + module.cli(['rollback running-config checkpoint %s' % checkpoint, + 'no checkpoint %s' % checkpoint], output='text') + except KeyError: + module.fail_json(msg='unable to rollback, checkpoint not found') + except NetworkError: + exc = get_exception() + msg = 'unable to rollback configuration' + module.fail_json(msg=msg, checkpoint=checkpoint, **exc.kwargs) + +def load_config(module, commands, result): + # create a config checkpoint + checkpoint = 'ansible_%s' % int(time.time()) + module.cli(['checkpoint %s' % checkpoint], output='text') + result['__checkpoint__'] = checkpoint + + # load the config into the device + module.config.load_config(commands) + + # load was successfully, remove the config checkpoint + module.cli(['no checkpoint %s' % checkpoint]) + +def load(module, commands, result): + candidate = NetworkConfig(indent=2, contents='\n'.join(commands)) + config = get_config(module) + configobjs = candidate.difference(config) + + if configobjs: + commands = dumps(configobjs, 'commands').split('\n') + result['updates'] = commands + if not module.check_mode: + load_config(module, commands, result) result['changed'] = True - result['commands'] = commands - execute_commands(module, commands) - return result - - # configure http and https - for protocol in ['http', 'https']: - cmds, chg = http_commands(protocol, module.params['{0}_port'.format(protocol)], - module.params[protocol], config) - if chg: - commands += cmds - result['changed'] = True - - # configure sandbox - config_sandbox = config.get('sandbox_status', None) - enable_sandbox = module.params.get('sandbox') - - if config_sandbox is None: - # there is no prior state, so we must set one - result['changed'] = True - if enable_sandbox: - commands.append('nxapi sandbox') - else: - commands.append('no nxapi sandbox') - else: - # there is a prior state, so be idempotent - if config_sandbox == 'Enabled' and not enable_sandbox: - # turn off sandbox - commands.append('no nxapi sandbox') - result['changed'] = True - elif config_sandbox == 'Disabled' and enable_sandbox: - # turn on sandbox - commands.append('nxapi sandbox') - result['changed'] = True - - if len(commands) > 0: - # something requires change - result['commands'] = commands - execute_commands(module, commands) - - return result + +def clean_result(result): + # strip out any keys that have two leading and two trailing + # underscore characters + for key in result.keys(): + if PRIVATE_KEYS_RE.match(key): + del result[key] def main(): @@ -241,28 +249,56 @@ def main(): """ argument_spec = dict( - state=dict(default='started', choices=['started', 'stopped']), - http_port=dict(default=80, type='int'), - https_port=dict(default=443, type='int'), - http=dict(aliases=['enable_http'], default=True, type='bool'), - https=dict(aliases=['enable_https'], default=True, type='bool'), - sandbox=dict(aliases=['enable_sandbox'], default=True, type='bool'), + http=dict(aliases=['enable_http'], default=True, type='bool', setter='set_http'), + http_port=dict(default=80, type='int', setter='set_http'), + + https=dict(aliases=['enable_https'], default=False, type='bool', setter='set_https'), + https_port=dict(default=443, type='int', setter='set_https'), + + sandbox=dict(aliases=['enable_sandbox'], default=False, type='bool'), # Only allow configuration of NXAPI using cli transpsort - transport=dict(required=True, choices=['cli']) + transport=dict(required=True, choices=['cli']), + + config=dict(), + + # Support for started and stopped is for backwards capability only and + # will be removed in a future version + state=dict(default='present', choices=['started', 'stopped', 'present', 'absent']) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + supports_check_mode=True) - result = config_server(module) + state = module.params['state'] - return module.exit_json(**result) + warnings = list() + + result = dict(changed=False, warnings=warnings) + + if state == 'started': + state = 'present' + warnings.append('state=started is deprecated and will be removed in a ' + 'a future release. Please use state=present instead') + elif state == 'stopped': + state = 'absent' + warnings.append('state=stopped is deprecated and will be removed in a ' + 'a future release. Please use state=absent instead') + + commands = list() + invoke(state, module, commands) + + try: + load(module, commands, result) + except (ValueError, NetworkError): + load_checkpoint(module, result) + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) + + clean_result(result) + module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() - From a6ffe2e7be96f01e2213ec5c373ca6bb18a1f51e Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 17:14:33 -0400 Subject: [PATCH 017/770] removes output keyword from command in ios_command IOS devices only support a single command output which is structured text. This removes the ability to specify the command output format when providing complex arguments to the commands --- network/ios/ios_command.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/network/ios/ios_command.py b/network/ios/ios_command.py index 31ae8da9631..b169c5e14c4 100644 --- a/network/ios/ios_command.py +++ b/network/ios/ios_command.py @@ -144,7 +144,7 @@ from ansible.module_utils.netcli import AddCommandError, FailedConditionsError from ansible.module_utils.ios import NetworkModule, NetworkError -VALID_KEYS = ['command', 'output', 'prompt', 'response'] +VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: @@ -158,15 +158,13 @@ def parse_commands(module): cmd = dict(command=cmd, output=None) elif 'command' not in cmd: module.fail_json(msg='command keyword argument is required') - elif cmd.get('output') not in [None, 'text', 'json']: - module.fail_json(msg='invalid output specified for command') elif not set(cmd.keys()).issubset(VALID_KEYS): module.fail_json(msg='unknown keyword specified') yield cmd def main(): spec = dict( - # { command: , output: , prompt: , response: } + # { command: , prompt: , response: } commands=dict(type='list', required=True), wait_for=dict(type='list', aliases=['waitfor']), From 2133b9298024e4f09101fd9ff51e49b9d8f90bc9 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 17:52:31 -0400 Subject: [PATCH 018/770] update ops_config module with new enhancements * add src argument to provide path to config file * add new choice to match used to ignore current running config * add update argument with choices merge or check * add backup argument to backup current running config to control host * add save argument to save current running config to startup config * add state argument to control state of config file * deprecated force argument, use match=none instead Note: this module only supports transport=cli Tested on OpenSwitch 0.4.0 --- network/openswitch/ops_config.py | 291 +++++++++++++++++++++---------- 1 file changed, 199 insertions(+), 92 deletions(-) diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index 37960038b2b..0370eb79686 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -20,7 +20,7 @@ --- module: ops_config version_added: "2.1" -author: "Peter sprygada (@privateip)" +author: "Peter Sprygada (@privateip)" short_description: Manage OpenSwitch configuration using CLI description: - OpenSwitch configurations use a simple block indent file syntax @@ -36,7 +36,8 @@ in the device running-config. Be sure to note the configuration command syntax as some commands are automatically modified by the device config parser. - required: true + required: false + default: null parents: description: - The ordered set of parents that uniquely identify the section @@ -45,6 +46,17 @@ level or global commands. required: false default: null + src: + description: + - The I(src) argument provides a path to the configuration file + to load into the remote system. The path can either be a full + system path to the configuration file if the value starts with / + or relative to the root of the implemented role or playbook. + This arugment is mutually exclusive with the I(lines) and + I(parents) arguments. + required: false + default: null + version_added: "2.2" before: description: - The ordered set of commands to push on to the command stack if @@ -72,7 +84,7 @@ must be an equal match. required: false default: line - choices: ['line', 'strict', 'exact'] + choices: ['line', 'strict', 'exact', 'none'] replace: description: - Instructs the module on the way to perform the configuration @@ -90,9 +102,25 @@ current devices running-config. When set to true, this will cause the module to push the contents of I(src) into the device without first checking if already configured. + - Note this argument should be considered deprecated. To achieve + the equivalent, set the match argument to none. This argument + will be removed in a future release. required: false default: false - choices: ['true', 'false'] + choices: ['yes', 'no'] + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) I(replace) and I(check). When the argument is + set to I(merge), the configuration changes are merged with the current + device running configuration. When the argument is set to I(check) + the configuration updates are determined but not actually configured + on the remote device. + required: false + default: merge + choices: ['merge', 'check'] + version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -104,20 +132,55 @@ config for comparison. required: false default: null + save: + description: + - The C(save) argument instructs the module to save the running- + config to the startup-config at the conclusion of the module + running. If check mode is specified, this argument is ignored. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" + state: + description: + - This argument specifies whether or not the running-config is + present on the remote device. When set to I(absent) the + running-config on the remote device is erased. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: netop + password: netop + - name: configure hostname over cli ops_config: - lines: - - "hostname {{ inventory_hostname }}" + lines: + - "hostname {{ inventory_hostname }}" + provider: "{{ cli }}" + - name: configure vlan 10 over cli ops_config: - lines: - - no shutdown - parents: - - vlan 10 + lines: + - no shutdown + parents: + - vlan 10 + provider: "{{ cli }}" + +- name: load config from file + ops_config: + src: ops01.cfg + backup: yes + provider: "{{ cli }}" """ RETURN = """ @@ -126,122 +189,166 @@ returned: always type: list sample: ['...', '...'] - +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: path + sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34 responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] """ import re -import itertools - -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config - return config - - -def build_candidate(lines, parents, config, strategy): - candidate = list() - - if strategy == 'strict': - for index, cmd in enumerate(lines): - try: - if cmd != config[index]: - candidate.append(cmd) - except IndexError: - candidate.append(cmd) - - elif strategy == 'exact': - if len(lines) != len(config): - candidate = list(lines) - else: - for cmd, cfg in itertools.izip(lines, config): - if cmd != cfg: - candidate = list(lines) - break - else: - for cmd in lines: - if cmd not in config: - candidate.append(cmd) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.openswitch import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.netcli import Command +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + +def check_args(module, warnings): + if module.params['parents']: + if not module.params['lines'] or module.params['src']: + warnings.append('ignoring unnecessary argument parents') + if module.params['force']: + warnings.append('The force argument is deprecated, please use ' + 'match=none instead. This argument will be ' + 'removed in the future') + +def get_config(module, result): + contents = module.params['config'] + if not contents: + contents = module.config.get_config() + return NetworkConfig(indent=4, contents=contents) + +def get_candidate(module): + candidate = NetworkConfig(indent=4) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) return candidate +def load_backup(module): + try: + module.cli(['exit', 'config replace flash:/ansible-rollback force']) + except NetworkError: + module.fail_json(msg='unable to rollback configuration') + +def backup_config(module): + cmd = 'copy running-config flash:/ansible-rollback' + cmd = Command(cmd, prompt=re.compile('\? $'), response='\n') + module.cli(cmd) + +def load_config(module, commands, result): + if not module.check_mode and module.params['update'] != 'check': + module.config(commands) + result['changed'] = module.params['update'] != 'check' + result['updates'] = commands.split('\n') + +def present(module, result): + match = module.params['match'] + replace = module.params['replace'] + + candidate = get_candidate(module) + + if match != 'none': + config = get_config(module, result) + configobjs = candidate.difference(config, match=match, replace=replace) + else: + config = None + configobjs = candidate.items + + if configobjs: + commands = dumps(configobjs, 'commands') + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + # send the configuration commands to the device and merge + # them with the current running config + load_config(module, commands, result) + + if module.params['save'] and not module.check_mode: + module.config.save_config() + +def absent(module, result): + if not module.check_mode: + module.cli('erase startup-config') + result['changed'] = True def main(): argument_spec = dict( - lines=dict(aliases=['commands'], required=True, type='list'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), + + src=dict(type='path'), + before=dict(type='list'), after=dict(type='list'), - match=dict(default='line', choices=['line', 'strict', 'exact']), + + match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), + + # this argument is deprecated in favor of setting match: none + # it will be removed in a future version force=dict(default=False, type='bool'), - config=dict(), - transport=dict(default='cli', choices=['cli']) - ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + update=dict(choices=['merge', 'check'], default='merge'), + backup=dict(type='bool', default=False), - lines = module.params['lines'] - parents = module.params['parents'] or list() + config=dict(), + default=dict(type='bool', default=False), - before = module.params['before'] - after = module.params['after'] + save=dict(type='bool', default=False), - match = module.params['match'] - replace = module.params['replace'] + state=dict(choices=['present', 'absent'], default='present'), - contents = get_config(module) - config = module.parse_config(contents) + # ops_config is only supported over Cli transport so force + # the value of transport to be cli + transport=dict(default='cli', choices=['cli']) + ) - if parents: - for parent in parents: - for item in config: - if item.text == parent: - config = item + mutually_exclusive = [('lines', 'src')] - try: - children = [c.text for c in config.children] - except AttributeError: - children = [c.text for c in config] + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) - else: - children = [c.text for c in config if not c.parents] + state = module.params['state'] - result = dict(changed=False) + if module.params['force'] is True: + module.params['match'] = 'none' - candidate = build_candidate(lines, parents, children, match) + warnings = list() + check_args(module, warnings) - if candidate: - if replace == 'line': - candidate[:0] = parents - else: - candidate = list(parents) - candidate.extend(lines) + result = dict(changed=False, warnings=warnings) - if before: - candidate[:0] = before + if module.params['backup']: + result['__backup__'] = module.config.get_config() - if after: - candidate.extend(after) + try: + invoke(state, module, result) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) - if not module.check_mode: - response = module.configure(candidate) - result['responses'] = response - result['changed'] = True + module.exit_json(**result) - result['updates'] = candidate - return module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.openswitch import * if __name__ == '__main__': main() + From cbbb4af99a704f6b4648a8005be6ec01ce2aea41 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 20:37:17 -0400 Subject: [PATCH 019/770] update ops_facts with enhancements * adds support for default facts subset * adds support for config facts subset * maintain legacy facts from ops_facts pre-2.2 Tested on Openswitch 0.4.0 --- network/openswitch/ops_facts.py | 361 ++++++++++++++++++++++++++------ 1 file changed, 300 insertions(+), 61 deletions(-) diff --git a/network/openswitch/ops_facts.py b/network/openswitch/ops_facts.py index df33ca8958e..162dc710f5e 100644 --- a/network/openswitch/ops_facts.py +++ b/network/openswitch/ops_facts.py @@ -22,10 +22,16 @@ author: "Peter Sprygada (@privateip)" short_description: Collect device specific facts from OpenSwitch description: - - This module collects additional device fact information from a - remote device running OpenSwitch using either the CLI or REST - interfaces. It provides optional arguments for collecting fact - information. + - Collects facts from devices running the OpenSwitch operating + system. Fact collection is supported over both Cli and Rest + transports. This module prepends all of the base network fact keys + with C(ansible_net_). The facts module will always collect a + base set of facts from the device and can enable or disable + collection of additional facts. + - The facts collected from pre Ansible 2.2 are still available and + are collected for backwards compatibility; however, these facts + should be considered deprecated and will be removed in a future + release. extends_documentation_fragment: openswitch options: config: @@ -47,129 +53,362 @@ valid when the C(transport=rest). required: false default: null -notes: - - The use of the REST transport is still experimental until it is - fully implemented. + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, hardware, config, legacy, and interfaces. Can specify a + list of values to include a larger subset. Values can also be used + with an initial C(M(!)) to specify that a specific subset should + not be collected. + required: false + default: '!config' + version_added: "2.2" """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: netop + password: netop + transport: cli + rest: + host: "{{ inventory_hostname }}" + username: netop + password: netop + transport: rest + +- ops_facts: + gather_subset: all + provider: "{{ rest }}" + +# Collect only the config and default facts +- ops_facts: + gather_subset: config + provider: "{{ cli }}" + +# Do not collect config facts +- ops_facts: + gather_subset: + - "!config" + provider: "{{ cli }}" + - name: collect device facts ops_facts: + provider: "{{ cli }}" - name: include the config ops_facts: config: yes + provider: "{{ rest }}" - name: include a set of rest endpoints ops_facts: endpoints: - /system/interfaces/1 - /system/interfaces/2 + provider: "{{ rest }}" """ RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +# default +ansible_net_model: + description: The model name returned from the device + returned: when transport is cli + type: str +ansible_net_serialnum: + description: The serial number of the remote device + returned: when transport is cli + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: string +ansible_net_image: + description: The image file the device is running + returned: when transport is cli + type: string + +# config +ansible_net_config: + description: The current active config from the device + returned: when config is enabled + type: str + +# legacy (pre Ansible 2.2) config: description: The current system configuration returned: when enabled type: string sample: '....' - hostname: description: returns the configured hostname returned: always type: string sample: ops01 - version: description: The current version of OpenSwitch returned: always type: string sample: '0.3.0' - endpoints: description: The JSON response from the URL endpoint - returned: when endpoints argument is defined + returned: when endpoints argument is defined and transport is rest type: list sample: [{....}, {....}] """ import re +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner, AddCommandError +from ansible.module_utils.openswitch import NetworkModule -def get(module, url, expected_status=200): - response = module.connection.get(url) - if response.headers['status'] != expected_status: - module.fail_json(**response.headers) - return response +def add_command(runner, command): + try: + runner.add_command(command) + except AddCommandError: + # AddCommandError is raised for any issue adding a command to + # the runner. Silently ignore the exception in this case + pass -def get_config(module): - if module.params['transport'] == 'ssh': - rc, out, err = module.run_command('vtysh -c "show running-config"') - return out - elif module.params['transport'] == 'rest': - response = get(module, '/system/full-configuration') - return response.json - elif module.params['transport'] == 'cli': - response = module.connection.send('show running-config') - return response[0] +class FactsBase(object): -def get_facts(module): - if module.params['transport'] == 'rest': - response = get(module, '/system') + def __init__(self, module, runner): + self.module = module + self.transport = module.params['transport'] + self.runner = runner + self.facts = dict() + + if self.transport == 'cli': + self.commands() + + def populate(self): + getattr(self, self.transport)() + + def cli(self): + pass + + def rest(self): + pass + + +class Default(FactsBase): + + def commands(self): + add_command(self.runner, 'show system') + add_command(self.runner, 'show hostname') + + def rest(self): + self.facts.update(self.get_system()) + + def cli(self): + data = self.runner.get_command('show system') + + self.facts['version'] = self.parse_version(data) + self.facts['serialnum'] = self.parse_serialnum(data) + self.facts['model'] = self.parse_model(data) + self.facts['image'] = self.parse_image(data) + + self.facts['hostname'] = self.runner.get_command('show hostname') + + def parse_version(self, data): + match = re.search(r'OpenSwitch Version\s+: (\S+)', data) + if match: + return match.group(1) + + def parse_model(self, data): + match = re.search(r'Platform\s+:\s(\S+)', data, re.M) + if match: + return match.group(1) + + def parse_image(self, data): + match = re.search(r'\(Build: (\S+)\)', data, re.M) + if match: + return match.group(1) + + def parse_serialnum(self, data): + match = re.search(r'Serial Number\s+: (\S+)', data) + if match: + return match.group(1) + + def get_system(self): + response = self.module.connection.get('/system') return dict( hostname=response.json['configuration']['hostname'], version=response.json['status']['switch_version'] ) - elif module.params['transport'] == 'cli': - response = module.connection.send(['show system', 'show hostname']) - facts = dict() - facts['hostname'] = response[1] - match = re.search(r'OpenSwitch Version\s*:\s*(.*)$', response[0], re.M) + + +class Config(FactsBase): + + def commands(self): + add_command(self.runner, 'show running-config') + + def cli(self): + self.facts['config'] = self.runner.get_command('show running-config') + +class Legacy(FactsBase): + # facts from ops_facts 2.1 + + def commands(self): + add_command(self.runner, 'show system') + add_command(self.runner, 'show hostname') + + if self.module.params['config']: + add_command(self.runner, 'show running-config') + + def rest(self): + self.facts['_endpoints'] = self.get_endpoints() + self.facts.update(self.get_system()) + + if self.module.params['config']: + self.facts['_config'] = self.get_config() + + def cli(self): + self.facts['_hostname'] = self.runner.get_command('show hostname') + + data = self.runner.get_command('show system') + self.facts['_version'] = self.parse_version(data) + + if self.module.params['config']: + self.facts['_config'] = self.runner.get_command('show running-config') + + def parse_version(self, data): + match = re.search(r'OpenSwitch Version\s+: (\S+)', data) if match: - facts['version'] = match.group(1) - return facts - return dict() + return match.group(1) + def get_endpoints(self): + responses = list() + urls = self.module.params['endpoints'] or list() + for ep in urls: + response = self.module.connection.get(ep) + if response.headers['status'] != 200: + self.module.fail_json(msg=response.headers['msg']) + responses.append(response.json) + return responses -def main(): - """ main entry point for module execution - """ + def get_system(self): + response = self.module.connection.get('/system') + return dict( + _hostname=response.json['configuration']['hostname'], + _version=response.json['status']['switch_version'] + ) + + def get_config(self): + response = self.module.connection.get('/system/full-configuration') + return response.json +def check_args(module, warnings): + if module.params['transport'] != 'rest' and module.params['endpoints']: + warnings.append('Endpoints can only be collected when transport is ' + 'set to "rest". Endpoints will not be collected') + + +FACT_SUBSETS = dict( + default=Default, + config=Config, + legacy=Legacy +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + +def main(): spec = dict( - endpoints=dict(type='list'), + gather_subset=dict(default=['!config'], type='list'), + + # the next two arguments are legacy from pre 2.2 ops_facts + # these will be deprecated and ultimately removed config=dict(default=False, type='bool'), + endpoints=dict(type='list'), + transport=dict(default='cli', choices=['cli', 'rest']) ) - module = get_module(argument_spec=spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=spec, supports_check_mode=True) + + gather_subset = module.params['gather_subset'] + + warnings = list() + check_args(module, warnings) + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Bad subset') + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + runable_subsets.add('legacy') - endpoints = module.params['endpoints'] or list() - if endpoints and not module.params['transport'] == 'rest': - module.fail_json(msg="endpoints argument can only be used " - "with transport `rest`") + facts = dict() + facts['gather_subset'] = list(runable_subsets) - result = dict(changed=False) + runner = CommandRunner(module) - facts = get_facts(module) + instances = list() + for key in runable_subsets: + instances.append(FACT_SUBSETS[key](module, runner)) - if module.params['config']: - facts['config'] = get_config(module) + if module.params['transport'] == 'cli': + runner.run() - responses = list() - for ep in endpoints: - response = get(module, ep) - responses.append(response.json) + try: + for inst in instances: + inst.populate() + facts.update(inst.facts) + except Exception: + raise + module.exit_json(out=module.from_json(runner.items)) - if responses: - facts['endpoints'] = responses + ansible_facts = dict() + for key, value in facts.iteritems(): + # this is to maintain capability with ops_facts 2.1 + if key.startswith('_'): + ansible_facts[key[1:]] = value + else: + key = 'ansible_net_%s' % key + ansible_facts[key] = value - result['ansible_facts'] = facts - module.exit_json(**result) + module.exit_json(ansible_facts=ansible_facts, warnings=warnings) -from ansible.module_utils.basic import * -from ansible.module_utils.openswitch import * if __name__ == '__main__': main() From 2a06a594ec94e524db80d641c56dce8617526d63 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Tue, 30 Aug 2016 20:06:52 -0700 Subject: [PATCH 020/770] Incorporated Ansible community feedback --- network/dnos10/__init__.py | 0 network/{dell => dnos10}/dnos10_command.py | 7 +- network/{dell => dnos10}/dnos10_config.py | 86 +++++++++++++--------- 3 files changed, 56 insertions(+), 37 deletions(-) create mode 100644 network/dnos10/__init__.py rename network/{dell => dnos10}/dnos10_command.py (96%) rename network/{dell => dnos10}/dnos10_config.py (77%) diff --git a/network/dnos10/__init__.py b/network/dnos10/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/dell/dnos10_command.py b/network/dnos10/dnos10_command.py similarity index 96% rename from network/dell/dnos10_command.py rename to network/dnos10/dnos10_command.py index aa682711b5b..fd068ffc2e7 100644 --- a/network/dell/dnos10_command.py +++ b/network/dnos10/dnos10_command.py @@ -23,7 +23,7 @@ author: "Senthil Kumar Ganesan (@skg_net)" short_description: Run commands on remote devices running Dell OS10 description: - - Sends arbitrary commands to an Dell OS10 node and returns the results + - Sends arbitrary commands to a Dell OS10 node and returns the results read from the device. This module includes an argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. @@ -44,11 +44,10 @@ - List of conditions to evaluate against the output of the command. The task will wait for each condition to be true before moving forward. If the conditional is not true - within the configured number of retries, the task fails. + within the configured number of I(retries), the task fails. See examples. required: false default: null - aliases: ['waitfor'] retries: description: - Specifies the number of retries a command should by tried @@ -145,7 +144,7 @@ def to_lines(stdout): def main(): spec = dict( commands=dict(type='list', required=True), - wait_for=dict(type='list', aliases=['waitfor']), + wait_for=dict(type='list'), retries=dict(default=10, type='int'), interval=dict(default=1, type='int') ) diff --git a/network/dell/dnos10_config.py b/network/dnos10/dnos10_config.py similarity index 77% rename from network/dell/dnos10_config.py rename to network/dnos10/dnos10_config.py index 3d7a0477d5a..26e731aa17c 100644 --- a/network/dell/dnos10_config.py +++ b/network/dnos10/dnos10_config.py @@ -23,8 +23,8 @@ author: "Senthil Kumar Ganesan (@skg_net)" short_description: Manage Dell OS10 configuration sections description: - - Dell OS10 configurations use a simple block indent file sytanx - for segementing configuration into sections. This module provides + - Dell OS10 configurations use a simple block indent file syntax + for segmenting configuration into sections. This module provides an implementation for working with Dell OS10 configuration sections in a deterministic way. extends_documentation_fragment: dnos10 @@ -35,7 +35,7 @@ section. The commands must be the exact same commands as found in the device running-config. Be sure to note the configuration command syntax as some commands are automatically modified by the - device config parser. + device config parser. This argument is mutually exclusive with O(src). required: false default: null aliases: ['commands'] @@ -62,7 +62,7 @@ a change needs to be made. This allows the playbook designer the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched - against the system + against the system. required: false default: null after: @@ -93,20 +93,39 @@ the modified lines are pushed to the device in configuration mode. If the replace argument is set to I(block) then the entire command block is pushed to the device in configuration mode if any - line is not correct + line is not correct. required: false default: line choices: ['line', 'block'] - update_config: + update: description: - - This arugment will either cause or prevent the changed commands - from being sent to the remote device. The set to true, the - remote Dell OS10 device will be configured with the updated commands - and when set to false, the remote device will not be updated. + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) and I(check). When the argument is set to + I(merge), the configuration changes are merged with the current + device running configuration. When the argument is set to I(check) + the configuration updates are determined but not actually configured + on the remote device. required: false - default: yes + default: merge + choices: ['merge', 'check'] + save: + description: + - The C(save) argument instructs the module to save the running- + config to the startup-config at the conclusion of the module + running. If check mode is specified, this argument is ignored. + required: false + default: no choices: ['yes', 'no'] - backup_config: + config: + description: + - The C(config) argument allows the playbook desginer to supply + the base configuration to be used to validate configuration + changes necessary. If this argument is provided, the module + will not download the running-config from the remote node. + required: false + default: null + backup: description: - This argument will cause the module to create a full backup of the current C(running-config) from the remote device before any @@ -121,7 +140,7 @@ EXAMPLES = """ - dnos10_config: lines: ['hostname {{ inventory_hostname }}'] - force: yes + provider: "{{ cli }}" - dnos10_config: lines: @@ -130,9 +149,10 @@ - 30 permit ip host 3.3.3.3 any log - 40 permit ip host 4.4.4.4 any log - 50 permit ip host 5.5.5.5 any log - parents: ['ip access-list extended test'] - before: ['no ip access-list extended test'] + parents: ['ip access-list test'] + before: ['no ip access-list test'] match: exact + provider: "{{ cli }}" - dnos10_config: lines: @@ -140,15 +160,10 @@ - 20 permit ip host 2.2.2.2 any log - 30 permit ip host 3.3.3.3 any log - 40 permit ip host 4.4.4.4 any log - parents: ['ip access-list extended test'] - before: ['no ip access-list extended test'] - replace: block - -- dnos10_config: - commands: "{{lookup('file', 'datcenter1.txt')}}" parents: ['ip access-list test'] before: ['no ip access-list test'] replace: block + provider: "{{ cli }}" """ @@ -164,9 +179,17 @@ retured: when not check_mode type: list sample: ['...', '...'] + +saved: + description: Returns whether the configuration is saved to the startup + configuration or not. + retured: when not check_mode + type: bool + sample: True + """ from ansible.module_utils.netcfg import NetworkConfig, dumps, ConfigLine -from ansible.module_utils.dnos10 import NetworkModule, dnos10_argument_spec +from ansible.module_utils.dnos10 import NetworkModule from ansible.module_utils.dnos10 import get_config, get_sublevel_config def get_candidate(module): @@ -194,10 +217,11 @@ def main(): choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), - update_config=dict(type='bool', default=True), - backup_config=dict(type='bool', default=False) + update=dict(choices=['merge', 'check'], default='merge'), + save=dict(type='bool', default=False), + config=dict(), + backup =dict(type='bool', default=False) ) - argument_spec.update(dnos10_argument_spec) mutually_exclusive = [('lines', 'src')] @@ -206,18 +230,15 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - module.check_mode = not module.params['update_config'] - parents = module.params['parents'] or list() match = module.params['match'] replace = module.params['replace'] - before = module.params['before'] result = dict(changed=False, saved=False) candidate = get_candidate(module) - if module.params['match'] != 'none': + if match != 'none': config = get_config(module) if parents: contents = get_sublevel_config(config, module) @@ -227,7 +248,7 @@ def main(): else: configobjs = candidate.items - if module.params['backup_config']: + if module.params['backup']: result['__backup__'] = module.cli('show running-config')[0] commands = list() @@ -241,18 +262,17 @@ def main(): if module.params['after']: commands.extend(module.params['after']) - if not module.check_mode: + if not module.check_mode and module.params['update'] == 'merge': response = module.config.load_config(commands) result['responses'] = response - if module.params['save_config']: + if module.params['save']: module.config.save_config() result['saved'] = True result['changed'] = True result['updates'] = commands - result['connected'] = module.connected module.exit_json(**result) From 166c2d0272b81597af2a2da696432f938f30b393 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 14:58:04 +0200 Subject: [PATCH 021/770] Fixing string case --- network/nxos/nxos_bgp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 682cf9fcb3e..b8d21f82f8e 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -41,7 +41,7 @@ required: true vrf: description: - - Name of the VRF. The name 'default' is a valid VRF representing the global bgp. + - Name of the VRF. The name 'default' is a valid VRF representing the global BGP. required: false default: null bestpath_always_compare_med: From ec1c490888d88c272294add12c466e9fc032b244 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 31 Aug 2016 14:31:16 +0100 Subject: [PATCH 022/770] Consistent naming of Arista EOS device --- network/eos/eos_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index 2690ef9bf7c..931ca237fb0 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -21,7 +21,7 @@ module: eos_command version_added: "2.1" author: "Peter Sprygada (@privateip)" -short_description: Run arbitrary commands on EOS device +short_description: Run arbitrary commands on an Arista EOS device description: - Sends an arbitrary set of commands to an EOS node and returns the results read from the device. This module includes an From c215398e2f30d1a7ca44301262e243085bfbd2b0 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 28 Aug 2016 16:38:10 -0400 Subject: [PATCH 023/770] update eos_eapi module with new enhancements * add support for vrf configurations * add support for configing the qos value for eapi * add config argument to specify the device running-config Tested on EOS 4.15.4F --- network/eos/eos_eapi.py | 534 +++++++++++++++++++++++----------------- 1 file changed, 305 insertions(+), 229 deletions(-) diff --git a/network/eos/eos_eapi.py b/network/eos/eos_eapi.py index e4207682364..7edbe3fcda6 100644 --- a/network/eos/eos_eapi.py +++ b/network/eos/eos_eapi.py @@ -21,8 +21,8 @@ --- module: eos_eapi version_added: "2.1" -author: "Chris Houseknecht (@chouseknecht)" -short_description: Manage and configure eAPI. +author: "Peter Sprygada (@privateip)" +short_description: Manage and configure Arista EOS eAPI. requirements: - "EOS v4.12 or greater" description: @@ -35,259 +35,335 @@ - Requires EOS v4.12 or greater. extends_documentation_fragment: eos options: - state: - description: - - A state of I(started) will - enable eAPI access, and a state of I(stopped) will - disable or shutdown all eAPI access. - choices: - - started - - stopped - required: false - default: started - http_port: - description: - - Port on which the HTTP server will listen. - required: false - default: 80 - https_port: - description: - - Port on which the HTTPS server will listen. - required: false - default: 443 - local_http_port: - description: - - Port on which the local HTTP server will listen. - required: false - default: 8080 - http: - description: - - Enable HTTP server access. - required: false - default: true - choices: - - yes - - no - aliases: - - enable_http - https: - description: - - Enable HTTPS server access. - required: false - default: true - choices: - - yes - - no - aliases: - - enable_https - local_http: - description: - - Enable local HTTP server access. - required: false - default: false - choices: - - yes - - no - aliases: - - enable_local_http - socket: - description: - - Enable Unix socket server access. - required: false - default: false - choices: - - yes - - no - aliases: - - enable_socket + http: + description: + - The C(http) argument controls the operating state of the HTTP + transport protocol when eAPI is present in the running-config. + When the value is set to True, the HTTP protocol is enabled and + when the value is set to False, the HTTP protocol is disabled. + By default, when eAPI is first configured, the HTTP protocol is + disabled. + required: false + default: yes + choices: ['yes', 'no'] + aliases: ['enable_http'] + http_port: + description: + - Configures the HTTP port that will listen for connections when + the HTTP transport protocol is enabled. This argument accepts + integer values in the valid range of 1 to 65535. + required: false + default: 80 + https: + description: + - The C(https) argument controls the operating state of the HTTPS + transport protocol when eAPI is present in the running-config. + When the value is set to True, the HTTPS protocol is enabled and + when the value is set to False, the HTTPS protocol is disabled. + By default, when eAPI is first configured, the HTTPS protocol is + enabled. + required: false + default: yes + choices: ['yes', 'no'] + aliases: ['enable_http'] + https_port: + description: + - Configures the HTTP port that will listen for connections when + the HTTP transport protocol is enabled. This argument accepts + integer values in the valid range of 1 to 65535. + required: false + default: 443 + local_http: + description: + - The C(local_http) argument controls the operating state of the + local HTTP transport protocol when eAPI is present in the + running-config. When the value is set to True, the HTTP protocol + is enabled and restricted to connections from localhost only. When + the value is set to False, the HTTP local protocol is disabled. + - Note is value is independent of the C(http) argument + required: false + default: false + choices: ['yes', 'no'] + aliases: ['enable_local_http'] + local_http_port: + description: + - Configures the HTTP port that will listen for connections when + the HTTP transport protocol is enabled. This argument accepts + integer values in the valid range of 1 to 65535. + required: false + default: 8080 + socket: + description: + - The C(socket) argument controls the operating state of the UNIX + Domain Socket used to receive eAPI requests. When the value + of this argument is set to True, the UDS will listen for eAPI + requests. When the value is set to False, the UDS will not be + available to handle requests. By default when eAPI is first + configured, the UDS is disabled. + required: false + default: false + choices: ['yes', 'no'] + aliases: ['enable_socket'] + vrf: + description: + - The C(vrf) argument will configure eAPI to listen for connections + in the specified VRF. By default, eAPI transports will listen + for connections in the global table. This value requires the + VRF to already be created otherwise the task will fail. + required: false + default: default + version_added: "2.2" + qos: + description: + - The C(qos) argument configures the IP DSCP value to assign to + eAPI response packets. This argument accepts integer values + in the valid IP DSCP range of 0 to 63. + required: false + default: 0 + version_added: "2.2" + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task in a playbook. The I(config) argument allows the + implementer to pass in the configuration to use as the base + config for comparison. + required: false + default: nul + version_added: "2.2" + state: + description: + - The C(state) argument controls the operational state of eAPI + on the remote device. When this argument is set to C(started), + eAPI is enabled to receive requests and when this argument is + C(stopped), eAPI is disabled and will not receive requests. + required: false + default: started + choices: ['started', 'stopped'] """ EXAMPLES = """ - - name: Enable eAPI access with default configuration - eos_eapi: - state: started - provider: {{ provider }} - - - name: Enable eAPI with no HTTP, HTTPS at port 9443, local HTTP at port 80, and socket enabled - eos_eapi: - state: started - http: false - https_port: 9443 - local_http: yes - local_http_port: 80 - socket: yes - provider: {{ provider }} - - - name: Shutdown eAPI access - eos_eapi: - state: stopped - provider: {{ provider }} +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: admin + password: admin + +- name: Enable eAPI access with default configuration + eos_eapi: + state: started + provider: {{ cli }} + +- name: Enable eAPI with no HTTP, HTTPS at port 9443, local HTTP at port 80, and socket enabled + eos_eapi: + state: started + http: false + https_port: 9443 + local_http: yes + local_http_port: 80 + socket: yes + provider: {{ cli }} + +- name: Shutdown eAPI access + eos_eapi: + state: stopped + provider: {{ cli }} """ RETURN = """ -changed: - description: - - Indicates if commands were sent to the device. - returned: always - type: boolean - sample: false - -commands: - description: - - Set of commands to be executed on remote device - returned: always - type: list - sample: [ - 'management api http-commands', - 'shutdown' - ] - -_config: - description: - - Configuration found on the device prior to executing any commands. - returned: always - type: object - sample: {...} +updates: + description: + - Set of commands to be executed on remote device + returned: always + type: list + sample: ['management api http-commands', 'shutdown'] +urls: + description: Hash of URL endpoints eAPI is listening on per interface + returned: when eAPI is started + type: dict + sample: {'Management1': ['http://172.26.10.1:80']} """ - - -def http_commands(protocol, port, enable, config): - - started_config = config['{0}Server'.format(protocol)] - commands = [] - changed = False - - if started_config.get('running'): - if not enable: - # turn off server - commands.append('no protocol {0}'.format(protocol)) - changed = True - elif started_config.get('port') != port: - # update the port - commands.append('protocol {0} port {1}'.format(protocol, port)) - changed = True - elif not started_config.get('running') and enable: - # turn on server - commands.append('protocol {0} port {1}'.format(protocol, port)) - changed = True - - return commands, changed - - -def execute_commands(module, commands): - - if not module.params.get('check_mode'): - module.configure(commands) - - -def config_server(module): - - state = module.params.get('state') - local_http_port = module.params.get('local_http_port') - socket= module.params.get('socket') - local_http = module.params.get('local_http') - config = module.from_json(module.execute(['show management api http-commands | json'])[0]) - result = dict(changed=False, _config=config, commands=[]) - commands = [ - 'management api http-commands' - ] - - if not config.get('enabled'): - if state == 'started': - # turn on eAPI access - commands.append('no shutdown') - result['changed'] = True - else: - # state is stopped. nothing to do - return result - - if config.get('enabled') and state == 'stopped': - # turn off eAPI access and exit - commands.append('shutdown') - result['changed'] = True - result['commands'] = commands - execute_commands(module, commands) - return result - - # http and https - for protocol in ['http', 'https']: - cmds, chg = http_commands(protocol, module.params['{0}_port'.format(protocol)], - module.params['{0}'.format(protocol)], config) - if chg: - commands += cmds - result['changed'] = True - - # local HTTP - if config.get('localHttpServer').get('running'): - if not local_http: - # turn off local http server - commands.append('no protocol http localhost') - result['changed'] = True - elif config.get('localHttpServer').get('port') != local_http_port: - # update the local http port - commands.append('protocol http localhost port {0}'.format(local_http_port)) - result['changed'] = True - - if not config.get('localHttpServer').get('running') and local_http: - # turn on local http server - commands.append('protocol http localhost port {0}'.format(local_http_port)) - result['changed'] = True - - # socket server - if config.get('unixSocketServer').get('running') and not socket: - # turn off unix socket - commands.append('no protocol unix-socket') - result['changed'] = True - - if not config.get('unixSocketServer').get('running') and socket: - # turn on unix socket +import re +import time + +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.eos import NetworkModule, NetworkError +from ansible.module_utils.basic import get_exception + +PRIVATE_KEYS_RE = re.compile('__.+__') + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + +def started(module, commands): + commands.append('no shutdown') + setters = set() + for key, value in module.argument_spec.iteritems(): + if module.params[key] is not None: + setter = value.get('setter') or 'set_%s' % key + if setter not in setters: + setters.add(setter) + invoke(setter, module, commands) + +def stopped(module, commands): + commands.append('shutdown') + +def set_protocol_http(module, commands): + port = module.params['http_port'] + if not 1 <= port <= 65535: + module.fail_json(msg='http_port must be between 1 and 65535') + elif module.params['http'] is True: + commands.append('protocol http port %s' % port) + elif module.params['http'] is False: + commands.append('no protocol http') + +def set_protocol_https(module, commands): + port = module.params['https_port'] + if not 1 <= port <= 65535: + module.fail_json(msg='https_port must be between 1 and 65535') + elif module.params['https'] is True: + commands.append('protocol https port %s' % port) + elif module.params['https'] is False: + commands.append('no protocol https') + +def set_local_http(module, commands): + port = module.params['local_http_port'] + if not 1 <= port <= 65535: + module.fail_json(msg='local_http_port must be between 1 and 65535') + elif module.params['local_http'] is True: + commands.append('protocol http localhost port %s' % port) + elif module.params['local_http'] is False: + commands.append('no protocol http localhost port 8080') + +def set_socket(module, commands): + if module.params['socket'] is True: commands.append('protocol unix-socket') - result['changed'] = True - - if len(commands) > 1: - # something requires change - execute_commands(module, commands) - result['commands'] = commands + elif module.params['socket'] is False: + commands.append('no protocol unix-socket') - return result +def set_vrf(module, commands): + vrf = module.params['vrf'] + if vrf != 'default': + resp = module.cli(['show vrf']) + if vrf not in resp[0]: + module.fail_json(msg="vrf '%s' is not configured" % vrf) + commands.append('vrf %s' % vrf) + +def set_qos(module, commands): + if not 0 <= module.params['qos'] <= 63: + module.fail_json(msg='qos must be between 0 and 63') + commands.append('qos dscp %s' % module.params['qos']) + +def get_config(module): + contents = module.params['config'] + if not contents: + cmd = 'show running-config all section management api http-commands' + contents = module.cli([cmd]) + config = NetworkConfig(indent=3, contents=contents[0]) + return config + +def load_config(module, commands, result): + session = 'ansible_%s' % int(time.time()) + commit = not module.check_mode + + diff = module.config.load_config(commands, session=session, commit=commit) + + # once the configuration is done, remove the config session and + # remove the session name from the result + module.cli(['no configure session %s' % session]) + + result['diff'] = dict(prepared=diff) + result['changed'] = diff is not None + +def load(module, commands, result): + candidate = NetworkConfig(indent=3) + candidate.add(commands, parents=['management api http-commands']) + + config = get_config(module) + configobjs = candidate.difference(config) + + if configobjs: + commands = dumps(configobjs, 'commands').split('\n') + result['updates'] = commands + load_config(module, commands, result) + +def clean_result(result): + # strip out any keys that have two leading and two trailing + # underscore characters + for key in result.keys(): + if PRIVATE_KEYS_RE.match(key): + del result[key] + +def collect_facts(module, result): + resp = module.cli(['show management api http-commands'], output='json') + facts = dict(eos_eapi_urls=dict()) + for each in resp[0]['urls']: + intf, url = each.split(' : ') + key = str(intf).strip() + if key not in facts['eos_eapi_urls']: + facts['eos_eapi_urls'][key] = list() + facts['eos_eapi_urls'][key].append(str(url).strip()) + result['ansible_facts'] = facts -def check_version(module): - config = module.from_json(module.execute(['show version | json'])[0]) - versions = config['version'].split('.') - if int(versions[0]) < 4 or int(versions[1]) < 12: - module.fail_json(msg="Device version {0} does not support eAPI. eAPI was introduced in EOS 4.12.") def main(): """ main entry point for module execution """ argument_spec = dict( - state=dict(default='started', choices=['stopped','started']), - http_port=dict(default=80, type='int'), - https_port=dict(default=443, type='int'), - local_http_port=dict(default=8080, type='int'), - http=dict(aliases=['enable_http'], default=True, type='bool'), - https=dict(aliases=['enable_https'], default=True, type='bool'), + http=dict(aliases=['enable_http'], default=False, type='bool', setter='set_protocol_http'), + http_port=dict(default=80, type='int', setter='set_protocol_http'), + + https=dict(aliases=['enable_https'], default=True, type='bool', setter='set_protocol_https'), + https_port=dict(default=443, type='int', setter='set_protocol_https'), + + local_http=dict(aliases=['enable_local_http'], default=False, type='bool', setter='set_local_http'), + local_http_port=dict(default=8080, type='int', setter='set_local_http'), + socket=dict(aliases=['enable_socket'], default=False, type='bool'), - local_http=dict(aliases=['enable_local_http'], default=False, type='bool'), + + vrf=dict(default='default'), + qos=dict(default=0, type='int'), + + config=dict(), # Only allow use of transport cli when configuring eAPI - transport=dict(required=True, choices=['cli']) + transport=dict(required=True, choices=['cli']), + + state=dict(default='started', choices=['stopped', 'started']), ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + supports_check_mode=True) + + state = module.params['state'] + + warnings = list() + + result = dict(changed=False, warnings=warnings) - check_version(module) + commands = list() + invoke(state, module, commands) - result = config_server(module) + try: + load(module, commands, result) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) - return module.exit_json(**result) + collect_facts(module, result) + clean_result(result) + module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.eos import * if __name__ == '__main__': main() From cf32ae029098853fdbc557ed26e2dcc8ba201a62 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 16:35:19 +0200 Subject: [PATCH 024/770] Adding nxos_acl module --- network/nxos/nxos_acl.py | 1001 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1001 insertions(+) create mode 100644 network/nxos/nxos_acl.py diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py new file mode 100644 index 00000000000..eac9f52efe7 --- /dev/null +++ b/network/nxos/nxos_acl.py @@ -0,0 +1,1001 @@ +#!/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_acl +version_added: "2.2" +short_description: Manages access list entries for ACLs. +description: + - Manages access list entries for ACLs. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - I(state)=absent removes the ACE if it exists + - I(state)=delete_acl deleted the ACL if it exists + - for idempotency, use port numbers for the src/dest port + params like I(src_port1) and names for the well defined protocols + for the I(proto) param. + - while this module is idempotent in that if the ace as presented in the + task is identical to the one on the switch, no changes will be made. If + there is any difference, what is in Ansible will be pushed (configured + options will be overridden). This is to improve security, but at the + same time remember an ACE is removed, then re-added, so if there is a + change, the new ACE will be exacty what params you are sending to the + module. +options: + seq: + description: + - sequence number of the entry (ACE) + required: true + name: + description: + - Case sensitive name of the access list (ACL) + required: true + action: + description: + - action of the ACE + required: true + choices: ['permit', 'deny', 'remark'] + remark: + description: + - If action is set to remark, this is the description + required: false + default: null + proto: + description: + - port number or protocol (as supported by the switch) + required: true + src: + description: + - src ip and mask using IP/MASK notation and supports keyword 'any' + required: true + src_port_op: + description: + - src port operands such as eq, neq, gt, lt, range + required: false + default: null + choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range'] + src_port1: + description: + - port/protocol and also first (lower) port when using range + operand + required: false + default: null + src_port2: + description: + - second (end) port when using range operand + required: false + default: null + dest: + description: + - dest ip and mask using IP/MASK notation and supports the + keyword 'any' + required: true + default: null + dest_port_op: + description: + - dest port operands such as eq, neq, gt, lt, range + required: false + default: null + choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range'] + dest_port1: + description: + - port/protocol and also first (lower) port when using range + operand + required: false + default: null + dest_port2: + description: + - second (end) port when using range operand + required: false + default: null + log: + description: + - Log matches against this entry + required: false + default: null + choices: ['enable'] + urg: + description: + - Match on the URG bit + required: false + default: null + choices: ['enable'] + ack: + description: + - Match on the ACK bit + required: false + default: null + choices: ['enable'] + psh: + description: + - Match on the PSH bit + required: false + default: null + choices: ['enable'] + rst: + description: + - Match on the RST bit + required: false + default: null + choices: ['enable'] + syn: + description: + - Match on the SYN bit + required: false + default: null + choices: ['enable'] + fin: + description: + - Match on the FIN bit + required: false + default: null + choices: ['enable'] + established: + description: + - Match established connections + required: false + default: null + choices: ['enable'] + fragments: + description: + - Check non-initial fragments + required: false + default: null + choices: ['enable'] + time-range: + description: + - Name of time-range to apply + required: false + default: null + precedence: + description: + - Match packets with given precedence + required: false + default: null + choices: ['critical', 'flash', 'flash-override', 'immediate', + 'internet', 'network', 'priority', 'routine'] + dscp: + description: + - Match packets with given dscp value + required: false + default: null + choices: ['af11, 'af12, 'af13, 'af21', 'af22', 'af23','af31','af32', + 'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4', + 'cs5', 'cs6', 'cs7', 'default', 'ef'] + state: + description: + - Specify desired state of the resource + required: false + default: present + choices: ['present','absent','delete_acl'] +''' + +EXAMPLES = ''' + +# configure ACL ANSIBLE +- nxos_acl: + name: ANSIBLE + seq: 10 + action: permit + proto: tcp + src: 1.1.1.1/24 + dest: any + state: present + provider: "{{ nxos_provider }}" +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import itertools + +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + +def get_config(module): + config = module.params['running_config'] + if not config: + config = module.get_config() + 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_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +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. Instead, we assume if '^' is found in response, + it is an invalid command. + """ + if 'xml' in response[0]: + body = [] + elif '^' in response[0]: + 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): + 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)) + 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 get_acl(module, acl_name, seq_number): + command = 'show ip access-list' + new_acl = [] + saveme = {} + seqs = [] + acl_body = {} + + body = execute_show_command(command, module)[0] + all_acl_body = body['TABLE_ip_ipv6_mac']['ROW_ip_ipv6_mac'] + + for acl in all_acl_body: + if acl.get('acl_name') == acl_name: + acl_body = acl + + try: + acl_entries = acl_body['TABLE_seqno']['ROW_seqno'] + acl_name = acl_body.get('acl_name') + except KeyError: # could be raised if no ACEs are configured for an ACL + return saveme, [{'acl': 'no_entries'}], seqs + + if isinstance(acl_entries, dict): + acl_entries = [acl_entries] + + for each in acl_entries: + temp = collections.OrderedDict() + keep = {} + temp['name'] = acl_name + temp['seq'] = str(each.get('seqno')) + temp['options'] = {} + remark = each.get('remark') + if remark: + temp['remark'] = remark + temp['action'] = 'remark' + else: + temp['action'] = each.get('permitdeny') + temp['proto'] = each.get('proto', each.get('proto_str', each.get('ip'))) + temp['src'] = each.get('src_any', each.get('src_ip_prefix')) + temp['src_port_op'] = each.get('src_port_op') + temp['src_port1'] = each.get('src_port1_num') + temp['src_port2'] = each.get('src_port2_num') + temp['dest'] = each.get('dest_any', each.get('dest_ip_prefix')) + temp['dest_port_op'] = each.get('dest_port_op') + temp['dest_port1'] = each.get('dest_port1_num') + temp['dest_port2'] = each.get('dest_port2_num') + + options = collections.OrderedDict() + options['log'] = each.get('log') + options['urg'] = each.get('urg') + options['ack'] = each.get('ack') + options['psh'] = each.get('psh') + options['rst'] = each.get('rst') + options['syn'] = each.get('syn') + options['fin'] = each.get('fin') + options['established'] = each.get('established') + options['dscp'] = each.get('dscp_str') + options['precedence'] = each.get('precedence_str') + options['fragments'] = each.get('fragments') + options['time_range'] = each.get('timerange') + + options_no_null = {} + for key, value in options.iteritems(): + if value is not None: + options_no_null[key] = value + + keep['options'] = options_no_null + + for key, value in temp.iteritems(): + if value: + keep[key] = value + # ensure options is always in the dict + if keep.get('options', 'DNE') == 'DNE': + keep['options'] = {} + + if keep.get('seq') == seq_number: + saveme = dict(keep) + + seqs.append(str(keep.get('seq'))) + new_acl.append(keep) + + return saveme, new_acl, seqs + + +def _acl_operand(operand, srcp1, sprcp2): + sub_entry = ' ' + operand + + if operand == 'range': + sub_entry += ' ' + srcp1 + ' ' + sprcp2 + else: + sub_entry += ' ' + srcp1 + + return sub_entry + + +def config_core_acl(proposed): + seq = proposed.get('seq') + action = proposed.get('action') + remark = proposed.get('remark') + proto = proposed.get('proto') + src = proposed.get('src') + src_port_op = proposed.get('src_port_op') + src_port1 = proposed.get('src_port1') + src_port2 = proposed.get('src_port2') + + dest = proposed.get('dest') + dest_port_op = proposed.get('dest_port_op') + dest_port1 = proposed.get('dest_port1') + dest_port2 = proposed.get('dest_port2') + + ace_start_entries = [action, proto, src] + if not remark: + ace = seq + ' ' + ' '.join(ace_start_entries) + if src_port_op: + ace += _acl_operand(src_port_op, src_port1, src_port2) + ace += ' ' + dest + if dest_port_op: + ace += _acl_operand(dest_port_op, dest_port1, dest_port2) + else: + ace = seq + ' remark ' + remark + + return ace + + +def config_acl_options(options): + ENABLE_ONLY = ['psh', 'urg', 'log', 'ack', 'syn', + 'established', 'rst', 'fin', 'fragments', + 'log'] + + OTHER = ['dscp', 'precedence', 'time-range'] + # packet-length is the only option not currently supported + + if options.get('time_range'): + options['time-range'] = options.get('time_range') + options.pop('time_range') + + command = '' + for option, value in options.iteritems(): + if option in ENABLE_ONLY: + if value == 'enable': + command += ' ' + option + elif option in OTHER: + command += ' ' + option + ' ' + value + if command: + command = command.strip() + return command + + +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 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) + + +def main(): + argument_spec = dict( + seq=dict(required=True, type='str'), + name=dict(required=True, type='str'), + action=dict(required=True, choices=['remark', 'permit', 'deny']), + remark=dict(requried=False, type='str'), + proto=dict(required=True, type='str'), + src=dict(required=True, type='str'), + src_port_op=dict(required=False), + src_port1=dict(required=False, type='str'), + src_port2=dict(required=False, type='str'), + dest=dict(required=True, type='str'), + dest_port_op=dict(required=False), + dest_port1=dict(required=False, type='str'), + dest_port2=dict(required=False, type='str'), + log=dict(required=False, choices=['enable']), + urg=dict(required=False, choices=['enable']), + ack=dict(required=False, choices=['enable']), + psh=dict(required=False, choices=['enable']), + rst=dict(required=False, choices=['enable']), + syn=dict(required=False, choices=['enable']), + fragments=dict(required=False, choices=['enable']), + fin=dict(required=False, choices=['enable']), + established=dict(required=False, choices=['enable']), + time_range=dict(required=False), + precedence=dict(required=False, choices=['critical', 'flash', + 'flash-override', + 'immediate', 'internet', + 'network', 'priority', + 'routine']), + dscp=dict(required=False, choices=['af11', 'af12', 'af13', 'af21', + 'af22', 'af23', 'af31', 'af32', + 'af33', 'af41', 'af42', 'af43', + 'cs1', 'cs2', 'cs3', 'cs4', + 'cs5', 'cs6', 'cs7', 'default', + 'ef']), + state=dict(choices=['absent', 'present', 'delete_acl'], + default='present'), + protocol=dict(choices=['http', 'https'], default='http'), + host=dict(required=True), + username=dict(type='str'), + password=dict(no_log=True, type='str'), + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + action = module.params['action'] + remark = module.params['remark'] + dscp = module.params['dscp'] + precedence = module.params['precedence'] + seq = module.params['seq'] + name = module.params['name'] + seq = module.params['seq'] + + if action == 'remark' and not remark: + module.fail_json(msg='when state is action, remark param is also ' + 'required') + + REQUIRED = ['seq', 'name', 'action', 'proto', 'src', 'dest'] + ABSENT = ['name', 'seq'] + if state == 'present': + if action and remark and seq: + pass + else: + for each in REQUIRED: + if module.params[each] is None: + module.fail_json(msg="req'd params when state is present:", + params=REQUIRED) + elif state == 'absent': + for each in ABSENT: + if module.params[each] is None: + module.fail_json(msg='require params when state is absent', + params=ABSENT) + elif state == 'delete_acl': + if module.params['name'] is None: + module.fail_json(msg="param name req'd when state is delete_acl") + + if dscp and precedence: + module.fail_json(msg='only one of the params dscp/precedence ' + 'are allowed') + + OPTIONS_NAMES = ['log', 'urg', 'ack', 'psh', 'rst', 'syn', 'fin', + 'established', 'dscp', 'precedence', 'fragments', + 'time_range'] + + CORE = ['seq', 'name', 'action', 'proto', 'src', 'src_port_op', + 'src_port1', 'src_port2', 'dest', 'dest_port_op', + 'dest_port1', 'dest_port2', 'remark'] + + proposed_core = dict((param, value) for (param, value) in + module.params.iteritems() + if param in CORE and value is not None) + + proposed_options = dict((param, value) for (param, value) in + module.params.iteritems() + if param in OPTIONS_NAMES and value is not None) + proposed = {} + proposed.update(proposed_core) + proposed.update(proposed_options) + + existing_options = {} + + # getting existing existing_core=dict, acl=list, seq=list + existing_core, acl, seqs = get_acl(module, name, seq) + if existing_core: + existing_options = existing_core.get('options') + existing_core.pop('options') + + end_state = acl + commands = [] + changed = False + delta_core = {} + delta_options = {} + + if not existing_core.get('remark'): + delta_core = dict( + set(proposed_core.iteritems()).difference( + existing_core.iteritems()) + ) + delta_options = dict( + set(proposed_options.iteritems()).difference( + existing_options.iteritems()) + ) + + if state == 'present': + if delta_core or delta_options: + if existing_core: # if the ace exists already + commands.append(['no {0}'.format(seq)]) + if delta_options: + myacl_str = config_core_acl(proposed_core) + myacl_str += ' ' + config_acl_options(proposed_options) + else: + myacl_str = config_core_acl(proposed_core) + command = [myacl_str] + commands.append(command) + elif state == 'absent': + if existing_core: + commands.append(['no {0}'.format(seq)]) + elif state == 'delete_acl': + if acl[0].get('acl') != 'no_entries': + commands.append(['no ip access-list {0}'.format(name)]) + + results = {} + cmds = [] + if commands: + preface = [] + if state in ['present', 'absent']: + preface = ['ip access-list {0}'.format(name)] + commands.insert(0, preface) + + cmds = flatten_list(commands) + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + execute_config_command(cmds, module) + changed = True + existing_core, end_state, seqs = get_acl(module, name, seq) + + results['proposed'] = proposed + results['existing'] = existing_core + results['state'] = state + results['changed'] = changed + results['updates'] = cmds + results['end_state'] = end_state + + module.exit_json(**results) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.nxos import * +if __name__ == '__main__': + main() From 7d7357bbb61bbd2335944a0e9c61c665540e34c0 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 16:45:45 +0200 Subject: [PATCH 025/770] Fixing DOCSTRING --- network/nxos/nxos_acl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index eac9f52efe7..0049436bcdd 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -178,7 +178,7 @@ - Match packets with given dscp value required: false default: null - choices: ['af11, 'af12, 'af13, 'af21', 'af22', 'af23','af31','af32', + choices: ['af11', 'af12', 'af13', 'af21', 'af22', 'af23','af31','af32', 'af33', 'af41', 'af42', 'af43', 'cs1', 'cs2', 'cs3', 'cs4', 'cs5', 'cs6', 'cs7', 'default', 'ef'] state: From 6063071a4677971dc44d5a62b87ba47df48ed0e5 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 16:49:45 +0200 Subject: [PATCH 026/770] Fix traceback on python3 (#4556) Traceback (most recent call last): File "/tmp/ansible_csqv781s/ansible_module_systemd.py", line 374, in main() File "/tmp/ansible_csqv781s/ansible_module_systemd.py", line 263, in main for line in out.split('\\n'): # systemd can have multiline values delimited with {} --- system/systemd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/systemd.py b/system/systemd.py index 3c83e3f69b7..fb94ba445e5 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -217,6 +217,7 @@ import os import glob from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_bytes, to_native # =========================================== # Main control flow @@ -260,7 +261,7 @@ def main(): # load return of systemctl show into dictionary for easy access and return k = None multival = [] - for line in out.split('\n'): # systemd can have multiline values delimited with {} + for line in to_native(out).split('\n'): # systemd can have multiline values delimited with {} if line.strip(): if k is None: if '=' in line: From 38992bbd5735d2dfc0ea2156d8a404cd3a177c21 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 16:52:46 +0200 Subject: [PATCH 027/770] Fix user module under python3 (#4560) Using something like: - name: Create ssh keys user: name: root generate_ssh_key: yes register: key result into this traceback on F24 Traceback (most recent call last): File \"/tmp/ansible_jm5d4vlh/ansible_module_user.py\", line 2170, in main() File \"/tmp/ansible_jm5d4vlh/ansible_module_user.py\", line 2108, in main (rc, out, err) = user.modify_user() File \"/tmp/ansible_jm5d4vlh/ansible_module_user.py\", line 660, in modify_user return self.modify_user_usermod() File \"/tmp/ansible_jm5d4vlh/ansible_module_user.py\", line 417, in modify_user_usermod has_append = self._check_usermod_append() File \"/tmp/ansible_jm5d4vlh/ansible_module_user.py\", line 405, in _check_usermod_append lines = helpout.split('\\n') TypeError: a bytes-like object is required, not 'str' --- system/user.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/user.py b/system/user.py index dbf3f3dc865..505bc3e48b8 100644 --- a/system/user.py +++ b/system/user.py @@ -221,6 +221,7 @@ import platform import socket import time +from ansible.module_utils._text import to_native try: import spwd @@ -402,7 +403,7 @@ def _check_usermod_append(self): helpout = data1 + data2 # check if --append exists - lines = helpout.split('\n') + lines = to_native(helpout).split('\n') for line in lines: if line.strip().startswith('-a, --append'): return True From d2135c70984c2aca3a2fbdc7850a147acd677b05 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 16:58:04 +0200 Subject: [PATCH 028/770] Adding RETURN string --- network/nxos/nxos_acl.py | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index 0049436bcdd..c6d0f630db6 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -203,6 +203,40 @@ provider: "{{ nxos_provider }}" ''' +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module. + returned: always + type: dict + sample: {"action": "permit", "dest": "any", "name": "ANSIBLE", + "proto": "tcp", "seq": "10", "src": "1.1.1.1/24"} +existing: + description: k/v pairs of existing ACL entries. + type: dict + sample: {} +end_state: + description: k/v pairs of ACL entries after module execution. + returned: always + type: dict + sample: {"action": "permit", "dest": "any", "name": "ANSIBLE", + "proto": "tcp", "seq": "10", "src": "1.1.1.1/24"} +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "present" +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ip access-list ANSIBLE", "10 permit tcp 1.1.1.1/24 any"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + # COMMON CODE FOR MIGRATION @@ -980,7 +1014,7 @@ def main(): else: execute_config_command(cmds, module) changed = True - existing_core, end_state, seqs = get_acl(module, name, seq) + new_existing_core, end_state, seqs = get_acl(module, name, seq) results['proposed'] = proposed results['existing'] = existing_core From c091a3e9acdb1de8ba83fb5c770dc29823ec2b3a Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 17:07:32 +0200 Subject: [PATCH 029/770] Fix mysql_user for python3 (#4576) dict no longer have a iteritems method, it was replaced by items. So we need to use six. Traceback (most recent call last): File \"/tmp/ansible_hjd7d65c/ansible_module_mysql_user.py\", line 587, in main() File \"/tmp/ansible_hjd7d65c/ansible_module_mysql_user.py\", line 571, in main changed = user_add(cursor, user, host, host_all, password, encrypted, priv, module.check_mode) File \"/tmp/ansible_hjd7d65c/ansible_module_mysql_user.py\", line 239, in user_add for db_table, priv in new_priv.iteritems(): AttributeError: 'dict' object has no attribute 'iteritems' --- database/mysql/mysql_user.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 961697b9dd9..ede172cedb2 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -166,6 +166,7 @@ mysqldb_found = False else: mysqldb_found = True +from ansible.module_utils.six import iteritems VALID_PRIVS = frozenset(('CREATE', 'DROP', 'GRANT', 'GRANT OPTION', 'LOCK TABLES', 'REFERENCES', 'EVENT', 'ALTER', @@ -234,9 +235,8 @@ def user_add(cursor, user, host, host_all, password, encrypted, new_priv, check_ cursor.execute("CREATE USER %s@%s IDENTIFIED BY %s", (user,host,password)) else: cursor.execute("CREATE USER %s@%s", (user,host)) - if new_priv is not None: - for db_table, priv in new_priv.iteritems(): + for db_table, priv in iteritems(new_priv): privileges_grant(cursor, user,host,db_table,priv) return True @@ -302,7 +302,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append # If the user has privileges on a db.table that doesn't appear at all in # the new specification, then revoke all privileges on it. - for db_table, priv in curr_priv.iteritems(): + for db_table, priv in iteritems(curr_priv): # If the user has the GRANT OPTION on a db.table, revoke it first. if "GRANT" in priv: grant_option = True @@ -315,7 +315,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append # If the user doesn't currently have any privileges on a db.table, then # we can perform a straight grant operation. - for db_table, priv in new_priv.iteritems(): + for db_table, priv in iteritems(new_priv): if db_table not in curr_priv: if module.check_mode: return True From 2e7cd6e02a12c43e3e6663f3db11f0356a2cfc09 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 17:08:12 +0200 Subject: [PATCH 030/770] Port postgresql module to python3 (#4579) Iteritems is no longer a dict method in Python3, replace it with the six wrapper. --- database/postgresql/postgresql_db.py | 3 ++- database/postgresql/postgresql_user.py | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/database/postgresql/postgresql_db.py b/database/postgresql/postgresql_db.py index bfb2f7e0951..33e812bc560 100644 --- a/database/postgresql/postgresql_db.py +++ b/database/postgresql/postgresql_db.py @@ -114,6 +114,7 @@ postgresqldb_found = False else: postgresqldb_found = True +from ansible.module_utils.six import iteritems class NotSupportedError(Exception): pass @@ -261,7 +262,7 @@ def main(): "login_password":"password", "port":"port" } - kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() + kw = dict( (params_map[k], v) for (k, v) in iteritems(module.params) if k in params_map and v != '' ) # If a login_unix_socket is specified, incorporate it here. diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index caa1dc2dbb2..7a3bb559936 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -170,6 +170,7 @@ postgresqldb_found = False else: postgresqldb_found = True +from ansible.module_utils.six import iteritems _flags = ('SUPERUSER', 'CREATEROLE', 'CREATEUSER', 'CREATEDB', 'INHERIT', 'LOGIN', 'REPLICATION') VALID_FLAGS = frozenset(itertools.chain(_flags, ('NO%s' % f for f in _flags))) @@ -433,7 +434,7 @@ def revoke_privileges(cursor, user, privs): changed = False for type_ in privs: - for name, privileges in privs[type_].iteritems(): + for name, privileges in iteritems(privs[type_]): # Check that any of the privileges requested to be removed are # currently granted to the user differences = check_funcs[type_](cursor, user, name, privileges) @@ -451,7 +452,7 @@ def grant_privileges(cursor, user, privs): changed = False for type_ in privs: - for name, privileges in privs[type_].iteritems(): + for name, privileges in iteritems(privs[type_]): # Check that any of the privileges requested for the user are # currently missing differences = check_funcs[type_](cursor, user, name, privileges) @@ -595,7 +596,7 @@ def main(): "port":"port", "db":"database" } - kw = dict( (params_map[k], v) for (k, v) in module.params.iteritems() + kw = dict( (params_map[k], v) for (k, v) in iteritems(module.params) if k in params_map and v != "" ) # If a login_unix_socket is specified, incorporate it here. From 1acb23f8d64ce95ee8133fec26598d3b7345b756 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 17:16:01 +0200 Subject: [PATCH 031/770] Fix uri to run on python3 (#4580) Since dict no longer have a method iteritems, we have to use the six wrapper. --- network/basics/uri.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/basics/uri.py b/network/basics/uri.py index 844761c38bd..e42bb068e64 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -391,7 +391,7 @@ def main(): # Grab all the http headers. Need this hack since passing multi-values is # currently a bit ugly. (e.g. headers='{"Content-Type":"application/json"}') - for key, value in module.params.iteritems(): + for key, value in six.iteritems(module.params): if key.startswith("HEADER_"): skey = key.replace("HEADER_", "") dict_headers[skey] = value @@ -435,7 +435,7 @@ def main(): # Transmogrify the headers, replacing '-' with '_', since variables dont # work with dashes. uresp = {} - for key, value in resp.iteritems(): + for key, value in six.iteritems(resp): ukey = key.replace("-", "_") uresp[ukey] = value From cee7473df6cf1d1f6fd675dc9c10bfb7d48694a4 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 17:16:35 +0200 Subject: [PATCH 032/770] Port mount.py to python3, need to use six.iteritems (#4581) --- system/mount.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/mount.py b/system/mount.py index 0be9ee2416e..bd62a148bb0 100644 --- a/system/mount.py +++ b/system/mount.py @@ -88,6 +88,7 @@ - mount: name=/home src='UUID=b3e48f45-f933-4c8e-a700-22a159ec9077' fstype=xfs opts=noatime state=present ''' +from ansible.module_utils.six import iteritems def write_fstab(lines, dest): @@ -122,7 +123,7 @@ def set_mount(module, **kwargs): to_write = [] exists = False changed = False - escaped_args = dict([(k, _escape_fstab(v)) for k, v in args.iteritems()]) + escaped_args = dict([(k, _escape_fstab(v)) for k, v in iteritems(args)]) for line in open(args['fstab'], 'r').readlines(): if not line.strip(): to_write.append(line) From 6dd2bc2bff83c9417e0a13b454cf196feba03bee Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 31 Aug 2016 17:18:54 +0200 Subject: [PATCH 033/770] Make async work on python 3 (#4583) Since dict no longer have a iteritems method, we have to use six to support python 2 and 3. --- utilities/logic/async_status.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utilities/logic/async_status.py b/utilities/logic/async_status.py index 0d434d46f87..2e11a301a42 100644 --- a/utilities/logic/async_status.py +++ b/utilities/logic/async_status.py @@ -50,6 +50,7 @@ import datetime import traceback +from ansible.module_utils.six import iteritems def main(): @@ -95,7 +96,7 @@ def main(): data['finished'] = 0 # Fix error: TypeError: exit_json() keywords must be strings - data = dict([(str(k), v) for k, v in data.iteritems()]) + data = dict([(str(k), v) for k, v in iteritems(data)]) module.exit_json(**data) From 224a47b7189d82f50665553df1a7cb9d503a603b Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 17:28:15 +0200 Subject: [PATCH 034/770] Adding nxos_acl_interface_module --- network/nxos/nxos_acl_interface.py | 765 +++++++++++++++++++++++++++++ 1 file changed, 765 insertions(+) create mode 100644 network/nxos/nxos_acl_interface.py diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py new file mode 100644 index 00000000000..3d7cfa45d30 --- /dev/null +++ b/network/nxos/nxos_acl_interface.py @@ -0,0 +1,765 @@ +#!/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_acl_interface +version_added: "2.2" +short_description: Manages applying ACLs to interfaces. +description + - Manages applying ACLs to interfaces. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +options: + name: + description: + - Case sensitive name of the access list (ACL). + required: true + interface: + description: + - Full name of interface. MUST be the full name. + required: true + direction: + description: + - Direction ACL to be applied in on the interface. + required: true + choices: ['ingress', 'egress'] + state: + description: + - Specify desired state of the resource. + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- name: apply egress acl to ethernet1/41 + nxos_acl_interface: + name: ANSIBLE + interface: ethernet1/41 + direction: egress + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"direction": "egress", "interface": "ethernet1/41", + "name": "ANSIBLE"} +existing: + description: k/v pairs of existing ACL applied to the interface + type: dict + sample: {} +end_state: + description: k/v pairs of interface ACL after module execution + returned: always + type: dict + sample: {"direction": "egress", "interface": "ethernet1/41", + "name": "ANSIBLE"} +acl_applied_to: + description: list of interfaces the ACL is applied to + returned: always + type: list + sample: [{"acl_type": "Router ACL", "direction": "egress", + "interface": "Ethernet1/41", "name": "ANSIBLE"}] +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "present" +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface ethernet1/41", "ip access-group ANSIBLE out"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import itertools + +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + +def get_config(module): + config = module.params['running_config'] + if not config: + config = module.get_config() + 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_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +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. Instead, we assume if '^' is found in response, + it is an invalid command. + """ + if 'xml' in response[0]: + body = [] + elif '^' in response[0] or 'summary' in command: + 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): + 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)) + return response + + +def execute_show_command(command, module, command_type='cli_show'): + if module.params['transport'] == 'cli': + if 'summary' not in command: + 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 get_acl_interface(module, acl): + command = 'show ip access-list summary' + name_regex = '.*IPV4\s+ACL\s+(?P\S+).*' + interface_regex = ('.*\s+(?P\w+(\d+)?\/?(\d+)?)\s-\s' + '(?P\w+)\s+\W(?P\w+\s\w+)\W.*') + acl_list = [] + + body = execute_show_command(command, module, command_type='cli_show_ascii') + body_split = body[0].split('Active on interfaces:') + + for each_acl in body_split: + intf_list = [] + temp = {} + try: + match_name = re.match(name_regex, each_acl, re.DOTALL) + name_dict = match_name.groupdict() + name = name_dict['name'] + except AttributeError: + name = '' + + temp['interfaces'] = [] + for line in each_acl.split('\n'): + intf_temp = {} + try: + match_interface = re.match(interface_regex, line, re.DOTALL) + interface_dict = match_interface.groupdict() + interface = interface_dict['interface'] + direction = interface_dict['direction'] + acl_type = interface_dict['acl_type'] + except AttributeError: + interface = '' + direction = '' + acl_type = '' + + if interface: + intf_temp['interface'] = interface + if acl_type: + intf_temp['acl_type'] = acl_type + if direction: + intf_temp['direction'] = direction + if intf_temp: + temp['interfaces'].append(intf_temp) + if name: + temp['name'] = name + + if temp: + acl_list.append(temp) + + existing_no_null = [] + for each in acl_list: + if each.get('name') == acl: + interfaces = each.get('interfaces') + for interface in interfaces: + new_temp = {} + new_temp['name'] = acl + new_temp.update(interface) + existing_no_null.append(new_temp) + return existing_no_null + + +def other_existing_acl(get_existing, interface, direction): + # now we'll just get the interface in question + # needs to be a list since same acl could be applied in both dirs + acls_interface = [] + if get_existing: + for each in get_existing: + if each.get('interface').lower() == interface: + acls_interface.append(each) + else: + acls_interface = [] + + if acls_interface: + this = {} + for each in acls_interface: + if each.get('direction') == direction: + this = each + else: + acls_interface = [] + this = {} + + return acls_interface, this + + +def apply_acl(proposed): + commands = [] + + commands.append('interface ' + proposed.get('interface')) + direction = proposed.get('direction') + if direction == 'egress': + cmd = 'ip access-group {0} {1}'.format(proposed.get('name'), 'out') + elif direction == 'ingress': + cmd = 'ip access-group {0} {1}'.format(proposed.get('name'), 'in') + commands.append(cmd) + + return commands + + +def remove_acl(proposed): + commands = [] + + commands.append('interface ' + proposed.get('interface')) + direction = proposed.get('direction') + if direction == 'egress': + cmd = 'no ip access-group {0} {1}'.format(proposed.get('name'), 'out') + elif direction == 'ingress': + cmd = 'no ip access-group {0} {1}'.format(proposed.get('name'), 'in') + commands.append(cmd) + + return commands + + +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 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) + + +def main(): + argument_spec = dict( + name=dict(required=False, type='str'), + interface=dict(required=True), + direction=dict(required=True, choices=['egress', 'ingress']), + state=dict(choices=['absent', 'present'], + default='present'), + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + name = module.params['name'] + interface = module.params['interface'].lower() + direction = module.params['direction'].lower() + + proposed = dict(name=name, interface=interface, direction=direction) + + # includes all interfaces the ACL is applied to (list) + get_existing = get_acl_interface(module, name) + + # interface_acls = includes entries of this ACL on the interface (list) + # this_dir_acl_intf = dict - not null if it already exists + interfaces_acls, existing = other_existing_acl( + get_existing, interface, direction) + + end_state = existing + end_state_acls = get_existing + changed = False + + cmds = [] + commands = [] + if state == 'present': + if not existing: + command = apply_acl(proposed) + if command: + commands.append(command) + + elif state == 'absent': + if existing: + command = remove_acl(proposed) + if command: + commands.append(command) + + if commands: + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + execute_config_command(cmds, module) + changed = True + end_state_acls = get_acl_interface(module, name) + interfaces_acls, this_dir_acl_intf = other_existing_acl( + end_state_acls, interface, direction) + end_state = this_dir_acl_intf + else: + cmds = [] + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['state'] = state + results['updates'] = cmds + results['changed'] = changed + results['end_state'] = end_state + results['acl_applied_to'] = end_state_acls + + module.exit_json(**results) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.nxos import * +if __name__ == '__main__': + main() From 958d894c6119cee6a976c5663156ee789bfcf239 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 31 Aug 2016 08:31:23 -0700 Subject: [PATCH 035/770] We've decided that pythn 3.5 is the minimum python3 version (#4572) --- .travis.yml | 1 - packaging/language/pip.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index ea3133a6097..abc512d43b2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -22,7 +22,6 @@ script: - python2.4 -m compileall -fq cloud/amazon/_ec2_ami_search.py cloud/amazon/ec2_facts.py - python2.6 -m compileall -fq . - python2.7 -m compileall -fq . - - python3.4 -m compileall -fq . - python3.5 -m compileall -fq . - ansible-validate-modules --exclude 'utilities/' . #- ansible-validate-modules --exclude 'cloud/amazon/ec2_lc\.py|cloud/amazon/ec2_scaling_policy\.py|cloud/amazon/ec2_scaling_policy\.py|cloud/amazon/ec2_asg\.py|cloud/azure/azure\.py|packaging/os/rhn_register\.py|network/openswitch/ops_template\.py|system/hostname\.py|utilities/' . diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 466c7ea8875..d3bda11d38e 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -83,7 +83,7 @@ version_added: "2.0" description: - The Python executable used for creating the virtual environment. - For example C(python3.4), C(python2.7). When not specified, the + For example C(python3.5), C(python2.7). When not specified, the system Python version is used. required: false default: null From 7af4081401f7f58faef3865a0b1588ead01a1855 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 17:35:56 +0200 Subject: [PATCH 036/770] Fixing typo --- network/nxos/nxos_acl_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 3d7cfa45d30..ad6a0cd30e1 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -21,7 +21,7 @@ module: nxos_acl_interface version_added: "2.2" short_description: Manages applying ACLs to interfaces. -description +description: - Manages applying ACLs to interfaces. extends_documentation_fragment: nxos author: From 84d957beaa1cda1d6bf622a17f8e3bee2fdd4ed6 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 17:54:06 +0200 Subject: [PATCH 037/770] Adding nxos_evpn_global module --- network/nxos/nxos_evpn_global.py | 579 +++++++++++++++++++++++++++++++ 1 file changed, 579 insertions(+) create mode 100644 network/nxos/nxos_evpn_global.py diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py new file mode 100644 index 00000000000..1f49ed8bd63 --- /dev/null +++ b/network/nxos/nxos_evpn_global.py @@ -0,0 +1,579 @@ +#!/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_evpn_global +version_added: "2.2" +short_description: Handles the EVPN control plane for VXLAN. +description: + - Handles the EVPN control plane for VXLAN. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +options: + nv_overlay_evpn: + description: + - EVPN control plane + required: true + choices: ['true', 'false'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_evpn_global: + nv_overlay_evpn=true +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: when I(m_facts)=true + type: dict + sample: {"nv_overlay_evpn": true} +existing: + description: k/v pairs of existing configuration + returned: when I(m_facts)=true + type: dict + sample: {"nv_overlay_evpn": false} +end_state: + description: k/v pairs of configuration after module execution + returned: when I(m_facts)=true + type: dict + sample: {"nv_overlay_evpn": true} +updates: + description: commands sent to the device + returned: when I(m_facts)=true + type: list + sample: ["nv overlay evpn"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import itertools + +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + +def get_config(module): + config = module.params['running_config'] + if not config: + config = module.get_config() + 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_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +PARAM_TO_COMMAND_KEYMAP = { + 'nv_overlay_evpn': 'nv overlay evpn', +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + if REGEX.search(config): + value = True + return value + + +def get_existing(module): + existing = {} + config = str(get_config(module)) + + existing['nv_overlay_evpn'] = get_value('nv_overlay_evpn', config, module) + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def get_commands(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + elif value is False: + commands.append('no {0}'.format(key)) + + if commands: + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + nv_overlay_evpn=dict(required=True, type='bool'), + m_facts=dict(required=False, default=False, type='bool'), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + existing = invoke('get_existing', module) + end_state = existing + proposed = dict(nv_overlay_evpn=module.params['nv_overlay_evpn']) + + result = {} + candidate = CustomNetworkConfig(indent=3) + invoke('get_commands', module, existing, proposed, candidate) + + if proposed != existing: + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.nxos import * +if __name__ == '__main__': + main() From 69000b82b58f6d89475e52207b5399c20557da97 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 18:12:26 +0200 Subject: [PATCH 038/770] Adding nxos_ospf module --- network/nxos/nxos_ospf.py | 608 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 608 insertions(+) create mode 100644 network/nxos/nxos_ospf.py diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py new file mode 100644 index 00000000000..18a47b3f498 --- /dev/null +++ b/network/nxos/nxos_ospf.py @@ -0,0 +1,608 @@ +#!/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_ospf +version_added: "2.2" +short_description: Manages configuration of an ospf instance. +description: + - Manages configuration of an ospf instance. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +options: + ospf: + description: + - Name of the ospf instance. + required: true + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' + +EXAMPLES = ''' +- nxos_ospf: + ospf=ntc + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: when I(m_facts)=true + type: dict + sample: {"ospf": "1"} +existing: + description: k/v pairs of existing configuration + returned: when I(m_facts)=true + type: dict + sample: {"ospf": ["2"]} +end_state: + description: k/v pairs of configuration after module execution + returned: when I(m_facts)=true + type: dict + sample: {"ospf": ["1", "2"]} +updates: + description: commands sent to the device + returned: when I(m_facts)=true + type: list + sample: ["router ospf 1"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import itertools + +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + +def get_config(module): + config = module.params['running_config'] + if not config: + config = module.get_config() + 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_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +PARAM_TO_COMMAND_KEYMAP = { + 'ospf': 'router ospf' +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(config, module): + splitted_config = config.splitlines() + value_list = [] + REGEX = '^router ospf\s(?P\S+).*' + for line in splitted_config: + value = '' + if 'router ospf' in line: + try: + match_ospf = re.match(REGEX, line, re.DOTALL) + ospf_group = match_ospf.groupdict() + value = ospf_group['ospf'] + except AttributeError: + value = '' + if value: + value_list.append(value) + + return value_list + + +def get_existing(module): + existing = {} + config = str(get_config(module)) + + value = get_value(config, module) + if value: + existing['ospf'] = value + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, proposed, candidate): + commands = ['router ospf {0}'.format(proposed['ospf'])] + candidate.add(commands, parents=[]) + + +def state_absent(module, proposed, candidate): + commands = ['no router ospf {0}'.format(proposed['ospf'])] + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + ospf=dict(required=True, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + ospf = str(module.params['ospf']) + + existing = invoke('get_existing', module) + end_state = existing + proposed = dict(ospf=ospf) + + if not existing: + existing_list = [] + else: + existing_list = existing['ospf'] + + result = {} + if (state == 'present' or (state == 'absent' and ospf in existing_list)): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed + + module.exit_json(**result) + + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.nxos import * +if __name__ == '__main__': + main() From e1e0e2b045cdfb48581372cd9a6eb2b63d87cb5a Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 18:38:45 +0200 Subject: [PATCH 039/770] Adding nxos_ospf_vrf module --- network/nxos/nxos_ospf_vrf.py | 855 ++++++++++++++++++++++++++++++++++ 1 file changed, 855 insertions(+) create mode 100644 network/nxos/nxos_ospf_vrf.py diff --git a/network/nxos/nxos_ospf_vrf.py b/network/nxos/nxos_ospf_vrf.py new file mode 100644 index 00000000000..2d97aed3a44 --- /dev/null +++ b/network/nxos/nxos_ospf_vrf.py @@ -0,0 +1,855 @@ +#!/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_ospf_vrf +version_added: "2.2" +short_description: Manages a VRF for an OSPF router. +description: + - Manages a VRF for an OSPF router. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - 'default' restores params default value, if any. Otherwise it removes + the existing param configuration. +options: + vrf: + description: + - Name of the resource instance. Valid value is a string. + The name 'default' is a valid VRF representing the global OSPF. + required: false + default: default + ospf: + description: + - Name of the OSPF instance. + required: true + default: null + router_id: + description: + - Router Identifier (ID) of the OSPF router VRF instance. + required: false + default: null + default_metric: + description: + - Specify the default Metric value. Valid values are an integer + or the keyword 'default'. + required: false + default: null + log_adjacency: + description: + - Controls the level of log messages generated whenever a + neighbor changes state. Valid values are 'log', 'detail', + and 'default'. + required: false + choices: ['log','detail','default'] + default: null + timer_throttle_lsa_start: + description: + - Specify the start interval for rate-limiting Link-State + Advertisement (LSA) generation. Valid values are an integer, + in milliseconds, or the keyword 'default'. + required: false + default: null + timer_throttle_lsa_hold: + description: + - Specify the hold interval for rate-limiting Link-State + Advertisement (LSA) generation. Valid values are an integer, + in milliseconds, or the keyword 'default'. + required: false + default: null + timer_throttle_lsa_max: + description: + - Specify the max interval for rate-limiting Link-State + Advertisement (LSA) generation. Valid values are an integer, + in milliseconds, or the keyword 'default'. + required: false + default: null + timer_throttle_spf_start: + description: + - Specify initial Shortest Path First (SPF) schedule delay. + Valid values are an integer, in milliseconds, or + the keyword 'default'. + required: false + default: null + timer_throttle_spf_hold: + description: + - Specify minimum hold time between Shortest Path First (SPF) + calculations. Valid values are an integer, in milliseconds, + or the keyword 'default'. + required: false + default: null + timer_throttle_spf_max: + description: + - Specify the maximum wait time between Shortest Path First (SPF) + calculations. Valid values are an integer, in milliseconds, + or the keyword 'default'. + required: false + default: null + auto_cost: + description: + - Specifies the reference bandwidth used to assign OSPF cost. + Valid values are an integer, in Mbps, or the keyword 'default'. + required: false + default: null + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' + +EXAMPLES = ''' +- nxos_ospf_vrf: + ospf: 1 + timer_throttle_spf_start: 50 + timer_throttle_spf_hold: 1000 + timer_throttle_spf_max: 2000 + timer_throttle_lsa_start: 60 + timer_throttle_lsa_hold: 1100 + timer_throttle_lsa_max: 3000 + vrf: test + m_facts: true + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: when I(m_facts)=true + type: dict + sample: {"ospf": "1", "timer_throttle_lsa_hold": "1100", + "timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60", + "timer_throttle_spf_hold": "1000", + "timer_throttle_spf_max": "2000", "timer_throttle_spf_start": "50", + "vrf": "test"} +existing: + description: k/v pairs of existing configuration + returned: when I(m_facts)=true + type: dict + sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "", + "ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "5000", + "timer_throttle_lsa_max": "5000", "timer_throttle_lsa_start": "0", + "timer_throttle_spf_hold": "1000", + "timer_throttle_spf_max": "5000", + "timer_throttle_spf_start": "200", "vrf": "test"} +end_state: + description: k/v pairs of configuration after module execution + returned: when I(m_facts)=true + type: dict + sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "", + "ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "1100", + "timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60", + "timer_throttle_spf_hold": "1000", + "timer_throttle_spf_max": "2000", "timer_throttle_spf_start": "50", + "vrf": "test"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router ospf 1", "vrf test", "timers throttle lsa 60 1100 3000", + "timers throttle spf 50 1000 2000"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import itertools + +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + +def get_config(module): + config = module.params['running_config'] + if not config: + config = module.get_config() + 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_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +PARAM_TO_COMMAND_KEYMAP = { + 'router_id': 'router-id', + 'default_metric': 'default-metric', + 'log_adjacency': 'log-adjacency-changes', + 'timer_throttle_lsa_start': 'timers throttle lsa', + 'timer_throttle_lsa_max': 'timers throttle lsa', + 'timer_throttle_lsa_hold': 'timers throttle lsa', + 'timer_throttle_spf_max': 'timers throttle spf', + 'timer_throttle_spf_start': 'timers throttle spf', + 'timer_throttle_spf_hold': 'timers throttle spf', + 'auto_cost': 'auto-cost reference-bandwidth' +} +PARAM_TO_DEFAULT_KEYMAP = { + 'timer_throttle_lsa_start': '0', + 'timer_throttle_lsa_max': '5000', + 'timer_throttle_lsa_hold': '5000', + 'timer_throttle_spf_start': '200', + 'timer_throttle_spf_max': '5000', + 'timer_throttle_spf_hold': '1000', + 'auto_cost': '40000' +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + if arg == 'log_adjacency': + if 'log-adjacency-changes detail' in config: + value = 'detail' + else: + value = 'log' + else: + value_list = REGEX.search(config).group('value').split() + if 'hold' in arg: + value = value_list[1] + elif 'max' in arg: + value = value_list[2] + elif 'auto' in arg: + if 'Gbps' in value_list: + value = str(int(value_list[0]) * 1000) + else: + value = value_list[0] + else: + value = value_list[0] + return value + + +def get_existing(module, args): + existing = {} + netcfg = get_config(module) + parents = ['router ospf {0}'.format(module.params['ospf'])] + + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + config = netcfg.get_section(parents) + if config: + if module.params['vrf'] == 'default': + splitted_config = config.splitlines() + vrf_index = False + for index in range(0, len(splitted_config) - 1): + if 'vrf' in splitted_config[index].strip(): + vrf_index = index + break + if vrf_index: + config = '\n'.join(splitted_config[0:vrf_index]) + + for arg in args: + if arg not in ['ospf', 'vrf']: + existing[arg] = get_value(arg, config, module) + + existing['vrf'] = module.params['vrf'] + existing['ospf'] = module.params['ospf'] + + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + + elif value is False: + commands.append('no {0}'.format(key)) + + elif value == 'default': + if existing_commands.get(key): + existing_value = existing_commands.get(key) + commands.append('no {0} {1}'.format(key, existing_value)) + else: + if key == 'timers throttle lsa': + command = '{0} {1} {2} {3}'.format( + key, + proposed['timer_throttle_lsa_start'], + proposed['timer_throttle_lsa_hold'], + proposed['timer_throttle_lsa_max']) + elif key == 'timers throttle spf': + command = '{0} {1} {2} {3}'.format( + key, + proposed['timer_throttle_spf_start'], + proposed['timer_throttle_spf_hold'], + proposed['timer_throttle_spf_max']) + elif key == 'log-adjacency-changes': + if value == 'log': + command = key + elif value == 'detail': + command = '{0} {1}'.format(key, value) + elif key == 'auto-cost reference-bandwidth': + if len(value) < 5: + command = '{0} {1} Mbps'.format(key, value) + else: + value = str(int(value) / 1000) + command = '{0} {1} Gbps'.format(key, value) + else: + command = '{0} {1}'.format(key, value.lower()) + + if command not in commands: + commands.append(command) + + if commands: + parents = ['router ospf {0}'.format(module.params['ospf'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ['router ospf {0}'.format(module.params['ospf'])] + if module.params['vrf'] == 'default': + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in existing_commands.iteritems(): + if value: + if key == 'timers throttle lsa': + command = 'no {0} {1} {2} {3}'.format( + key, + existing['timer_throttle_lsa_start'], + existing['timer_throttle_lsa_hold'], + existing['timer_throttle_lsa_max']) + elif key == 'timers throttle spf': + command = 'no {0} {1} {2} {3}'.format( + key, + existing['timer_throttle_spf_start'], + existing['timer_throttle_spf_hold'], + existing['timer_throttle_spf_max']) + else: + existing_value = existing_commands.get(key) + command = 'no {0} {1}'.format(key, existing_value) + + if command not in commands: + commands.append(command) + else: + commands = ['no vrf {0}'.format(module.params['vrf'])] + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + vrf=dict(required=False, type='str', default='default'), + ospf=dict(required=True, type='str'), + router_id=dict(required=False, type='str'), + default_metric=dict(required=False, type='str'), + log_adjacency=dict(required=False, type='str', + choices=['log', 'detail', 'default']), + timer_throttle_lsa_start=dict(required=False, type='str'), + timer_throttle_lsa_hold=dict(required=False, type='str'), + timer_throttle_lsa_max=dict(required=False, type='str'), + timer_throttle_spf_start=dict(required=False, type='str'), + timer_throttle_spf_hold=dict(required=False, type='str'), + timer_throttle_spf_max=dict(required=False, type='str'), + auto_cost=dict(required=False, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + args = [ + 'vrf', + 'ospf', + 'router_id', + 'default_metric', + 'log_adjacency', + 'timer_throttle_lsa_start', + 'timer_throttle_lsa_hold', + 'timer_throttle_lsa_max', + 'timer_throttle_spf_start', + 'timer_throttle_spf_hold', + 'timer_throttle_spf_max', + 'auto_cost' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'interface': + if str(value).lower() == 'true': + value = True + elif str(value).lower() == 'false': + value = False + elif str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + module.exit_json(**result) + + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +from ansible.module_utils.shell import * +from ansible.module_utils.netcfg import * +from ansible.module_utils.nxos import * + +if __name__ == '__main__': + main() From 84de0c5c570d891106159d36dd43a3b2d8a175d7 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 18:43:05 +0200 Subject: [PATCH 040/770] Fixing RETURN string --- network/nxos/nxos_ospf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index 18a47b3f498..04572b10c60 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -71,7 +71,7 @@ sample: {"ospf": ["1", "2"]} updates: description: commands sent to the device - returned: when I(m_facts)=true + returned: always type: list sample: ["router ospf 1"] changed: From 1358e0fd91f0dedf34db53f5756576df03620935 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 18:44:04 +0200 Subject: [PATCH 041/770] Fixing RETURN string --- network/nxos/nxos_evpn_global.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index 1f49ed8bd63..4ddeb785806 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -61,7 +61,7 @@ sample: {"nv_overlay_evpn": true} updates: description: commands sent to the device - returned: when I(m_facts)=true + returned: always type: list sample: ["nv overlay evpn"] changed: From 7f02615fd94d9bed6a8d97596407aa4424839e7f Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 31 Aug 2016 18:46:46 +0200 Subject: [PATCH 042/770] Fixing DOC string --- network/nxos/nxos_ospf_vrf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_ospf_vrf.py b/network/nxos/nxos_ospf_vrf.py index 2d97aed3a44..6878d4f2849 100644 --- a/network/nxos/nxos_ospf_vrf.py +++ b/network/nxos/nxos_ospf_vrf.py @@ -26,8 +26,8 @@ author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - 'default' restores params default value, if any. Otherwise it removes - the existing param configuration. + - Value I(default) restores params default value, if any. + Otherwise it removes the existing param configuration. options: vrf: description: From b99cad412858951c557a8235e5ae4b2ba847669f Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 17:12:52 -0400 Subject: [PATCH 043/770] added new functionality to ops_command * commands argument now accepts a dict arguments * waitfor has been renamed to wait_for with an alias to waitfor * only show commands are allowed when check mode is specified * config mode is no longer allowed in the command stack * add argument match with valid values any, all Tested on OpenSwitch 0.4.0 --- network/openswitch/ops_command.py | 148 ++++++++++++++++++++---------- 1 file changed, 102 insertions(+), 46 deletions(-) diff --git a/network/openswitch/ops_command.py b/network/openswitch/ops_command.py index 27a668ff203..f74ef6191fb 100644 --- a/network/openswitch/ops_command.py +++ b/network/openswitch/ops_command.py @@ -37,15 +37,29 @@ module is not returned until the condition is satisfied or the number of retires as expired. required: true - waitfor: + wait_for: description: - List of conditions to evaluate against the output of the - command. The task will wait for a each condition to be true + command. The task will wait for each condition to be true before moving forward. If the conditional is not true - within the configured number of I(retries), the task fails. + within the configured number of retries, the task fails. See examples. required: false default: null + aliases: ['waitfor'] + version_added: "2.2" + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all) + then all conditionals in the I(wait_for) must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + required: false + default: all + choices: ['any', 'all'] + version_added: "2.2" retries: description: - Specifies the number of retries a command should by tried @@ -65,21 +79,32 @@ """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: netop + password: netop + transport: cli + - ops_command: commands: - show version - register: output + provider: "{{ cli }}" - ops_command: commands: - show version - waitfor: + wait_for: - "result[0] contains OpenSwitch" + provider: "{{ cli }}" - ops_command: - commands: - - show version - - show interfaces + commands: + - show version + - show interfaces + provider: "{{ cli }}" """ RETURN = """ @@ -101,9 +126,12 @@ type: list sample: ['...', '...'] """ +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.netcli import AddCommandError, FailedConditionsError +from ansible.module_utils.openswitch import NetworkModule, NetworkError -import time - +VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: @@ -111,57 +139,85 @@ def to_lines(stdout): item = str(item).split('\n') yield item +def parse_commands(module): + for cmd in module.params['commands']: + if isinstance(cmd, basestring): + cmd = dict(command=cmd, output=None) + elif 'command' not in cmd: + module.fail_json(msg='command keyword argument is required') + elif not set(cmd.keys()).issubset(VALID_KEYS): + module.fail_json(msg='unknown keyword specified') + yield cmd def main(): spec = dict( - commands=dict(type='list'), - waitfor=dict(type='list'), + # { command: , prompt: , response: } + commands=dict(type='list', required=True), + + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + retries=dict(default=10, type='int'), - interval=dict(default=1, type='int'), - transport=dict(default='cli', choices=['cli']) + interval=dict(default=1, type='int') ) - module = get_module(argument_spec=spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=spec, + connect_on_load=False, + supports_check_mode=True) - commands = module.params['commands'] + commands = list(parse_commands(module)) + conditionals = module.params['wait_for'] or list() - retries = module.params['retries'] - interval = module.params['interval'] + warnings = list() - try: - queue = set() - for entry in (module.params['waitfor'] or list()): - queue.add(Conditional(entry)) - except AttributeError: - exc = get_exception() - module.fail_json(msg=exc.message) + runner = CommandRunner(module) - result = dict(changed=False) + for cmd in commands: + if module.check_mode and not cmd['command'].startswith('show'): + warnings.append('only show commands are supported when using ' + 'check mode, not executing `%s`' % cmd['command']) + else: + if cmd['command'].startswith('conf'): + module.fail_json(msg='ops_command does not support running ' + 'config mode commands. Please use ' + 'ops_config instead') + try: + runner.add_command(**cmd) + except AddCommandError: + exc = get_exception() + warnings.append('duplicate command detected: %s' % cmd) - while retries > 0: - response = module.execute(commands) - result['stdout'] = response + for item in conditionals: + runner.add_conditional(item) - for item in list(queue): - if item(response): - queue.remove(item) + runner.retries = module.params['retries'] + runner.interval = module.params['interval'] + runner.match = module.params['match'] - if not queue: - break + try: + runner.run() + except FailedConditionsError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) - time.sleep(interval) - retries -= 1 - else: - failed_conditions = [item.raw for item in queue] - module.fail_json(msg='timeout waiting for value', failed_conditions=failed_conditions) + result = dict(changed=False, stdout=list()) + for cmd in commands: + try: + output = runner.get_command(cmd['command']) + except ValueError: + output = 'command not executed due to check_mode, see warnings' + result['stdout'].append(output) + + result['warnings'] = warnings result['stdout_lines'] = list(to_lines(result['stdout'])) - return module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.openswitch import * + module.exit_json(**result) + + if __name__ == '__main__': - main() + main() + From 9c64d1947cea3d6d67cdb6ab0e363e85e4097ca5 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 30 Aug 2016 20:28:58 -0400 Subject: [PATCH 044/770] update ops_template module using refactored network shared modules This updates the ops_template module to work with the network shared modules introduced in Ansible 2.2 Tested with OpenSwitch 0.4.0 --- network/openswitch/ops_template.py | 87 ++++++++++-------------------- 1 file changed, 27 insertions(+), 60 deletions(-) diff --git a/network/openswitch/ops_template.py b/network/openswitch/ops_template.py index 644e103e2ee..48f21c0ce9c 100644 --- a/network/openswitch/ops_template.py +++ b/network/openswitch/ops_template.py @@ -97,40 +97,14 @@ """ import copy - -def compare(this, other): - parents = [item.text for item in this.parents] - for entry in other: - if this == entry: - return None - return this - - -def expand(obj, queue): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = queue - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - -def flatten(data, obj): - for k, v in data.items(): - obj.append(k) - flatten(v, obj) - return obj +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.openswitch import NetworkModule def get_config(module): config = module.params['config'] or dict() if not config and not module.params['force']: - config = module.config + config = module.config.get_config() return config @@ -170,7 +144,6 @@ def merge(changeset, config=None): current_level[key] = value return config - def main(): """ main entry point for module execution """ @@ -184,18 +157,25 @@ def main(): mutually_exclusive = [('config', 'backup'), ('config', 'force')] - module = get_module(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + if not module.params['transport'] and not HAS_OPS: + module.fail_json(msg='unable to import ops.dc library') result = dict(changed=False) contents = get_config(module) - result['_backup'] = copy.deepcopy(module.config) + result['_backup'] = contents if module.params['transport'] in ['ssh', 'rest']: config = contents - src = module.from_json(module.params['src']) + + try: + src = module.from_json(module.params['src']) + except ValueError: + module.fail_json(msg='unable to load src due to json parsing error') changeset = diff(src, config) candidate = merge(changeset, config) @@ -208,45 +188,32 @@ def main(): if changeset: if not module.check_mode: - module.configure(config) + module.config(config) result['changed'] = True else: - config = module.parse_config(config) - candidate = module.parse_config(module.params['src']) + candidate = NetworkConfig(contents=module.params['src'], indent=4) - commands = collections.OrderedDict() - toplevel = [c.text for c in config] + if contents: + config = NetworkConfig(contents=contents, indent=4) - for line in candidate: - if line.text in ['!', '']: - continue - - if not line.parents: - if line.text not in toplevel: - expand(line, commands) - else: - item = compare(line, config) - if item: - expand(item, commands) - - commands = flatten(commands, list()) + if not module.params['force']: + commands = candidate.difference(config) + commands = dumps(commands, 'commands').split('\n') + commands = [str(c) for c in commands if c] + else: + commands = str(candidate).split('\n') if commands: if not module.check_mode: - commands = [str(c).strip() for c in commands] - response = module.configure(commands) + response = module.config(commands) result['responses'] = response result['changed'] = True + result['updates'] = commands module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.shell import * -from ansible.module_utils.openswitch import * if __name__ == '__main__': main() From e7cf786851b1bcc23ec0f4339e5b868d01a6602d Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Wed, 31 Aug 2016 14:12:29 -0500 Subject: [PATCH 045/770] Adding 'end_play' docs to meta module --- utilities/helper/meta.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/utilities/helper/meta.py b/utilities/helper/meta.py index 6735bbc8e98..6e039123c38 100644 --- a/utilities/helper/meta.py +++ b/utilities/helper/meta.py @@ -35,7 +35,8 @@ - "C(noop) (added in 2.0) This literally does 'nothing'. It is mainly used internally and not recommended for general use." - "C(clear_facts) (added in 2.1) causes the gathered facts for the hosts specified in the play's list of hosts to be cleared, including the fact cache." - "C(clear_host_errors) (added in 2.1) clears the failed state (if any) from hosts specified in the play's list of hosts." - choices: ['noop', 'flush_handlers', 'refresh_inventory', 'clear_facts', 'clear_host_errors'] + - "C(end_play) (added in 2.2) causes the play to end without failing the host." + choices: ['noop', 'flush_handlers', 'refresh_inventory', 'clear_facts', 'clear_host_errors', 'end_play'] required: true default: null notes: From 86366eca683461a14f0709ca5c649f3b3e9c73d5 Mon Sep 17 00:00:00 2001 From: Jean Prat Date: Wed, 31 Aug 2016 21:56:08 +0200 Subject: [PATCH 046/770] if user is empty, it is not converted to tuple when using host_all (#3038) --- database/mysql/mysql_user.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index ede172cedb2..d386adcd327 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -214,7 +214,7 @@ def get_mode(cursor): def user_exists(cursor, user, host, host_all): if host_all: - cursor.execute("SELECT count(*) FROM user WHERE user = %s", user) + cursor.execute("SELECT count(*) FROM user WHERE user = %s", ([user])) else: cursor.execute("SELECT count(*) FROM user WHERE user = %s AND host = %s", (user,host)) @@ -252,7 +252,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append grant_option = False if host_all: - hostnames = user_get_hostnames(cursor, user) + hostnames = user_get_hostnames(cursor, [user]) else: hostnames = [host] @@ -342,7 +342,7 @@ def user_delete(cursor, user, host, host_all, check_mode): return True if host_all: - hostnames = user_get_hostnames(cursor, user) + hostnames = user_get_hostnames(cursor, [user]) for hostname in hostnames: cursor.execute("DROP USER %s@%s", (user, hostname)) From 34201d6bc44670a38f7b362c2d60533af8aea312 Mon Sep 17 00:00:00 2001 From: Tobias Wolf Date: Wed, 31 Aug 2016 23:23:54 +0200 Subject: [PATCH 047/770] Fix database table quoting in privileges_unpack() (#3858) In Ansible 2.x this module gives `changed = True` for all privileges that are specified including a table with priv: "database.table:GRANT" Mysql returns escaped names in the format `database`.`tables`:GRANT However in PR #1358, which was intended to support dotted database names (a crazy idea to begin with), the quotes for the table name were left out, leading to `curr_priv != new_priv`. This means that the idempotency comparison between new_priv and curr_priv is always 'changed'. This PR re-introduces quoting to the table part of the priv. --- database/mysql/mysql_user.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index d386adcd327..495ccdf6b96 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -415,9 +415,12 @@ def privileges_unpack(priv, mode): for item in priv.strip().split('/'): pieces = item.strip().split(':') dbpriv = pieces[0].rsplit(".", 1) - # Do not escape if privilege is for database '*' (all databases) - if dbpriv[0].strip('`') != '*': - pieces[0] = '%s%s%s.%s' % (quote, dbpriv[0].strip('`'), quote, dbpriv[1]) + # Do not escape if privilege is for database or table, i.e. + # neither quote *. nor .* + for i, side in enumerate(dbpriv): + if side.strip('`') != '*': + dbpriv[i] = '%s%s%s' % (quote, side.strip('`'), quote) + pieces[0] = '.'.join(dbpriv) if '(' in pieces[1]: output[pieces[0]] = re.split(r',\s*(?=[^)]*(?:\(|$))', pieces[1].upper()) From 51c13ad82db731604a7f9ffda0d6d4ffc893d4ae Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Wed, 31 Aug 2016 14:39:16 -0700 Subject: [PATCH 048/770] Cleaned up the module imports --- network/dnos10/dnos10_command.py | 4 +++- network/dnos10/dnos10_config.py | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/network/dnos10/dnos10_command.py b/network/dnos10/dnos10_command.py index fd068ffc2e7..2d68e691c5a 100644 --- a/network/dnos10/dnos10_command.py +++ b/network/dnos10/dnos10_command.py @@ -131,9 +131,11 @@ type: list sample: ['...', '...'] """ + from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, FailedConditionsError -from ansible.module_utils.dnos10 import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError +import ansible.module_utils.dnos10 def to_lines(stdout): for item in stdout: diff --git a/network/dnos10/dnos10_config.py b/network/dnos10/dnos10_config.py index 26e731aa17c..8aa64c973d2 100644 --- a/network/dnos10/dnos10_config.py +++ b/network/dnos10/dnos10_config.py @@ -188,8 +188,8 @@ sample: True """ -from ansible.module_utils.netcfg import NetworkConfig, dumps, ConfigLine -from ansible.module_utils.dnos10 import NetworkModule +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.network import NetworkModule from ansible.module_utils.dnos10 import get_config, get_sublevel_config def get_candidate(module): From 95353ded58b30903d3ef9b77b9a3625f8dc4ebf5 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 31 Aug 2016 16:59:54 -0700 Subject: [PATCH 049/770] Add python3 testing for module PRs. (#4629) --- shippable.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/shippable.yml b/shippable.yml index c182c149488..c392bfa675f 100644 --- a/shippable.yml +++ b/shippable.yml @@ -17,6 +17,8 @@ matrix: - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 + - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 - env: TEST=integration PLATFORM=windows VERSION=2012-RTM From ceddebaf28f35c6d65838000a94e843f66e46079 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 1 Sep 2016 12:10:32 +0100 Subject: [PATCH 050/770] Consistent naming of Arista EOS device (#4616) --- network/eos/eos_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/eos/eos_facts.py b/network/eos/eos_facts.py index affd00375d2..89cc699a418 100644 --- a/network/eos/eos_facts.py +++ b/network/eos/eos_facts.py @@ -21,7 +21,7 @@ module: eos_facts version_added: "2.2" author: "Peter Sprygada (@privateip)" -short_description: Collect facts from remote devices running EOS +short_description: Collect facts from remote devices running Arista EOS description: - Collects a base set of device facts from a remote device that is running eos. This module prepends all of the From 7e79c59d386d43182bc078123995674476b3a60e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 1 Sep 2016 04:19:15 -0700 Subject: [PATCH 051/770] to_text, to_bytes, and to_native now have surrogate_or_strict error handler (#4630) On python3, we want to use the surrogateescape error handler if available for filesystem paths and the like. On python2, have to use strict in these circumstances. Use the new error strategy for to_text, to_bytes, and to_native that allows this. --- files/lineinfile.py | 26 ++++++++++++++------------ source_control/git.py | 4 ++-- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/files/lineinfile.py b/files/lineinfile.py index 61d1f35310e..045827f7fb8 100644 --- a/files/lineinfile.py +++ b/files/lineinfile.py @@ -164,13 +164,15 @@ def write_changes(module, b_lines, dest): if validate: if "%s" not in validate: module.fail_json(msg="validate must contain %%s: %s" % (validate)) - (rc, out, err) = module.run_command(to_bytes(validate % tmpfile)) + (rc, out, err) = module.run_command(to_bytes(validate % tmpfile, errors='surrogate_or_strict')) valid = rc == 0 if rc != 0: module.fail_json(msg='failed to validate: ' 'rc:%s error:%s' % (rc, err)) if valid: - module.atomic_move(tmpfile, os.path.realpath(dest), unsafe_writes=module.params['unsafe_writes']) + module.atomic_move(tmpfile, + to_native(os.path.realpath(to_bytes(dest, errors='surrogate_or_strict')), errors='surrogate_or_strict'), + unsafe_writes=module.params['unsafe_writes']) def check_file_attrs(module, changed, message, diff): @@ -194,7 +196,7 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create, 'before_header': '%s (content)' % dest, 'after_header': '%s (content)' % dest} - b_dest = to_bytes(dest) + b_dest = to_bytes(dest, errors='surrogate_or_strict') if not os.path.exists(b_dest): if not create: module.fail_json(rc=257, msg='Destination %s does not exist !' % dest) @@ -211,12 +213,12 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create, diff['before'] = to_native(b('').join(b_lines)) if regexp is not None: - bre_m = re.compile(to_bytes(regexp)) + bre_m = re.compile(to_bytes(regexp, errors='surrogate_or_strict')) if insertafter not in (None, 'BOF', 'EOF'): - bre_ins = re.compile(to_bytes(insertafter)) + bre_ins = re.compile(to_bytes(insertafter, errors='surrogate_or_strict')) elif insertbefore not in (None, 'BOF'): - bre_ins = re.compile(to_bytes(insertbefore)) + bre_ins = re.compile(to_bytes(insertbefore, errors='surrogate_or_strict')) else: bre_ins = None @@ -224,7 +226,7 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create, # index[1] is the line num where insertafter/inserbefore has been found index = [-1, -1] m = None - b_line = to_bytes(line) + b_line = to_bytes(line, errors='surrogate_or_strict') for lineno, b_cur_line in enumerate(b_lines): if regexp is not None: match_found = bre_m.search(b_cur_line) @@ -244,7 +246,7 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create, msg = '' changed = False # Regexp matched a line in the file - b_linesep = to_bytes(os.linesep) + b_linesep = to_bytes(os.linesep, errors='surrogate_or_strict') if index[0] != -1: if backrefs: b_new_line = m.expand(b_line) @@ -310,7 +312,7 @@ def present(module, dest, regexp, line, insertafter, insertbefore, create, def absent(module, dest, regexp, line, backup): - b_dest = to_bytes(dest) + b_dest = to_bytes(dest, errors='surrogate_or_strict') if not os.path.exists(b_dest): module.exit_json(changed=False, msg="file not present") @@ -328,10 +330,10 @@ def absent(module, dest, regexp, line, backup): diff['before'] = to_native(b('').join(b_lines)) if regexp is not None: - bre_c = re.compile(to_bytes(regexp)) + bre_c = re.compile(to_bytes(regexp, errors='surrogate_or_strict')) found = [] - b_line = to_bytes(line) + b_line = to_bytes(line, errors='surrogate_or_strict') def matcher(b_cur_line): if regexp is not None: match_found = bre_c.search(b_cur_line) @@ -392,7 +394,7 @@ def main(): backrefs = params['backrefs'] dest = params['dest'] - b_dest = to_bytes(dest) + b_dest = to_bytes(dest, errors='surrogate_or_strict') if os.path.isdir(b_dest): module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) diff --git a/source_control/git.py b/source_control/git.py index 1641eac6498..570350fffe3 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -483,7 +483,7 @@ def get_remote_head(git_path, module, dest, version, remote, bare): def is_remote_tag(git_path, module, dest, remote, version): cmd = '%s ls-remote %s -t refs/tags/%s' % (git_path, remote, version) (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) - if to_bytes(version) in out: + if to_bytes(version, errors='surrogate_or_strict') in out: return True else: return False @@ -513,7 +513,7 @@ def get_tags(git_path, module, dest): def is_remote_branch(git_path, module, dest, remote, version): cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) - if to_bytes(version) in out: + if to_bytes(version, errors='surrogate_or_strict') in out: return True else: return False From 5f652c758487a96e08c33989e846b87e204def72 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 1 Sep 2016 13:26:23 +0200 Subject: [PATCH 052/770] Add support for selinux_boolean_sub conversion (#4570) SELinux since 2012 use a configuration file to convert boolean names from a old name to a new name, for preserving backward compatibility. However, this has to be done explicitely when using the python bindings, and the module was not doing it. Openshift ansible script use this construct to detect if a boolean exist or not: - name: Check for existence of virt_sandbox_use_nfs seboolean command: getsebool virt_sandbox_use_nfs register: virt_sandbox_use_nfs_output failed_when: false changed_when: false - name: Set seboolean to allow nfs storage plugin access from containers(sandbox) seboolean: name: virt_sandbox_use_nfs state: yes persistent: yes when: virt_sandbox_use_nfs_output.rc == 0 On a system where virt_sandbox_use_nfs do not exist, this work. But on a system where virt_sandbox_use_nfs is a alias to virt_use_nfs (like Fedora 24), this fail because the seboolean is not aware of the alias. --- system/seboolean.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/seboolean.py b/system/seboolean.py index 1fbb83f2a7d..1fc9ac2579b 100644 --- a/system/seboolean.py +++ b/system/seboolean.py @@ -182,6 +182,11 @@ def main(): result = {} result['name'] = name + if hasattr(selinux, 'selinux_boolean_sub'): + # selinux_boolean_sub allows sites to rename a boolean and alias the old name + # Feature only available in selinux library since 2012. + name = selinux.selinux_boolean_sub(name) + if not has_boolean_value(module, name): module.fail_json(msg="SELinux boolean %s does not exist." % name) From 4c8bbae415fe17b227fa8f044a5677f5025380b1 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 2 Sep 2016 08:15:07 -0400 Subject: [PATCH 053/770] bugfix that adds missing itertools import to ios_facts fixes #4647 --- network/ios/ios_facts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py index d842c2b4c09..884e9b5b296 100644 --- a/network/ios/ios_facts.py +++ b/network/ios/ios_facts.py @@ -124,6 +124,7 @@ type: dict """ import re +import itertools from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, AddCommandError From 71067b1d4e86b0796827f40b63bce545a6bf2d14 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 14:23:54 +0200 Subject: [PATCH 054/770] Removing delete for bollean, fixing argparse, embedding python object --- network/nxos/nxos_bgp.py | 368 +++++++++++++++++++++++++++++++++------ 1 file changed, 314 insertions(+), 54 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index b8d21f82f8e..7bd3a3678e9 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -336,11 +336,6 @@ "router_id": "1.1.1.1", "suppress_fib_pending": false, "timer_bestpath_limit": "", "timer_bgp_hold": "180", "timer_bgp_keepalive": "60", "vrf": "test"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: commands sent to the device returned: always @@ -360,9 +355,13 @@ import collections import itertools import shlex -import itertools +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + DEFAULT_COMMENT_TOKENS = ['#', '!'] @@ -730,14 +729,282 @@ def argument_spec(): ) nxos_argument_spec = argument_spec() -def get_config(module): + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): config = module.params['running_config'] if not config: - config = module.get_config() + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): - config = get_config(module) + config = custom_get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] @@ -758,12 +1025,8 @@ def load_config(module, candidate): return result # END OF COMMON CODE -import re WARNINGS = [] -BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 'True', 1, True] -BOOLEANS_FALSE = ['no', 'off', '0', 'false', 'False', 0, False] -ACCEPTED = BOOLEANS_TRUE + BOOLEANS_FALSE + ['default'] BOOL_PARAMS = [ 'bestpath_always_compare_med', 'bestpath_aspath_multipath_relax', @@ -877,6 +1140,15 @@ def get_custom_value(config, arg): if REGEX.search(config): value = True + elif arg == 'enforce_first_as' or arg == 'fast_external_fallover': + REGEX = re.compile(r'no\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = True + try: + if REGEX.search(config): + value = False + except TypeError: + value = True + elif arg == 'confederation_peers': REGEX = re.compile(r'(?:confederation peers\s)(?P.*)$', re.M) value = '' @@ -909,10 +1181,14 @@ def get_value(arg, config): 'event_history_detail', 'confederation_peers', 'timer_bgp_hold', - 'timer_bgp_keepalive' + 'timer_bgp_keepalive', + 'enforce_first_as', + 'fast_external_fallover' ] - if arg in BOOL_PARAMS: + if arg in custom: + value = get_custom_value(config, arg) + elif arg in BOOL_PARAMS: REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) value = False try: @@ -920,8 +1196,6 @@ def get_value(arg, config): value = True except TypeError: value = False - elif arg in custom: - value = get_custom_value(config, arg) else: REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) value = '' @@ -932,7 +1206,7 @@ def get_value(arg, config): def get_existing(module, args): existing = {} - netcfg = get_config(module) + netcfg = custom_get_config(module) try: asn_regex = '.*router\sbgp\s(?P\d+).*' @@ -993,6 +1267,7 @@ def state_present(module, existing, proposed, candidate): commands = list() proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.iteritems(): if value is True: commands.append(key) @@ -1007,9 +1282,6 @@ def state_present(module, existing, proposed, candidate): commands.append('no {0} {1}'.format(key, ' '.join(existing_value))) else: commands.append('no {0} {1}'.format(key, existing_value)) - else: - if key.replace(' ', '_').replace('-', '_') in BOOL_PARAMS: - commands.append('no {0}'.format(key)) else: if key == 'confederation peers': existing_confederation_peers = existing.get('confederation_peers') @@ -1100,40 +1372,40 @@ def main(): argument_spec = dict( asn=dict(required=True, type='str'), vrf=dict(required=False, type='str', default='default'), - bestpath_always_compare_med=dict(required=False, choices=ACCEPTED), - bestpath_aspath_multipath_relax=dict(required=False, choices=ACCEPTED), - bestpath_compare_neighborid=dict(required=False, choices=ACCEPTED), - bestpath_compare_routerid=dict(required=False, choices=ACCEPTED), - bestpath_cost_community_ignore=dict(required=False, choices=ACCEPTED), - bestpath_med_confed=dict(required=False, choices=ACCEPTED), - bestpath_med_missing_as_worst=dict(required=False, choices=ACCEPTED), - bestpath_med_non_deterministic=dict(required=False, choices=ACCEPTED), + bestpath_always_compare_med=dict(required=False, type='bool'), + bestpath_aspath_multipath_relax=dict(required=False, type='bool'), + bestpath_compare_neighborid=dict(required=False, type='bool'), + bestpath_compare_routerid=dict(required=False, type='bool'), + bestpath_cost_community_ignore=dict(required=False, type='bool'), + bestpath_med_confed=dict(required=False, type='bool'), + bestpath_med_missing_as_worst=dict(required=False, type='bool'), + bestpath_med_non_deterministic=dict(required=False, type='bool'), cluster_id=dict(required=False, type='str'), confederation_id=dict(required=False, type='str'), confederation_peers=dict(required=False, type='str'), - disable_policy_batching=dict(required=False, choices=ACCEPTED), + disable_policy_batching=dict(required=False, type='bool'), disable_policy_batching_ipv4_prefix_list=dict(required=False, type='str'), disable_policy_batching_ipv6_prefix_list=dict(required=False, type='str'), - enforce_first_as=dict(required=False, choices=ACCEPTED), + enforce_first_as=dict(required=False, type='bool'), event_history_cli=dict(required=False, choices=['true', 'false', 'default', 'size_small', 'size_medium', 'size_large', 'size_disable']), event_history_detail=dict(required=False, choices=['true', 'false', 'default', 'size_small', 'size_medium', 'size_large', 'size_disable']), event_history_events=dict(required=False, choices=['true', 'false', 'default' 'size_small', 'size_medium', 'size_large', 'size_disable']), event_history_periodic=dict(required=False, choices=['true', 'false', 'default', 'size_small', 'size_medium', 'size_large', 'size_disable']), - fast_external_fallover=dict(required=False, choices=ACCEPTED), - flush_routes=dict(required=False, choices=ACCEPTED), - graceful_restart=dict(required=False, choices=ACCEPTED), - graceful_restart_helper=dict(required=False, choices=ACCEPTED), + fast_external_fallover=dict(required=False, type='bool'), + flush_routes=dict(required=False, type='bool'), + graceful_restart=dict(required=False, type='bool'), + graceful_restart_helper=dict(required=False, type='bool'), graceful_restart_timers_restart=dict(required=False, type='str'), graceful_restart_timers_stalepath_time=dict(required=False, type='str'), - isolate=dict(required=False, choices=ACCEPTED), + isolate=dict(required=False, type='bool'), local_as=dict(required=False, type='str'), - log_neighbor_changes=dict(required=False, choices=ACCEPTED), + log_neighbor_changes=dict(required=False, type='bool'), maxas_limit=dict(required=False, type='str'), - neighbor_down_fib_accelerate=dict(required=False, choices=ACCEPTED), + neighbor_down_fib_accelerate=dict(required=False, type='bool'), reconnect_interval=dict(required=False, type='str'), router_id=dict(required=False, type='str'), - shutdown=dict(required=False, choices=ACCEPTED), - suppress_fib_pending=dict(required=False, choices=ACCEPTED), + shutdown=dict(required=False, type='bool'), + suppress_fib_pending=dict(required=False, type='bool'), timer_bestpath_limit=dict(required=False, type='str'), timer_bgp_hold=dict(required=False, type='str'), timer_bgp_keepalive=dict(required=False, type='str'), @@ -1214,17 +1486,10 @@ def main(): proposed = {} for key, value in proposed_args.iteritems(): if key != 'asn' and key != 'vrf': - if value.lower() == 'true': - value = True - elif value.lower() == 'false': - value = False - elif value.lower() == 'default': + if str(value).lower() == 'default': value = PARAM_TO_DEFAULT_KEYMAP.get(key) if value is None: - if key in BOOL_PARAMS: - value = False - else: - value = 'default' + value = 'default' if existing.get(key) or (not existing.get(key) and value): proposed[key] = value @@ -1256,10 +1521,5 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 2579ca43bfe96fd6b177795247721103d93b54ea Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 14:34:07 +0200 Subject: [PATCH 055/770] Fixing docstring --- network/nxos/nxos_bgp.py | 42 ++++++++++++++++++++-------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 7bd3a3678e9..b183d94510a 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -48,48 +48,48 @@ description: - Enable/Disable MED comparison on paths from different autonomous systems. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_aspath_multipath_relax: description: - Enable/Disable load sharing across the providers with different (but equal-length) AS paths. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_compare_routerid: description: - Enable/Disable comparison of router IDs for identical eBGP paths. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_cost_community_ignore: description: - Enable/Disable Ignores the cost community for BGP best-path calculations. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_med_confed: description: - Enable/Disable enforcement of bestpath to do a MED comparison only between paths originated within a confederation. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_med_missing_as_worst: description: - Enable/Disable assigns the value of infinity to received routes that do not carry the MED attribute, making these routes the least desirable. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null bestpath_med_non_deterministic: description: - Enable/Disable deterministic selection of the best MED path from among the paths from the same autonomous system. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null cluster_id: description: @@ -110,7 +110,7 @@ description: - Enable/Disable the batching evaluation of prefix advertisements to all peers. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null disable_policy_batching_ipv4_prefix_list: description: @@ -128,7 +128,7 @@ listed in the AS path attribute for eBGP. On NX-OS, this property is only supported in the global BGP context. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null event_history_cli: description: @@ -158,43 +158,43 @@ - Enable/Disable immediately reset the session if the link to a directly connected BGP peer goes down. Only supported in the global BGP context. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null flush_routes: description: - Enable/Disable flush routes in RIB upon controlled restart. On NX-OS, this property is only supported in the global BGP context. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart: description: - Enable/Disable graceful restart. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart_helper: description: - Enable/Disable graceful restart helper mode. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart_timers_restart: description: - Set maximum time for a restart sent to the BGP peer. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null graceful_restart_timers_stalepath_time: description: - Set maximum time that BGP keeps the stale routes from the restarting BGP peer. - choices: ['true','false', 'default'] + choices: ['true','false'] default: null isolate: description: - Enable/Disable isolate this router from BGP perspective. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null local_as: description: @@ -205,7 +205,7 @@ description: - Enable/Disable message logging for neighbor up/down event. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null maxas_limit: description: @@ -217,7 +217,7 @@ description: - Enable/Disable handle BGP neighbor down event, due to various reasons. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null reconnect_interval: description: @@ -233,13 +233,13 @@ description: - Administratively shutdown the BGP protocol. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null suppress_fib_pending: description: - Enable/Disable advertise only routes programmed in hardware to peers. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null timer_bestpath_limit: description: @@ -250,7 +250,7 @@ description: - Enable/Disable update-delay-always option. required: false - choices: ['true','false', 'default'] + choices: ['true','false'] default: null timer_bgp_hold: description: From 8518cb4e97a210e98a395ef9f8c8af4ffcb9a780 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 16:05:23 +0200 Subject: [PATCH 056/770] Adding python objects for 2.1 support --- network/nxos/nxos_acl.py | 318 +++++++++++++++++++++++++++++++++++---- 1 file changed, 292 insertions(+), 26 deletions(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index c6d0f630db6..05a2bda8ee4 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -44,7 +44,8 @@ seq: description: - sequence number of the entry (ACE) - required: true + required: false + default: null name: description: - Case sensitive name of the access list (ACL) @@ -52,7 +53,8 @@ action: description: - action of the ACE - required: true + required: false + default: null choices: ['permit', 'deny', 'remark'] remark: description: @@ -62,11 +64,13 @@ proto: description: - port number or protocol (as supported by the switch) - required: true + required: false + default: null src: description: - src ip and mask using IP/MASK notation and supports keyword 'any' - required: true + required: false + default: null src_port_op: description: - src port operands such as eq, neq, gt, lt, range @@ -88,7 +92,8 @@ description: - dest ip and mask using IP/MASK notation and supports the keyword 'any' - required: true + required: false + default: null default: null dest_port_op: description: @@ -220,11 +225,6 @@ type: dict sample: {"action": "permit", "dest": "any", "name": "ANSIBLE", "proto": "tcp", "seq": "10", "src": "1.1.1.1/24"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: commands sent to the device returned: always @@ -245,9 +245,14 @@ import collections import itertools import shlex -import itertools +import json +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + DEFAULT_COMMENT_TOKENS = ['#', '!'] @@ -615,14 +620,282 @@ def argument_spec(): ) nxos_argument_spec = argument_spec() -def get_config(module): + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): config = module.params['running_config'] if not config: - config = module.get_config() + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): - config = get_config(module) + config = custom_get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] @@ -643,7 +916,6 @@ def load_config(module, candidate): return result # END OF COMMON CODE - 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 @@ -858,16 +1130,16 @@ def execute_config_command(commands, module): def main(): argument_spec = dict( - seq=dict(required=True, type='str'), + seq=dict(required=False, type='str'), name=dict(required=True, type='str'), - action=dict(required=True, choices=['remark', 'permit', 'deny']), + action=dict(required=False, choices=['remark', 'permit', 'deny']), remark=dict(requried=False, type='str'), - proto=dict(required=True, type='str'), - src=dict(required=True, type='str'), + proto=dict(required=False, type='str'), + src=dict(required=False, type='str'), src_port_op=dict(required=False), src_port1=dict(required=False, type='str'), src_port2=dict(required=False, type='str'), - dest=dict(required=True, type='str'), + dest=dict(required=False, type='str'), dest_port_op=dict(required=False), dest_port1=dict(required=False, type='str'), dest_port2=dict(required=False, type='str'), @@ -1018,7 +1290,6 @@ def main(): results['proposed'] = proposed results['existing'] = existing_core - results['state'] = state results['changed'] = changed results['updates'] = cmds results['end_state'] = end_state @@ -1026,10 +1297,5 @@ def main(): module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From fe11b09b8da84eda019bab2ec00ddde2640c90e1 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 16:17:03 +0200 Subject: [PATCH 057/770] Adding python object for 2.1 support --- network/nxos/nxos_acl_interface.py | 286 ++++++++++++++++++++++++++++- 1 file changed, 277 insertions(+), 9 deletions(-) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index ad6a0cd30e1..4a0cc4c3827 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -108,9 +108,13 @@ import collections import itertools import shlex -import itertools +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + DEFAULT_COMMENT_TOKENS = ['#', '!'] @@ -478,14 +482,282 @@ def argument_spec(): ) nxos_argument_spec = argument_spec() -def get_config(module): + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): config = module.params['running_config'] if not config: - config = module.get_config() + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): - config = get_config(module) + config = custom_get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] @@ -756,10 +1028,6 @@ def main(): module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * + if __name__ == '__main__': main() From 1c48fe9bc3869d0576a0625a77d58594689252be Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 16:42:25 +0200 Subject: [PATCH 058/770] Adding python object for 2.1 support --- network/nxos/nxos_evpn_global.py | 287 +++++++++++++++++++++++++++++-- 1 file changed, 277 insertions(+), 10 deletions(-) diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index 4ddeb785806..ff1e0645be2 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -79,9 +79,13 @@ import collections import itertools import shlex -import itertools +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + DEFAULT_COMMENT_TOKENS = ['#', '!'] @@ -449,14 +453,282 @@ def argument_spec(): ) nxos_argument_spec = argument_spec() -def get_config(module): + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): config = module.params['running_config'] if not config: - config = module.get_config() + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): - config = get_config(module) + config = custom_get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] @@ -498,7 +770,7 @@ def get_value(arg, config, module): def get_existing(module): existing = {} - config = str(get_config(module)) + config = str(custom_get_config(module)) existing['nv_overlay_evpn'] = get_value('nv_overlay_evpn', config, module) return existing @@ -570,10 +842,5 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 4b20a897390528f3ce26bf90303551b32fbeaf70 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 16:51:32 +0200 Subject: [PATCH 059/770] Adding python object for 2.1 support --- network/nxos/nxos_ospf.py | 288 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 277 insertions(+), 11 deletions(-) diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index 04572b10c60..f02f0584308 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -89,9 +89,13 @@ import collections import itertools import shlex -import itertools +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + DEFAULT_COMMENT_TOKENS = ['#', '!'] @@ -459,14 +463,282 @@ def argument_spec(): ) nxos_argument_spec = argument_spec() -def get_config(module): + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): config = module.params['running_config'] if not config: - config = module.get_config() + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): - config = get_config(module) + config = custom_get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] @@ -519,7 +791,7 @@ def get_value(config, module): def get_existing(module): existing = {} - config = str(get_config(module)) + config = str(custom_get_config(module)) value = get_value(config, module) if value: @@ -598,11 +870,5 @@ def main(): module.exit_json(**result) - -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 00f6276eaf0279003a4d5c00cc1e63aaf84abc25 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 16:59:34 +0200 Subject: [PATCH 060/770] Adding python object for 2.1 support --- network/nxos/nxos_ospf_vrf.py | 288 ++++++++++++++++++++++++++++++++-- 1 file changed, 277 insertions(+), 11 deletions(-) diff --git a/network/nxos/nxos_ospf_vrf.py b/network/nxos/nxos_ospf_vrf.py index 6878d4f2849..ef5e96ea8d4 100644 --- a/network/nxos/nxos_ospf_vrf.py +++ b/network/nxos/nxos_ospf_vrf.py @@ -182,9 +182,13 @@ import collections import itertools import shlex -import itertools +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + DEFAULT_COMMENT_TOKENS = ['#', '!'] @@ -552,14 +556,282 @@ def argument_spec(): ) nxos_argument_spec = argument_spec() -def get_config(module): + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): config = module.params['running_config'] if not config: - config = module.get_config() + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + return CustomNetworkConfig(indent=2, contents=config) def load_config(module, candidate): - config = get_config(module) + config = custom_get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] @@ -637,7 +909,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = get_config(module) + netcfg = custom_get_config(module) parents = ['router ospf {0}'.format(module.params['ospf'])] if module.params['vrf'] != 'default': @@ -845,11 +1117,5 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * - if __name__ == '__main__': main() From 5ba6a45d65643dbab93d6f36b8f0b40880d21f07 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 17:28:36 +0200 Subject: [PATCH 061/770] Adding nxos_bgp_af module --- network/nxos/nxos_bgp_af.py | 1639 +++++++++++++++++++++++++++++++++++ 1 file changed, 1639 insertions(+) create mode 100644 network/nxos/nxos_bgp_af.py diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py new file mode 100644 index 00000000000..6acab3655a2 --- /dev/null +++ b/network/nxos/nxos_bgp_af.py @@ -0,0 +1,1639 @@ +#!/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_bgp_af +version_added: "2.2" +short_description: Manages BGP Address-family configuration +description: + - Manages BGP Address-family configurations on NX-OS switches +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - State 'absent' removes the whole BGP ASN configuration + - default, where supported, restores params default value +options: + asn: + description: + - BGP autonomous system number. Valid values are String, + Integer in ASPLAIN or ASDOT notation. + required: true + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing + the global bgp. + required: true + afi: + description: + - Address Family Identifie. + required: true + choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn'] + safi: + description: + - Sub Address Family Identifier. + required: true + choices: ['unicast','multicast', 'evpn'] + additional_paths_install: + description: + - Install a backup path into the forwarding table and provide + prefix 'independent convergence (PIC) in case of a PE-CE link + failure. + required: false + choices: ['true','false'] + default: null + additional_paths_receive: + description: + - Enables the receive capability of additional paths for all of + the neighbors under this address family for which the capability + has not been disabled. + required: false + choices: ['true','false'] + default: null + additional_paths_selection: + description: + - Configures the capability of selecting additional paths for + a prefix. Valid values are a string defining the name of + the route-map. + required: false + default: null + additional_paths_send: + description: + - Enables the send capability of additional paths for all of + the neighbors under this address family for which the capability + has not been disabled. + required: false + choices: ['true','false'] + default: null + advertise_l2vpn_evpn: + description: + - Advertise evpn routes. + required: false + choices: ['true','false'] + default: null + client_to_client: + description: + - Configure client-to-client route reflection. + required: false + choices: ['true','false'] + default: null + dampen_igp_metric: + description: + - Specify dampen value for IGP metric-related changes, in seconds. + Valid values are integer and keyword 'default'. + required: false + default: null + dampening_state: + description: + - Enable/disable route-flap dampening. + required: false + choices: ['true','false'] + default: null + dampening_half_time: + description: + - Specify decay half-life in minutes for route-flap dampening. + Valid values are integer and keyword 'default'. + required: false + default: null + dampening_max_suppress_time: + description: + - Specify max suppress time for route-flap dampening stable route. + Valid values are integer and keyword 'default'. + required: false + default: null + dampening_reuse_time: + description: + - Specify route reuse time for route-flap dampening. + Valid values are integer and keyword 'default'. + required: false + dampening_routemap: + description: + - Specify route-map for route-flap dampening. Valid values are a + string defining the name of the route-map. + required: false + default: null + dampening_suppress_time: + description: + - Specify route suppress time for route-flap dampening. + Valid values are integer and keyword 'default'. + required: false + default: null + default_information_originate: + description: + - Default information originate. + required: false + choices: ['true','false'] + default: null + default_metric: + description: + - Sets default metrics for routes redistributed into BGP. + Valid values are Integer or keyword 'default' + required: false + default: null + distance_ebgp: + description: + - Sets the administrative distance for eBGP routes. + Valid values are Integer or keyword 'default'. + required: false + default: null + distance_ibgp: + description: + - Sets the administrative distance for iBGP routes. + Valid values are Integer or keyword 'default'. + required: false + default: null + distance_local: + description: + - Sets the administrative distance for local BGP routes. + Valid values are Integer or keyword 'default'. + required: false + default: null + inject_map: + description: + - An array of route-map names which will specify prefixes to + inject. Each array entry must first specify the inject-map name, + secondly an exist-map name, and optionally the copy-attributes + keyword which indicates that attributes should be copied from + the aggregate. Example: [['lax_inject_map', 'lax_exist_map'], + ['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'], + ['fsd_inject_map', 'fsd_exist_map']] + required: false + default: null + maximum_paths: + description: + - Configures the maximum number of equal-cost paths for + load sharing. Valid value is an integer in the range 1-64. + default: null + maximum_paths_ibgp: + description: + - Configures the maximum number of ibgp equal-cost paths for + load sharing. Valid value is an integer in the range 1-64. + required: false + default: null + networks: + description: + - Networks to configure. Valid value is a list of network + prefixes to advertise. The list must be in the form of an array. + Each entry in the array must include a prefix address and an + optional route-map. Example: [['10.0.0.0/16', 'routemap_LA'], + ['192.168.1.1', 'Chicago'], ['192.168.2.0/24], + ['192.168.3.0/24', 'routemap_NYC']] + required: false + default: null + next_hop_route_map: + description: + - Configure a route-map for valid nexthops. Valid values are a + string defining the name of the route-map. + required: false + default: null + redistribute: + description: + - A list of redistribute directives. Multiple redistribute entries + are allowed. The list must be in the form of a nested array: + the first entry of each array defines the source-protocol to + redistribute from; the second entry defines a route-map name. + A route-map is highly advised but may be optional on some + platforms, in which case it may be omitted from the array list. + Example: [['direct', 'rm_direct'], ['lisp', 'rm_lisp']] + required: false + default: null + suppress_inactive: + description: + - Advertises only active routes to peers. + required: false + choices: ['true','false'] + default: null + table_map: + description: + - Apply table-map to filter routes downloaded into URIB. + Valid values are a string. + required: false + default: null + table_map_filter: + description: + - Filters routes rejected by the route-map and does not download + them to the RIB. + required: false + choices: ['true','false'] + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +# configure a simple address-family +- nxos_bgp_af: + asn=65535 + vrf=TESTING + afi=ipv4 + safi=unicast + advertise_l2vpn_evpn=true + state=present +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"advertise_l2vpn_evpn": true, "afi": "ipv4", + "asn": "65535", "safi": "unicast", "vrf": "TESTING"} +existing: + description: k/v pairs of existing BGP AF configuration + type: dict + sample: {} +end_state: + description: k/v pairs of BGP AF configuration after module execution + returned: always + type: dict + sample: {"additional_paths_install": false, + "additional_paths_receive": false, + "additional_paths_selection": "", + "additional_paths_send": false, + "advertise_l2vpn_evpn": true, "afi": "ipv4", + "asn": "65535", "client_to_client": true, + "dampen_igp_metric": "600", "dampening_half_time": "", + "dampening_max_suppress_time": "", "dampening_reuse_time": "", + "dampening_routemap": "", "dampening_state": false, + "dampening_suppress_time": "", + "default_information_originate": false, "default_metric": "", + "distance_ebgp": "20", "distance_ibgp": "200", + "distance_local": "220", "inject_map": [], "maximum_paths": "1", + "maximum_paths_ibgp": "1", "networks": [], + "next_hop_route_map": "", "redistribute": [], "safi": "unicast", + "suppress_inactive": false, "table_map": "", + "table_map_filter": false, "vrf": "TESTING"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf TESTING", + "address-family ipv4 unicast", "advertise l2vpn evpn"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +WARNINGS = [] +BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 'True', 1, True] +BOOLEANS_FALSE = ['no', 'off', '0', 'false', 'False', 0, False] +BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE +ACCEPTED = BOOLEANS_TRUE + BOOLEANS_FALSE + ['default'] +BOOL_PARAMS = [ + 'additional_paths_install', + 'additional_paths_receive', + 'additional_paths_send', + 'advertise_l2vpn_evpn', + 'client_to_client', + 'dampening_state', + 'default_information_originate', + 'suppress_inactive', +] +PARAM_TO_DEFAULT_KEYMAP = { + 'maximum_paths': '1', + 'maximum_paths_ibgp': '1', + 'client_to_client': True, + 'distance_ebgp': '20', + 'distance_ibgp': '200', + 'distance_local': '220', + 'dampen_igp_metric': '600' +} +PARAM_TO_COMMAND_KEYMAP = { + 'asn': 'router bgp', + 'afi': 'address-family', + 'safi': 'address-family', + 'additional_paths_install': 'additional-paths install backup', + 'additional_paths_receive': 'additional-paths receive', + 'additional_paths_selection': 'additional-paths selection route-map', + 'additional_paths_send': 'additional-paths send', + 'advertise_l2vpn_evpn': 'advertise l2vpn evpn', + 'client_to_client': 'client-to-client reflection', + 'dampen_igp_metric': 'dampen-igp-metric', + 'dampening_state': 'dampening', + 'dampening_half_time': 'dampening', + 'dampening_max_suppress_time': 'dampening', + 'dampening_reuse_time': 'dampening', + 'dampening_routemap': 'dampening route-map', + 'dampening_suppress_time': 'dampening', + 'default_information_originate': 'default-information originate', + 'default_metric': 'default-metric', + 'distance_ebgp': 'distance', + 'distance_ibgp': 'distance', + 'distance_local': 'distance', + 'inject_map': 'inject-map', + 'maximum_paths': 'maximum-paths', + 'maximum_paths_ibgp': 'maximum-paths ibgp', + 'networks': 'network', + 'redistribute': 'redistribute', + 'next_hop_route_map': 'nexthop route-map', + 'suppress_inactive': 'suppress-inactive', + 'table_map': 'table-map', + 'table_map_filter': 'table-map-filter', + 'vrf': 'vrf' +} +DAMPENING_PARAMS = [ + 'dampening_half_time', + 'dampening_suppress_time', + 'dampening_reuse_time', + 'dampening_max_suppress_time' + ] + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_custom_list_value(config, arg, module): + value_list = [] + splitted_config = config.splitlines() + if arg == 'inject_map': + REGEX_INJECT = ('.*inject-map\s(?P\S+)' + '\sexist-map\s(?P\S+)-*') + + for line in splitted_config: + value = [] + inject_group = {} + try: + match_inject = re.match(REGEX_INJECT, line, re.DOTALL) + inject_group = match_inject.groupdict() + inject_map = inject_group['inject_map'] + exist_map = inject_group['exist_map'] + value.append(inject_map) + value.append(exist_map) + except AttributeError: + value = [] + + if value: + copy_attributes = False + inject_map_command = ('inject-map {0} exist-map {1} ' + 'copy-attributes'.format( + inject_group['inject_map'], + inject_group['exist_map'])) + + REGEX = re.compile(r'\s+{0}\s*$'.format( + inject_map_command), re.M) + try: + if REGEX.search(config): + copy_attributes = True + except TypeError: + copy_attributes = False + + if copy_attributes: + value.append('copy_attributes') + value_list.append(value) + + elif arg == 'networks': + REGEX_NETWORK = re.compile(r'(?:network\s)(?P.*)$') + + for line in splitted_config: + value = [] + network_group = {} + if 'network' in line: + value = REGEX_NETWORK.search(line).group('value').split() + + if value: + if len(value) == 3: + value.pop(1) + value_list.append(value) + + elif arg == 'redistribute': + RED_REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format( + PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + for line in splitted_config: + value = [] + redistribute_group = {} + if 'redistribute' in line: + value = RED_REGEX.search(line).group('value').split() + if value: + if len(value) == 3: + value.pop(1) + elif len(value) == 4: + value = ['{0} {1}'.format( + value[0], value[1]), value[3]] + value_list.append(value) + return value_list + + +def get_custom_string_value(config, arg, module): + value = '' + if arg.startswith('distance'): + REGEX_DISTANCE = ('.*distance\s(?P\w+)\s(?P\w+)' + '\s(?P\w+)') + try: + match_distance = re.match(REGEX_DISTANCE, config, re.DOTALL) + distance_group = match_distance.groupdict() + except AttributeError: + distance_group = {} + + if distance_group: + if arg == 'distance_ebgp': + value = distance_group['d_ebgp'] + elif arg == 'distance_ibgp': + value = distance_group['d_ibgp'] + elif arg == 'distance_local': + value = distance_group['d_local'] + + elif arg.startswith('dampening'): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format( + PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + if arg == 'dampen_igp_metric' or arg == 'dampening_routemap': + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + else: + REGEX_DAMPENING = ('.*dampening\s(?P\w+)\s(?P\w+)' + '\s(?P\w+)\s(?P\w+)') + try: + match_dampening = re.match(REGEX_DAMPENING, config, re.DOTALL) + dampening_group = match_dampening.groupdict() + except AttributeError: + dampening_group = {} + + if dampening_group: + if arg == 'dampening_half_time': + value = dampening_group['half'] + elif arg == 'dampening_reuse_time': + value = dampening_group['reuse'] + elif arg == 'dampening_suppress_time': + value = dampening_group['suppress'] + elif arg == 'dampening_max_suppress_time': + value = dampening_group['max_suppress'] + + elif arg == 'table_map_filter': + TMF_REGEX = re.compile(r'\s+table-map.*filter$', re.M) + value = False + try: + if TMF_REGEX.search(config): + value = True + except TypeError: + value = False + elif arg == 'table_map': + TM_REGEX = re.compile(r'(?:table-map\s)(?P\S+)(\sfilter)?$', re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = TM_REGEX.search(config).group('value') + return value + + +def get_value(arg, config, module): + custom = [ + 'inject_map', + 'networks', + 'redistribute' + ] + + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + + elif arg in custom: + value = get_custom_list_value(config, arg, module) + + elif (arg.startswith('distance') or arg.startswith('dampening') or + arg.startswith('table_map')): + value = get_custom_string_value(config, arg, module) + + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + + try: + asn_regex = '.*router\sbgp\s(?P\d+).*' + match_asn = re.match(asn_regex, str(netcfg), re.DOTALL) + existing_asn_group = match_asn.groupdict() + existing_asn = existing_asn_group['existing_asn'] + except AttributeError: + existing_asn = '' + + if existing_asn: + parents = ["router bgp {0}".format(existing_asn)] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + parents.append('address-family {0} {1}'.format(module.params['afi'], + module.params['safi'])) + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg not in ['asn', 'afi', 'safi', 'vrf']: + existing[arg] = get_value(arg, config, module) + + existing['asn'] = existing_asn + existing['afi'] = module.params['afi'] + existing['safi'] = module.params['safi'] + existing['vrf'] = module.params['vrf'] + else: + WARNINGS.append("The BGP process {0} didn't exist but the task" + " just created it.".format(module.params['asn'])) + + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def fix_proposed(module, proposed, existing): + commands = list() + command = '' + fixed_proposed = {} + for key, value in proposed.iteritems(): + if key in DAMPENING_PARAMS: + if value != 'default': + command = 'dampening {0} {1} {2} {3}'.format( + proposed.get('dampening_half_time'), + proposed.get('dampening_reuse_time'), + proposed.get('dampening_suppress_time'), + proposed.get('dampening_max_suppress_time')) + else: + if existing.get(key): + command = ('no dampening {0} {1} {2} {3}'.format( + existing['dampening_half_time'], + existing['dampening_reuse_time'], + existing['dampening_suppress_time'], + existing['dampening_max_suppress_time'])) + if 'default' in command: + command = '' + elif key.startswith('distance'): + command = 'distance {0} {1} {2}'.format( + proposed.get('distance_ebgp'), + proposed.get('distance_ibgp'), + proposed.get('distance_local')) + else: + fixed_proposed[key] = value + + if command: + if command not in commands: + commands.append(command) + + return fixed_proposed, commands + + +def default_existing(existing_value, key, value): + commands = [] + if key == 'network': + for network in existing_value: + if len(network) == 2: + commands.append('no network {0} route-map {1}'.format( + network[0], network[1])) + elif len(network) == 1: + commands.append('no network {0}'.format( + network[0])) + + elif key == 'inject-map': + for maps in existing_value: + if len(maps) == 2: + commands.append('no inject-map {0} exist-map {1}'.format( + maps[0], maps[1])) + elif len(maps) == 3: + commands.append('no inject-map {0} exist-map {1} ' + 'copy-attributes'.format( + maps[0], maps[1])) + else: + commands.append('no {0} {1}'.format(key, existing_value)) + return commands + + +def get_network_command(existing, key, value): + commands = [] + existing_networks = existing.get('networks', []) + for inet in value: + if not isinstance(inet, list): + inet = [inet] + if inet not in existing_networks: + if len(inet) == 1: + command = '{0} {1}'.format(key, inet[0]) + elif len(inet) == 2: + command = '{0} {1} route-map {2}'.format(key, + inet[0], inet[1]) + commands.append(command) + return commands + + +def get_inject_map_command(existing, key, value): + commands = [] + existing_maps = existing.get('inject_map', []) + for maps in value: + if not isinstance(maps, list): + maps = [maps] + if maps not in existing_maps: + if len(maps) == 2: + command = ('inject-map {0} exist-map {1}'.format( + maps[0], maps[1])) + elif len(maps) == 3: + command = ('inject-map {0} exist-map {1} ' + 'copy-attributes'.format(maps[0], + maps[1])) + commands.append(command) + return commands + + +def get_redistribute_command(existing, key, value): + commands = [] + for rule in value: + if rule[1] == 'default': + existing_rule = existing.get('redistribute', []) + for each_rule in existing_rule: + if rule[0] in each_rule: + command = 'no {0} {1} route-map {2}'.format( + key, each_rule[0], each_rule[1]) + commands.append(command) + else: + command = '{0} {1} route-map {2}'.format(key, rule[0], rule[1]) + commands.append(command) + return commands + + +def get_table_map_command(module, existing, key, value): + commands = [] + if key == 'table-map': + if value != 'default': + command = '{0} {1}'.format(key, module.params['table_map']) + if (module.params['table_map_filter'] is not None and + module.params['table_map_filter'] != 'default'): + command += ' filter' + commands.append(command) + else: + if existing.get('table_map'): + command = 'no {0} {1}'.format(key, existing.get('table_map')) + commands.append(command) + return commands + + +def get_default_table_map_filter(existing): + commands = [] + existing_table_map_filter = existing.get('table_map_filter') + if existing_table_map_filter: + existing_table_map = existing.get('table_map') + if existing_table_map: + command = 'table-map {0}'.format(existing_table_map) + commands.append(command) + return commands + + +def state_present(module, existing, proposed, candidate): + fixed_proposed, commands = fix_proposed(module, proposed, existing) + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, fixed_proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.iteritems(): + if key == 'address-family': + addr_family_command = "address-family {0} {1}".format( + module.params['afi'], module.params['safi']) + if addr_family_command not in commands: + commands.append(addr_family_command) + + elif key.startswith('table-map'): + table_map_commands = get_table_map_command(module, existing, key, value) + if table_map_commands: + commands.extend(table_map_commands) + + elif value is True: + commands.append(key) + + elif value is False: + commands.append('no {0}'.format(key)) + + elif value == 'default': + if key in PARAM_TO_DEFAULT_KEYMAP.keys(): + commands.append('{0} {1}'.format(key, PARAM_TO_DEFAULT_KEYMAP[key])) + + elif existing_commands.get(key): + if key == 'table-map-filter': + default_tmf_command = get_default_table_map_filter(existing) + + if default_tmf_command: + commands.extend(default_tmf_command) + else: + existing_value = existing_commands.get(key) + default_command = default_existing(existing_value, key, value) + if default_command: + commands.extend(default_command) + else: + if key == 'network': + network_commands = get_network_command(existing, key, value) + if network_commands: + commands.extend(network_commands) + + elif key == 'inject-map': + inject_map_commands = get_inject_map_command(existing, key, value) + if inject_map_commands: + commands.extend(inject_map_commands) + + elif key == 'redistribute': + redistribute_commands = get_redistribute_command(existing, key, value) + if redistribute_commands: + commands.extend(redistribute_commands) + + else: + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + parents = ["router bgp {0}".format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + if len(commands) == 1: + candidate.add(commands, parents=parents) + elif len(commands) > 1: + parents.append('address-family {0} {1}'.format(module.params['afi'], + module.params['safi'])) + if addr_family_command in commands: + commands.remove(addr_family_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ["router bgp {0}".format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + commands.append('no address-family {0} {1}'.format( + module.params['afi'], module.params['safi'])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + asn=dict(required=True, type='str'), + vrf=dict(required=False, type='str', default='default'), + safi=dict(required=True, type='str', choices=['unicast','multicast', 'evpn']), + afi=dict(required=True, type='str', choices=['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn']), + additional_paths_install=dict(required=False, type='bool'), + additional_paths_receive=dict(required=False, type='bool'), + additional_paths_selection=dict(required=False, type='str'), + additional_paths_send=dict(required=False, ype='bool'), + advertise_l2vpn_evpn=dict(required=False, type='bool'), + client_to_client=dict(required=False, type='bool'), + dampen_igp_metric=dict(required=False, type='str'), + dampening_state=dict(required=False, type='bool'), + dampening_half_time=dict(required=False, type='str'), + dampening_max_suppress_time=dict(required=False, type='str'), + dampening_reuse_time=dict(required=False, type='str'), + dampening_routemap=dict(required=False, type='str'), + dampening_suppress_time=dict(required=False, type='str'), + default_information_originate=dict(required=False, type='bool'), + default_metric=dict(required=False, type='str'), + distance_ebgp=dict(required=False, type='str'), + distance_ibgp=dict(required=False, type='str'), + distance_local=dict(required=False, type='str'), + inject_map=dict(required=False, type='list'), + maximum_paths=dict(required=False, type='str'), + maximum_paths_ibgp=dict(required=False, type='str'), + networks=dict(required=False, type='list'), + next_hop_route_map=dict(required=False, type='str'), + redistribute=dict(required=False, type='list'), + suppress_inactive=dict(required=False, type='bool'), + table_map=dict(required=False, type='str'), + table_map_filter=dict(required=False, type='bool'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + required_together=[DAMPENING_PARAMS, + ['distance_ibgp', + 'distance_ebgp', + 'distance_local']], + supports_check_mode=True) + + state = module.params['state'] + if module.params['dampening_routemap']: + for param in DAMPENING_PARAMS: + if module.params[param]: + module.fail_json(msg='dampening_routemap cannot be used with' + ' the {0} param'.format(param)) + + if module.params['advertise_l2vpn_evpn']: + if module.params['vrf'] == 'default': + module.fail_json(msg='It is not possible to advertise L2VPN ' + 'EVPN in the default VRF. Please specify ' + 'another one.', vrf=module.params['vrf']) + + if module.params['table_map_filter'] and not module.params['table_map']: + module.fail_json(msg='table_map param is needed when using' + ' table_map_filter filter.') + + args = [ + "additional_paths_install", + "additional_paths_receive", + "additional_paths_selection", + "additional_paths_send", + "advertise_l2vpn_evpn", + "afi", + "asn", + "client_to_client", + "dampen_igp_metric", + "dampening_half_time", + "dampening_max_suppress_time", + "dampening_reuse_time", + "dampening_suppress_time", + "dampening_routemap", + "dampening_state", + "default_information_originate", + "default_metric", + "distance_ebgp", + "distance_ibgp", + "distance_local", + "inject_map", + "maximum_paths", + "maximum_paths_ibgp", + "networks", + "next_hop_route_map", + "redistribute", + "safi", + "suppress_inactive", + "table_map", + "table_map_filter", + "vrf" + ] + + existing = invoke('get_existing', module, args) + + if existing.get('asn'): + if (existing.get('asn') != module.params['asn'] and + state == 'present'): + module.fail_json(msg='Another BGP ASN already exists.', + proposed_asn=module.params['asn'], + existing_asn=existing.get('asn')) + + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + if proposed_args.get('networks'): + if proposed_args['networks'][0] == 'default': + proposed_args['networks'] = 'default' + if proposed_args.get('inject_map'): + if proposed_args['inject_map'][0] == 'default': + proposed_args['inject_map'] = 'default' + + proposed = {} + for key, value in proposed_args.iteritems(): + if key not in ['asn', 'vrf']: + if str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 687c2198d01bfcc7c5115f9d324a19c89e2d4cd1 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 17:35:46 +0200 Subject: [PATCH 062/770] Fixing DOCSTRING --- network/nxos/nxos_bgp_af.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 6acab3655a2..78f85048369 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -169,7 +169,7 @@ inject. Each array entry must first specify the inject-map name, secondly an exist-map name, and optionally the copy-attributes keyword which indicates that attributes should be copied from - the aggregate. Example: [['lax_inject_map', 'lax_exist_map'], + the aggregate. For example [['lax_inject_map', 'lax_exist_map'], ['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'], ['fsd_inject_map', 'fsd_exist_map']] required: false @@ -978,10 +978,6 @@ def load_config(module, candidate): # END OF COMMON CODE WARNINGS = [] -BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 'True', 1, True] -BOOLEANS_FALSE = ['no', 'off', '0', 'false', 'False', 0, False] -BOOLEANS = BOOLEANS_TRUE + BOOLEANS_FALSE -ACCEPTED = BOOLEANS_TRUE + BOOLEANS_FALSE + ['default'] BOOL_PARAMS = [ 'additional_paths_install', 'additional_paths_receive', From 710962824ec74ba3aba1c5b249662ade75d025f2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 17:39:43 +0200 Subject: [PATCH 063/770] Fixing DOCSTRING yaml format --- network/nxos/nxos_bgp_af.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 78f85048369..dbbe3d5e4e0 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -190,7 +190,7 @@ - Networks to configure. Valid value is a list of network prefixes to advertise. The list must be in the form of an array. Each entry in the array must include a prefix address and an - optional route-map. Example: [['10.0.0.0/16', 'routemap_LA'], + optional route-map. For example [['10.0.0.0/16', 'routemap_LA'], ['192.168.1.1', 'Chicago'], ['192.168.2.0/24], ['192.168.3.0/24', 'routemap_NYC']] required: false @@ -209,7 +209,7 @@ redistribute from; the second entry defines a route-map name. A route-map is highly advised but may be optional on some platforms, in which case it may be omitted from the array list. - Example: [['direct', 'rm_direct'], ['lisp', 'rm_lisp']] + For example [['direct', 'rm_direct'], ['lisp', 'rm_lisp']] required: false default: null suppress_inactive: From 0b7038d65bdb07ba7b900c89c70f34c849520707 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 17:43:57 +0200 Subject: [PATCH 064/770] Fixing DOCSTRING yaml format --- network/nxos/nxos_bgp_af.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index dbbe3d5e4e0..a86b4e96064 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -204,7 +204,7 @@ redistribute: description: - A list of redistribute directives. Multiple redistribute entries - are allowed. The list must be in the form of a nested array: + are allowed. The list must be in the form of a nested array. the first entry of each array defines the source-protocol to redistribute from; the second entry defines a route-map name. A route-map is highly advised but may be optional on some From fc527220a8ebf6ce0c8c838169839964c9a60195 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 17:51:57 +0200 Subject: [PATCH 065/770] Adding nxos_bgp_neighbor module --- network/nxos/nxos_bgp_neighbor.py | 1294 +++++++++++++++++++++++++++++ 1 file changed, 1294 insertions(+) create mode 100644 network/nxos/nxos_bgp_neighbor.py diff --git a/network/nxos/nxos_bgp_neighbor.py b/network/nxos/nxos_bgp_neighbor.py new file mode 100644 index 00000000000..db1c1e8821c --- /dev/null +++ b/network/nxos/nxos_bgp_neighbor.py @@ -0,0 +1,1294 @@ +#!/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_bgp_neighbor +version_added: "2.2" +short_description: Manages BGP neighbors configurations +description: + - Manages BGP neighbors configurations on NX-OS switches +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - State 'absent' removes the whole BGP neighbor configuration + - default, where supported, restores params default value +options: + asn: + description: + - BGP autonomous system number. Valid values are String, + Integer in ASPLAIN or ASDOT notation. + required: true + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing + the global bgp. + required: false + default: default + neighbor: + description: + - Neighbor Identifier. Valid values are string. Neighbors may use + IPv4 or IPv6 notation, with or without prefix length. + required: true + description: + description: + - Description of the neighbor. + required: false + default: null + connected_check: + description: + - Configure whether or not to check for directly connected peer. + required: false + choices: ['true', 'false'] + default: null + capability_negotiation: + description: + - Configure whether or not to negotiate capability with + this neighbor. + required: false + choices: ['true', 'false'] + default: null + dynamic_capability: + description: + - Configure whether or not to enable dynamic capability. + required: false + choices: ['true', 'false'] + default: null + ebgp_multihop: + description: + - Specify multihop TTL for a remote peer. Valid values are + integers between 2 and 255, or keyword 'default' to disable + this property. + required: false + default: null + local_as: + description: + - Specify the local-as number for the eBGP neighbor. + Valid values are String or Integer in ASPLAIN or ASDOT notation, + or 'default', which means not to configure it. + required: false + default: null + log_neighbor_changes: + description: + - Specify whether or not to enable log messages for neighbor + up/down event. + required: false + choices: ['enable', 'disable', 'inherit'] + default: null + low_memory_exempt: + description: + - Specify whether or not to shut down this neighbor under + memory pressure. + required: false + choices: ['true', 'false'] + default: null + maximum_peers: + description: + - Specify Maximum number of peers for this neighbor prefix + Valid values are between 1 and 1000, or 'default', which does + not impose the limit. + required: false + default: null + pwd: + description: + - Specify the password for neighbor. Valid value is string. + required: false + default: null + pwd_type: + description: + - Specify the encryption type the password will use. Valid values + are '3des' or 'cisco_type_7' encryption. + required: false + choices: ['3des', 'cisco_type_7'] + default: null + remote_as: + description: + - Specify Autonomous System Number of the neighbor. + Valid values are String or Integer in ASPLAIN or ASDOT notation, + or 'default', which means not to configure it. + required: false + default: null + remove_private_as: + description: + - Specify the config to remove private AS number from outbound + updates. Valid values are 'enable' to enable this config, + 'disable' to disable this config, 'all' to remove all + private AS number, or 'replace-as', to replace the private + AS number. + required: false + choices: ['enable', 'disable', 'all', 'replace-as'] + default: null + shutdown: + description: + - Configure to administratively shutdown this neighbor. + required: false + choices: ['true','false'] + default: null + suppress_4_byte_as: + description: + - Configure to suppress 4-byte AS Capability. + required: false + choices: ['true','false'] + default: null + timers_keepalive: + description: + - Specify keepalive timer value. Valid values are integers + between 0 and 3600 in terms of seconds, or 'default', + which is 60. + required: false + default: null + timers_holdtime: + description: + - Specify holdtime timer value. Valid values are integers between + 0 and 3600 in terms of seconds, or 'default', which is 180. + required: false + default: null + transport_passive_only: + description: + - Specify whether or not to only allow passive connection setup. + Valid values are 'true', 'false', and 'default', which defaults + to 'false'. This property can only be configured when the + neighbor is in 'ip' address format without prefix length. + This property and the transport_passive_mode property are + mutually exclusive. + required: false + choices: ['true','false'] + default: null + update_source: + description: + - Specify source interface of BGP session and updates. + required: false + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +# create a new neighbor +- nxos_bgp_neighbor: + asn: 65535 + neighbor: 3.3.3.3 + local_as: 20 + remote_as: 30 + description: "just a description" + update_source: Ethernet1/3 + shutdown: default + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"asn": "65535", "description": "just a description", + "local_as": "20", "neighbor": "3.3.3.3", + "remote_as": "30", "shutdown": "default", + "update_source": "Ethernet1/3", "vrf": "default"} +existing: + description: k/v pairs of existing BGP neighbor configuration + type: dict + sample: {} +end_state: + description: k/v pairs of BGP neighbor configuration after module execution + returned: always + type: dict + sample: {"asn": "65535", "capability_negotiation": false, + "connected_check": false, "description": "just a description", + "dynamic_capability": true, "ebgp_multihop": "", + "local_as": "20", "log_neighbor_changes": "", + "low_memory_exempt": false, "maximum_peers": "", + "neighbor": "3.3.3.3", "pwd": "", + "pwd_type": "", "remote_as": "30", + "remove_private_as": "disable", "shutdown": false, + "suppress_4_byte_as": false, "timers_holdtime": "180", + "timers_keepalive": "60", "transport_passive_only": false, + "update_source": "Ethernet1/3", "vrf": "default"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "neighbor 3.3.3.3", + "remote-as 30", "update-source Ethernet1/3", + "description just a description", "local-as 20"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +WARNINGS = [] +BOOL_PARAMS = [ + 'capability_negotiation', + 'shutdown', + 'connected_check', + 'dynamic_capability', + 'low_memory_exempt', + 'suppress_4_byte_as', + 'transport_passive_only' +] +PARAM_TO_COMMAND_KEYMAP = { + 'asn': 'router bgp', + 'capability_negotiation': 'dont-capability-negotiate', + 'connected_check': 'disable-connected-check', + 'description': 'description', + 'dynamic_capability': 'dynamic-capability', + 'ebgp_multihop': 'ebgp-multihop', + 'local_as': 'local-as', + 'log_neighbor_changes': 'log-neighbor-changes', + 'low_memory_exempt': 'low-memory exempt', + 'maximum_peers': 'maximum-peers', + 'neighbor': 'neighbor', + 'pwd': 'password', + 'pwd_type': 'password-type', + 'remote_as': 'remote-as', + 'remove_private_as': 'remove-private-as', + 'shutdown': 'shutdown', + 'suppress_4_byte_as': 'capability suppress 4-byte-as', + 'timers_keepalive': 'timers-keepalive', + 'timers_holdtime': 'timers-holdtime', + 'transport_passive_only': 'transport connection-mode passive', + 'update_source': 'update-source', + 'vrf': 'vrf' +} +PARAM_TO_DEFAULT_KEYMAP = { + 'shutdown': False, + 'dynamic_capability': True, + 'timers_keepalive': 60, + 'timers_holdtime': 180 +} + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_custom_value(arg, config, module): + value = '' + splitted_config = config.splitlines() + + if arg == 'log_neighbor_changes': + for line in splitted_config: + if 'log-neighbor-changes' in line: + if 'disable' in line: + value = 'disable' + else: + value = 'enable' + + elif arg == 'pwd': + for line in splitted_config: + if 'password' in line: + splitted_line = line.split() + value = splitted_line[2] + + elif arg == 'pwd_type': + for line in splitted_config: + if 'password' in line: + splitted_line = line.split() + value = splitted_line[1] + + elif arg == 'remove_private_as': + value = 'disable' + for line in splitted_config: + if 'remove-private-as' in line: + splitted_line = line.split() + if len(splitted_line) == 1: + value = 'enable' + elif len(splitted_line) == 2: + value = splitted_line[1] + + elif arg == 'timers_keepalive': + REGEX = re.compile(r'(?:timers\s)(?P.*)$', re.M) + value = '' + if 'timers' in config: + parsed = REGEX.search(config).group('value').split() + value = parsed[0] + + elif arg == 'timers_holdtime': + REGEX = re.compile(r'(?:timers\s)(?P.*)$', re.M) + value = '' + if 'timers' in config: + parsed = REGEX.search(config).group('value').split() + if len(parsed) == 2: + value = parsed[1] + + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + custom = [ + 'log_neighbor_changes', + 'pwd', + 'pwd_type', + 'remove_private_as', + 'timers_holdtime', + 'timers_keepalive' + ] + try: + asn_regex = '.*router\sbgp\s(?P\d+).*' + match_asn = re.match(asn_regex, str(netcfg), re.DOTALL) + existing_asn_group = match_asn.groupdict() + existing_asn = existing_asn_group['existing_asn'] + except AttributeError: + existing_asn = '' + + if existing_asn: + parents = ["router bgp {0}".format(existing_asn)] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + parents.append('neighbor {0}'.format(module.params['neighbor'])) + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg not in ['asn', 'vrf', 'neighbor']: + if arg in custom: + existing[arg] = get_custom_value(arg, config, module) + else: + existing[arg] = get_value(arg, config, module) + + existing['asn'] = existing_asn + existing['neighbor'] = module.params['neighbor'] + existing['vrf'] = module.params['vrf'] + else: + WARNINGS.append("The BGP process didn't exist but the task" + " just created it.") + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + + elif value is False: + commands.append('no {0}'.format(key)) + + elif value == 'default': + if existing_commands.get(key): + existing_value = existing_commands.get(key) + commands.append('no {0} {1}'.format(key, existing_value)) + else: + if key == 'log-neighbor-changes': + if value == 'enable': + commands.append('{0}'.format(key)) + elif value == 'disable': + commands.append('{0} {1}'.format(key, value)) + elif value == 'inherit': + if existing_commands.get(key): + commands.append('no {0}'.format(key)) + elif key == 'password': + pwd_type = module.params['pwd_type'] + if pwd_type == '3des': + pwd_type = 3 + else: + pwd_type = 7 + command = '{0} {1} {2}'.format(key, pwd_type, value) + if command not in commands: + commands.append(command) + elif key == 'remove-private-as': + if value == 'enable': + command = '{0}'.format(key) + commands.append(command) + elif value == 'disable': + if existing_commands.get(key) != 'disable': + command = 'no {0}'.format(key) + commands.append(command) + else: + command = '{0} {1}'.format(key, value) + commands.append(command) + elif key.startswith('timers'): + command = 'timers {0} {1}'.format( + proposed_commands['timers-keepalive'], + proposed_commands['timers-holdtime']) + if command not in commands: + commands.append(command) + else: + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + parents = ["router bgp {0}".format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + parents.append('neighbor {0}'.format(module.params['neighbor'])) + + # make sure that local-as is the last command in the list. + local_as_command = 'local-as {0}'.format(module.params['local_as']) + if local_as_command in commands: + commands.remove(local_as_command) + commands.append(local_as_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ["router bgp {0}".format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + commands.append('no neighbor {0}'.format(module.params['neighbor'])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + asn=dict(required=True, type='str'), + vrf=dict(required=False, type='str', default='default'), + neighbor=dict(required=True, type='str'), + description=dict(required=False, type='str'), + capability_negotiation=dict(required=False, type='bool'), + connected_check=dict(required=False, type='bool'), + dynamic_capability=dict(required=False, type='bool'), + ebgp_multihop=dict(required=False, type='str'), + local_as=dict(required=False, type='str'), + log_neighbor_changes=dict(required=False, type='str', choices=['enable', 'disable', 'inherit']), + low_memory_exempt=dict(required=False, type='bool'), + maximum_peers=dict(required=False, type='str'), + pwd=dict(required=False, type='str'), + pwd_type=dict(required=False, type='str', choices=['cleartext', '3des', 'cisco_type_7', 'default']), + remote_as=dict(required=False, type='str'), + remove_private_as=dict(required=False, type='str', choices=['enable', 'disable', 'all', 'replace-as']), + shutdown=dict(required=False, type='str'), + suppress_4_byte_as=dict(required=False, type='bool'), + timers_keepalive=dict(required=False, type='str'), + timers_holdtime=dict(required=False, type='str'), + transport_passive_only=dict(required=False, type='bool'), + update_source=dict(required=False, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + required_together=[['pwd', 'pwd_type'], + ['timers_holdtime', 'timers_keepalive']], + supports_check_mode=True) + + state = module.params['state'] + if module.params['pwd_type'] == 'default': + module.params['pwd_type'] = '0' + + args = [ + 'asn', + 'capability_negotiation', + 'connected_check', + 'description', + 'dynamic_capability', + 'ebgp_multihop', + 'local_as', + 'log_neighbor_changes', + 'low_memory_exempt', + 'maximum_peers', + 'neighbor', + 'pwd', + 'pwd_type', + 'remote_as', + 'remove_private_as', + 'shutdown', + 'suppress_4_byte_as', + 'timers_keepalive', + 'timers_holdtime', + 'transport_passive_only', + 'update_source', + 'vrf' + ] + + existing = invoke('get_existing', module, args) + if existing.get('asn'): + if (existing.get('asn') != module.params['asn'] and + state == 'present'): + module.fail_json(msg='Another BGP ASN already exists.', + proposed_asn=module.params['asn'], + existing_asn=existing.get('asn')) + + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key not in ['asn', 'vrf', 'neighbor', 'pwd_type']: + if str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From bb9d8c6385e2bbb0d9c9e395622a24ab4a65a5f7 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:07:41 +0200 Subject: [PATCH 066/770] Adding nxos_bgp_neighbor_af module --- network/nxos/nxos_bgp_neighbor_af.py | 1648 ++++++++++++++++++++++++++ 1 file changed, 1648 insertions(+) create mode 100644 network/nxos/nxos_bgp_neighbor_af.py diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py new file mode 100644 index 00000000000..a2eb8746734 --- /dev/null +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -0,0 +1,1648 @@ +#!/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_bgp_neighbor_af +version_added: "2.2" +short_description: Manages BGP address-family's neighbors configuration +description: + - Manages BGP address-family's neighbors configurations on NX-OS switches +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - State 'absent' removes the whole BGP address-family's + neighbor configuration + - default, when supported, removes properties + - to defaults maximum-prefix configuration, only + max_prefix_limit=default is needed +options: + asn: + description: + - BGP autonomous system number. Valid values are String, + Integer in ASPLAIN or ASDOT notation. + required: true + vrf: + description: + - Name of the VRF. The name 'default' is a valid VRF representing + the global bgp. + required: false + default: default + neighbor: + description: + - Neighbor Identifier. Valid values are string. Neighbors may use + IPv4 or IPv6 notation, with or without prefix length. + required: true + afi: + description: + - Address Family Identifie. + required: true + choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn'] + safi: + description: + - Sub Address Family Identifier. + required: true + choices: ['unicast','multicast', 'evpn'] + additional_paths_receive: + description: + - Valid values are enable for basic command enablement; disable + for disabling the command at the neighbor_af level + (it adds the disable keyword to the basic command); and inherit + to remove the command at this level (the command value is + inherited from a higher BGP layer). + required: false + choices: ['enable','disable', 'inherit'] + default: null + additional_paths_send: + description: + - Valid values are enable for basic command enablement; disable + for disabling the command at the neighbor_af level + (it adds the disable keyword to the basic command); and inherit + to remove the command at this level (the command value is + inherited from a higher BGP layer). + required: false + choices: ['enable','disable', 'inherit'] + default: null + advertise_map_exist: + description: + - Conditional route advertisement. This property requires two + route maps: an advertise-map and an exist-map. Valid values are + an array specifying both the advertise-map name and the exist-map + name, or simply 'default' e.g. ['my_advertise_map', + 'my_exist_map']. This command is mutually exclusive with the + advertise_map_non_exist property. + required: false + default: null + advertise_map_non_exist: + description: + - Conditional route advertisement. This property requires two + route maps: an advertise-map and an exist-map. Valid values are + an array specifying both the advertise-map name and the + non-exist-map name, or simply 'default' e.g. + ['my_advertise_map', 'my_non_exist_map']. This command is mutually + exclusive with the advertise_map_exist property. + required: false + default: null + allowas_in: + description: + - Activate allowas-in property + required: false + default: null + allowas_in_max: + description: + - Optional max-occurrences value for allowas_in. Valid values are + an integer value or 'default'. Can be used independently or in + conjunction with allowas_in. + required: false + default: null + as_override: + description: + - Activate the as-override feature. + required: false + choices: ['true', 'false'] + default: null + default_originate: + description: + - Activate the default-originate feature. + required: false + choices: ['true', 'false'] + default: null + default_originate_route_map: + description: + - Optional route-map for the default_originate property. Can be + used independently or in conjunction with default_originate. + Valid values are a string defining a route-map name, + or 'default'. + required: false + default: null + filter_list_in: + description: + - Valid values are a string defining a filter-list name, + or 'default'. + required: false + default: null + filter_list_out: + description: + - Valid values are a string defining a filter-list name, + or 'default'. + required: false + default: null + max_prefix_limit: + description: + - maximum-prefix limit value. Valid values are an integer value + or 'default'. + required: false + default: null + max_prefix_interval: + description: + - Optional restart interval. Valid values are an integer. + Requires max_prefix_limit. + required: false + default: null + max_prefix_threshold: + description: + - Optional threshold percentage at which to generate a warning. + Valid values are an integer value. + Requires max_prefix_limit. + required: false + default: null + max_prefix_warning: + description: + - Optional warning-only keyword. Requires max_prefix_limit. + required: false + choices: ['true','false'] + default: null + next_hop_self: + description: + - Activate the next-hop-self feature. + required: false + choices: ['true','false'] + default: null + next_hop_third_party: + description: + - Activate the next-hop-third-party feature. + required: false + choices: ['true','false'] + default: null + prefix_list_in: + description: + - Valid values are a string defining a prefix-list name, + or 'default'. + required: false + default: null + prefix_list_out: + description: + - Valid values are a string defining a prefix-list name, + or 'default'. + required: false + default: null + route_map_in: + description: + - Valid values are a string defining a route-map name, + or 'default'. + required: false + default: null + route_map_out: + description: + - Valid values are a string defining a route-map name, + or 'default'. + required: false + default: null + route_reflector_client: + description: + - Router reflector client. + required: false + choices: ['true','false'] + default: null + send_community: + description: + - send-community attribute. + required: false + choices: ['none', 'both', 'extended', 'standard', 'default'] + default: null + soft_reconfiguration_in: + description: + - Valid values are 'enable' for basic command enablement; 'always' + to add the always keyword to the basic command; and 'inherit' to + remove the command at this level (the command value is inherited + from a higher BGP layer). + required: false + choices: ['enable','always','inherit'] + default: null + soo: + description: + - Site-of-origin. Valid values are a string defining a VPN + extcommunity or 'default'. + required: false + default: null + suppress_inactive: + description: + - suppress-inactive feature. + required: false + choices: ['true','false','default'] + default: null + unsuppress_map: + description: + - unsuppress-map. Valid values are a string defining a route-map + name or 'default'. + required: false + default: null + weight: + description: + - weight value. Valid values are an integer value or 'default'. + required: false + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +configure RR client +- nxos_bgp_neighbor_af: + asn: 65535 + neighbor: '3.3.3.3' + afi: ipv4 + safi: unicast + route_reflector_client: true + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"afi": "ipv4", "asn": "65535", + "neighbor": "3.3.3.3", "route_reflector_client": true, + "safi": "unicast", "vrf": "default"} +existing: + description: k/v pairs of existing configuration + type: dict + sample: {} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"additional_paths_receive": "inherit", + "additional_paths_send": "inherit", + "advertise_map_exist": [], "advertise_map_non_exist": [], + "afi": "ipv4", "allowas_in": false, + "allowas_in_max": "", "as_override": false, + "asn": "65535", "default_originate": false, + "default_originate_route_map": "", "filter_list_in": "", + "filter_list_out": "", "max_prefix_interval": "", + "max_prefix_limit": "", "max_prefix_threshold": "", + "max_prefix_warning": "", "neighbor": "3.3.3.3", + "next_hop_self": false, "next_hop_third_party": true, + "prefix_list_in": "", "prefix_list_out": "", + "route_map_in": "", "route_map_out": "", + "route_reflector_client": true, "safi": "unicast", + "send_community": "", + "soft_reconfiguration_in": "inherit", "soo": "", + "suppress_inactive": false, "unsuppress_map": "", + "vrf": "default", "weight": ""} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "neighbor 3.3.3.3", + "address-family ipv4 unicast", "route-reflector-client"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +WARNINGS = [] +BOOL_PARAMS = [ + 'allowas_in', + 'as_override', + 'default_originate', + 'next_hop_self', + 'next_hop_third_party', + 'route_reflector_client', + 'suppress_inactive' +] +PARAM_TO_COMMAND_KEYMAP = { + 'afi': 'address-family', + 'asn': 'router bgp', + 'neighbor': 'neighbor', + 'additional_paths_receive': 'capability additional-paths receive', + 'additional_paths_send': 'capability additional-paths send', + 'advertise_map_exist': 'advertise-map exist', + 'advertise_map_non_exist': 'advertise-map non-exist', + 'allowas_in': 'allowas-in', + 'allowas_in_max': 'allowas-in max', + 'as_override': 'as-override', + 'default_originate': 'default-originate', + 'default_originate_route_map': 'default-originate route-map', + 'filter_list_in': 'filter-list in', + 'filter_list_out': 'filter-list out', + 'max_prefix_limit': 'maximum-prefix', + 'max_prefix_interval': 'maximum-prefix options', + 'max_prefix_threshold': 'maximum-prefix options', + 'max_prefix_warning': 'maximum-prefix options', + 'next_hop_self': 'next-hop-self', + 'next_hop_third_party': 'next-hop-third-party', + 'prefix_list_in': 'prefix-list in', + 'prefix_list_out': 'prefix-list out', + 'route_map_in': 'route-map in', + 'route_map_out': 'route-map out', + 'route_reflector_client': 'route-reflector-client', + 'safi': 'address-family', + 'send_community': 'send-community', + 'soft_reconfiguration_in': 'soft-reconfiguration inbound', + 'soo': 'soo', + 'suppress_inactive': 'suppress-inactive', + 'unsuppress_map': 'unsuppress-map', + 'weight': 'weight', + 'vrf': 'vrf' +} +PARAM_TO_DEFAULT_KEYMAP = {} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def in_out_param(arg, config, module): + value = '' + for line in config: + if PARAM_TO_COMMAND_KEYMAP[arg].split()[0] in line: + splitted_line = line.split() + if splitted_line[-1] == PARAM_TO_COMMAND_KEYMAP[arg].split()[1]: + value = splitted_line[1] + return value + + +def get_custom_value(arg, config, module): + splitted_config = config.splitlines() + value = '' + + if (arg.startswith('filter_list') or arg.startswith('prefix_list') or + arg.startswith('route_map')): + value = in_out_param(arg, splitted_config, module) + elif arg == 'send_community': + for line in splitted_config: + if PARAM_TO_COMMAND_KEYMAP[arg] in line: + splitted_line = line.split() + if len(splitted_line) == 1: + value = 'none' + else: + value = splitted_line[1] + elif arg == 'additional_paths_receive': + value = 'inherit' + for line in splitted_config: + if PARAM_TO_COMMAND_KEYMAP[arg] in line: + if 'disable' in line: + value = 'disable' + else: + value = 'enable' + elif arg == 'additional_paths_send': + value = 'inherit' + for line in splitted_config: + if PARAM_TO_COMMAND_KEYMAP[arg] in line: + if 'disable' in line: + value = 'disable' + else: + value = 'enable' + elif arg == 'advertise_map_exist': + value = [] + for line in splitted_config: + if 'advertise-map' in line and 'exist-map' in line: + splitted_line = line.split() + value = [splitted_line[1], splitted_line[3]] + elif arg == 'advertise_map_non_exist': + value = [] + for line in splitted_config: + if 'advertise-map' in line and 'non-exist-map' in line: + splitted_line = line.split() + value = [splitted_line[1], splitted_line[3]] + elif arg == 'allowas_in_max': + for line in splitted_config: + if 'allowas-in' in line: + splitted_line = line.split() + if len(splitted_line) == 2: + value = splitted_line[-1] + elif arg.startswith('max_prefix'): + for line in splitted_config: + if 'maximum-prefix' in line: + splitted_line = line.split() + if arg == 'max_prefix_limit': + value = splitted_line[1] + elif arg == 'max_prefix_interval' and 'restart' in line: + value = splitted_line[-1] + elif arg == 'max_prefix_threshold' and len(splitted_line) > 2: + try: + int(splitted_line[2]) + value = splitted_line[2] + except ValueError: + value = '' + elif arg == 'max_prefix_warning': + if 'warning-only' in line: + value = True + else: + value = False + elif arg == 'soft_reconfiguration_in': + value = 'inherit' + for line in splitted_config: + if PARAM_TO_COMMAND_KEYMAP[arg] in line: + if 'always' in line: + value = 'always' + else: + value = 'enable' + elif arg == 'next_hop_third_party': + PRESENT_REGEX = re.compile(r'\s+{0}\s*'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + ABSENT_REGEX = re.compile(r'\s+no\s+{0}\s*'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if ABSENT_REGEX.search(config): + value = False + elif PRESENT_REGEX.search(config): + value = True + except TypeError: + value = False + + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + + custom = [ + 'allowas_in_max', + 'send_community', + 'additional_paths_send', + 'additional_paths_receive', + 'advertise_map_exist', + 'advertise_map_non_exist', + 'filter_list_in', + 'filter_list_out', + 'max_prefix_limit', + 'max_prefix_interval', + 'max_prefix_threshold', + 'max_prefix_warning', + 'next_hop_third_party', + 'prefix_list_in', + 'prefix_list_out', + 'route_map_in', + 'route_map_out', + 'soft_reconfiguration_in' + ] + try: + asn_regex = '.*router\sbgp\s(?P\d+).*' + match_asn = re.match(asn_regex, str(netcfg), re.DOTALL) + existing_asn_group = match_asn.groupdict() + existing_asn = existing_asn_group['existing_asn'] + except AttributeError: + existing_asn = '' + + if existing_asn: + parents = ["router bgp {0}".format(existing_asn)] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + parents.append('neighbor {0}'.format(module.params['neighbor'])) + parents.append('address-family {0} {1}'.format( + module.params['afi'], module.params['safi'])) + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg not in ['asn', 'vrf', 'neighbor', 'afi', 'safi']: + if arg in custom: + existing[arg] = get_custom_value(arg, config, module) + else: + existing[arg] = get_value(arg, config, module) + + existing['asn'] = existing_asn + existing['neighbor'] = module.params['neighbor'] + existing['vrf'] = module.params['vrf'] + existing['afi'] = module.params['afi'] + existing['safi'] = module.params['safi'] + else: + WARNINGS.append("The BGP process didn't exist but the task" + " just created it.") + + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def get_address_family_command(key, value, module): + command = "address-family {0} {1}".format( + module.params['afi'], module.params['safi']) + return command + + +def get_capability_additional_paths_receive_command(key, value, module): + command = '' + if value == 'enable': + command = key + elif value == 'disable': + command = '{0} {1}'.format(key, value) + return command + + +def get_capability_additional_paths_send_command(key, value, module): + command = '' + if value == 'enable': + command = key + elif value == 'disable': + command = '{0} {1}'.format(key, value) + return command + + +def get_advertise_map_exist_command(key, value, module): + command = 'advertise-map {0} exist-map {1}'.format( + value[0], value[1]) + return command + + +def get_advertise_map_non_exist_command(key, value, module): + command = 'advertise-map {0} non-exist-map {1}'.format( + value[0], value[1]) + return command + + +def get_allowas_in_max_command(key, value, module): + command = 'allowas-in {0}'.format(value) + return command + + +def get_filter_list_in_command(key, value, module): + command = 'filter-list {0} in'.format(value) + return command + + +def get_filter_list_out_command(key, value, module): + command = 'filter-list {0} out'.format(value) + return command + + +def get_prefix_list_in_command(key, value, module): + command = 'prefix-list {0} in'.format(value) + return command + + +def get_prefix_list_out_command(key, value, module): + command = 'prefix-list {0} out'.format(value) + return command + + +def get_route_map_in_command(key, value, module): + command = 'route-map {0} in'.format(value) + return command + + +def get_route_map_out_command(key, value, module): + command = 'route-map {0} out'.format(value) + return command + + +def get_maximum_prefix_command(key, value, module): + return get_maximum_prefix_options_command(key, value, module) + + +def get_maximum_prefix_options_command(key, value, module): + command = 'maximum-prefix {0}'.format(module.params['max_prefix_limit']) + if module.params['max_prefix_threshold']: + command += ' {0}'.format(module.params['max_prefix_threshold']) + if module.params['max_prefix_interval']: + command += ' restart {0}'.format(module.params['max_prefix_interval']) + elif module.params['max_prefix_warning']: + command += ' warning-only' + return command + + +def get_soft_reconfiguration_inbound_command(key, value, module): + command = '' + if value == 'enable': + command = key + elif value == 'always': + command = '{0} {1}'.format(key, value) + return command + + +def get_default_command(key, value, existing_commands): + command = '' + if key == 'send-community' and existing_commands.get(key) == 'none': + command = 'no {0}'.format(key) + + elif existing_commands.get(key): + existing_value = existing_commands.get(key) + if value == 'inherit': + if existing_value != 'inherit': + command = 'no {0}'.format(key) + else: + if key == 'advertise-map exist': + command = 'no advertise-map {0} exist-map {1}'.format( + existing_value[0], existing_value[1]) + elif key == 'advertise-map non-exist': + command = 'no advertise-map {0} non-exist-map {1}'.format( + existing_value[0], existing_value[1]) + elif key == 'filter-list in': + command = 'no filter-list {0} in'.format(existing_value) + elif key == 'filter-list out': + command = 'no filter-list {0} out'.format(existing_value) + elif key == 'prefix-list in': + command = 'no prefix-list {0} in'.format(existing_value) + elif key == 'prefix-list out': + command = 'no prefix-list {0} out'.format(existing_value) + elif key == 'route-map in': + command = 'no route-map {0} in'.format(existing_value) + elif key == 'route-map out': + command = 'no route-map {0} out'.format(existing_value) + elif key.startswith('maximum-prefix'): + command = 'no maximum-prefix {0}'.format( + existing_commands.get('maximum-prefix')) + elif key == 'allowas-in max': + command = ['no allowas-in {0}'.format(existing_value)] + command.append('allowas-in') + else: + command = 'no {0} {1}'.format(key, existing_value) + else: + if key.replace(' ', '_').replace('-', '_') in BOOL_PARAMS: + command = 'no {0}'.format(key) + return command + + +def fix_proposed(module, proposed): + allowas_in = proposed.get('allowas_in') + allowas_in_max = proposed.get('allowas_in_max') + + if allowas_in is False and allowas_in_max: + proposed.pop('allowas_in_max') + elif allowas_in and allowas_in_max: + proposed.pop('allowas_in') + + return proposed + + +def state_present(module, existing, proposed, candidate): + commands = list() + + proposed = fix_proposed(module, proposed) + + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + custom = [ + 'address-family', + 'capability additional-paths receive', + 'capability additional-paths send', + 'advertise-map exist', + 'advertise-map non-exist', + 'allowas-in max', + 'filter-list in', + 'filter-list out', + 'maximum-prefix', + 'maximum-prefix options', + 'prefix-list in', + 'prefix-list out', + 'route-map in', + 'route-map out', + 'soft-reconfiguration inbound' + ] + for key, value in proposed_commands.iteritems(): + if key == 'send-community' and value == 'none': + commands.append('{0}'.format(key)) + + elif value is True and key != 'maximum-prefix options': + commands.append(key) + + elif value is False and key != 'maximum-prefix options': + commands.append('no {0}'.format(key)) + + elif value == 'default' or value == 'inherit': + command = get_default_command(key, value, existing_commands) + + if isinstance(command, str): + if command and command not in commands: + commands.append(command) + elif isinstance(command, list): + for cmd in command: + if cmd not in commands: + commands.append(cmd) + + elif key in custom: + fixed_key = key.replace(' ', '_').replace('-', '_') + command = invoke('get_%s_command' % fixed_key, key, value, module) + if command and command not in commands: + commands.append(command) + else: + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + parents = ["router bgp {0}".format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + parents.append('neighbor {0}'.format(module.params['neighbor'])) + + if len(commands) == 1: + candidate.add(commands, parents=parents) + elif len(commands) > 1: + af_command = 'address-family {0} {1}'.format( + module.params['afi'], module.params['safi']) + if af_command in commands: + commands.remove(af_command) + parents.append('address-family {0} {1}'.format( + module.params['afi'], module.params['safi'])) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ["router bgp {0}".format(module.params['asn'])] + if module.params['vrf'] != 'default': + parents.append('vrf {0}'.format(module.params['vrf'])) + + parents.append('neighbor {0}'.format(module.params['neighbor'])) + commands.append('no address-family {0} {1}'.format( + module.params['afi'], module.params['safi'])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + asn=dict(required=True, type='str'), + vrf=dict(required=False, type='str', default='default'), + neighbor=dict(required=True, type='str'), + afi=dict(required=True, type='str'), + safi=dict(required=True, type='str'), + additional_paths_receive=dict(required=False, type='str', + choices=['enable', 'disable', 'inherit']), + additional_paths_send=dict(required=False, type='str', + choices=['enable', 'disable', 'inherit']), + advertise_map_exist=dict(required=False, type='list'), + advertise_map_non_exist=dict(required=False, type='list'), + allowas_in=dict(required=False, type='bool'), + allowas_in_max=dict(required=False, type='str'), + as_override=dict(required=False, type='bool'), + default_originate=dict(required=False, type='bool'), + default_originate_route_map=dict(required=False, type='str'), + filter_list_in=dict(required=False, type='str'), + filter_list_out=dict(required=False, type='str'), + max_prefix_limit=dict(required=False, type='str'), + max_prefix_interval=dict(required=False, type='str'), + max_prefix_threshold=dict(required=False, type='str'), + max_prefix_warning=dict(required=False, type='bool'), + next_hop_self=dict(required=False, type='bool'), + next_hop_third_party=dict(required=False, type='bool'), + prefix_list_in=dict(required=False, type='str'), + prefix_list_out=dict(required=False, type='str'), + route_map_in=dict(required=False, type='str'), + route_map_out=dict(required=False, type='str'), + route_reflector_client=dict(required=False, type='bool'), + send_community=dict(required=False, choices=['none', + 'both', + 'extended', + 'standard', + 'default']), + soft_reconfiguration_in=dict(required=False, type='str', + choices=['enable', 'always', 'inherit']), + soo=dict(required=False, type='str'), + suppress_inactive=dict(required=False, type='bool'), + unsuppress_map=dict(required=False, type='str'), + weight=dict(required=False, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + mutually_exclusive=[['advertise_map_exist', + 'advertise_map_non_exist']], + supports_check_mode=True) + + state = module.params['state'] + if ((module.params['max_prefix_interval'] or + module.params['max_prefix_warning'] or + module.params['max_prefix_threshold']) and + not module.params['max_prefix_limit']): + module.fail_json(msg='max_prefix_limit is required when using ' + 'max_prefix_warning, max_prefix_limit or ' + 'max_prefix_threshold.') + if module.params['vrf'] == 'default' and module.params['soo']: + module.fail_json(msg='SOO is only allowed in non-default VRF') + + args = [ + 'afi', + 'asn', + 'neighbor', + 'additional_paths_receive', + 'additional_paths_send', + 'advertise_map_exist', + 'advertise_map_non_exist', + 'allowas_in', + 'allowas_in_max', + 'as_override', + 'default_originate', + 'default_originate_route_map', + 'filter_list_in', + 'filter_list_out', + 'max_prefix_limit', + 'max_prefix_interval', + 'max_prefix_threshold', + 'max_prefix_warning', + 'next_hop_self', + 'next_hop_third_party', + 'prefix_list_in', + 'prefix_list_out', + 'route_map_in', + 'route_map_out', + 'soft_reconfiguration_in', + 'soo', + 'suppress_inactive', + 'unsuppress_map', + 'weight', + 'route_reflector_client', + 'safi', + 'send_community', + 'vrf' + ] + + existing = invoke('get_existing', module, args) + if existing.get('asn'): + if (existing.get('asn') != module.params['asn'] and + state == 'present'): + module.fail_json(msg='Another BGP ASN already exists.', + proposed_asn=module.params['asn'], + existing_asn=existing.get('asn')) + + if module.params['advertise_map_exist'] == ['default']: + module.params['advertise_map_exist'] = 'default' + if module.params['advertise_map_non_exist'] == ['default']: + module.params['advertise_map_non_exist'] = 'default' + + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key not in ['asn', 'vrf', 'neighbor']: + if not isinstance(value, list): + if str(value).lower() == 'true': + value = True + elif str(value).lower() == 'false': + value = False + elif str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + if key in BOOL_PARAMS: + value = False + else: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 1f3d7681e2556aaebc11a154f4c037919dcd6301 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:15:30 +0200 Subject: [PATCH 067/770] Fixed DOCSTRING --- network/nxos/nxos_bgp_neighbor_af.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py index a2eb8746734..11b0899dc64 100644 --- a/network/nxos/nxos_bgp_neighbor_af.py +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -81,7 +81,7 @@ advertise_map_exist: description: - Conditional route advertisement. This property requires two - route maps: an advertise-map and an exist-map. Valid values are + route maps, an advertise-map and an exist-map. Valid values are an array specifying both the advertise-map name and the exist-map name, or simply 'default' e.g. ['my_advertise_map', 'my_exist_map']. This command is mutually exclusive with the @@ -91,7 +91,7 @@ advertise_map_non_exist: description: - Conditional route advertisement. This property requires two - route maps: an advertise-map and an exist-map. Valid values are + route maps, an advertise-map and an exist-map. Valid values are an array specifying both the advertise-map name and the non-exist-map name, or simply 'default' e.g. ['my_advertise_map', 'my_non_exist_map']. This command is mutually From 90cd9485eca1e342c3edbe51f02ae03737e1fa9a Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:31:49 +0200 Subject: [PATCH 068/770] Adding nxos_evpn_vni --- network/nxos/nxos_evpn_vni.py | 1018 +++++++++++++++++++++++++++++++++ 1 file changed, 1018 insertions(+) create mode 100644 network/nxos/nxos_evpn_vni.py diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py new file mode 100644 index 00000000000..13cc756e043 --- /dev/null +++ b/network/nxos/nxos_evpn_vni.py @@ -0,0 +1,1018 @@ +#!/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_evpn_vni +version_added: "2.2" +short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI) +description: + - Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network + Identifier (VNI) configurations of a Nexus device. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - default, where supported, restores params default value + - RD override is not permitted. You should set it to the defalt values + first and then reconfigure it. + - route_target_both, route_target_import and route_target_export valid + values are a list of extended communities, (i.e. ['1.2.3.4:5', '33:55']) + or the keywords 'auto' or 'default'. + - The route_target_both property is discouraged due to the inconsistent + behavior of the property across Nexus platforms and image versions. + For this reason it is recommended to use explicit 'route_target_export' + and 'route_target_import' properties instead of route_target_both. + - RD valid values are a String in one of the route-distinguisher formats, + the keyword 'auto', or the keyword 'default'. +options: + vni: + description: + - The EVPN VXLAN Network Identifier. + required: true + default: null + route_distinguisher: + description: + - The VPN Route Distinguisher (RD). The RD is combined with + the IPv4 or IPv6 prefix learned by the PE router to create a + globally unique address. + required: true + default: null + route_target_both: + description: + - Enables/Disables route-target settings for both import and + export target communities using a single property. + required: false + default: null + route_target_import: + description: + - Sets the route-target 'import' extended communities. + required: false + default: null + route_target_export: + description: + - Sets the route-target 'import' extended communities. + required: false + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_evpn_vni: + vni: 6000 + route_distinguisher: "60:10" + route_target_import: + - "5000:10" + - "4100:100" + route_target_export: auto + route_target_both: default + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"route_target_import": ["5000:10", "4100:100", + "5001:10"],"vni": "6000"} +existing: + description: k/v pairs of existing EVPN VNI configuration + type: dict + sample: {"route_distinguisher": "70:10", "route_target_both": [], + "route_target_export": [], "route_target_import": [ + "4100:100", "5000:10"], "vni": "6000"} +end_state: + description: k/v pairs of EVPN VNI configuration after module execution + returned: always + type: dict + sample: {"route_distinguisher": "70:10", "route_target_both": [], + "route_target_export": [], "route_target_import": [ + "4100:100", "5000:10", "5001:10"], "vni": "6000"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["evpn", "vni 6000 l2", "route-target import 5001:10"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +PARAM_TO_COMMAND_KEYMAP = { + 'vni': 'vni', + 'route_target_both': 'route-target both', + 'route_target_import': 'route-target import', + 'route_target_export': 'route-target export', + 'route_distinguisher': 'rd' +} +WARNINGS = [] + +import time + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_route_target_value(arg, config, module): + splitted_config = config.splitlines() + value_list = [] + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + + for line in splitted_config: + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in line.strip(): + value = REGEX.search(line).group('value') + value_list.append(value) + return value_list + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])] + config = netcfg.get_section(parents) + + if config: + for arg in args: + if arg != 'vni': + if arg == 'route_distinguisher': + existing[arg] = get_value(arg, config, module) + else: + existing[arg] = get_route_target_value(arg, config, module) + + existing_fix = dict((k, v) for k, v in existing.iteritems() if v) + if existing_fix: + existing['vni'] = module.params['vni'] + else: + existing = existing_fix + + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed): + commands = list() + parents = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if key.startswith('route-target'): + if value == ['default']: + existing_value = existing_commands.get(key) + + if existing_value: + for target in existing_value: + commands.append('no {0} {1}'.format(key, target)) + else: + if not isinstance(value, list): + value = [value] + for target in value: + if existing: + if target not in existing.get(key.replace('-', '_').replace(' ', '_')): + commands.append('{0} {1}'.format(key, target)) + else: + commands.append('{0} {1}'.format(key, target)) + else: + if value == 'default': + existing_value = existing_commands.get(key) + if existing_value: + commands.append('no {0} {1}'.format(key, existing_value)) + else: + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])] + + return commands, parents + + +def state_absent(module, existing, proposed): + commands = ['no vni {0} l2'.format(module.params['vni'])] + parents = ['evpn'] + return commands, parents + + +def execute_config(module, candidate): + result = {} + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + return result + + +def main(): + argument_spec = dict( + vni=dict(required=True, type='str'), + route_distinguisher=dict(required=False, type='str'), + route_target_both=dict(required=False, type='list'), + route_target_import=dict(required=False, type='list'), + route_target_export=dict(required=False, type='list'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + args = [ + 'vni', + 'route_distinguisher', + 'route_target_both', + 'route_target_import', + 'route_target_export' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'vni': + if value == 'true': + value = True + elif value == 'false': + value = False + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + result = {} + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + commands, parents = invoke('state_%s' % state, module, existing, + proposed) + if commands: + if (existing.get('route_distinguisher') and + proposed.get('route_distinguisher')): + if (existing['route_distinguisher'] != proposed[ + 'route_distinguisher'] and + proposed['route_distinguisher'] != 'default'): + WARNINGS.append('EVPN RD {0} was automatically removed. ' + 'It is highly recommended to use a task ' + '(with default as value) to explicitly ' + 'unconfigure it.'.format( + existing['route_distinguisher'])) + remove_commands = ['no rd {0}'.format( + existing['route_distinguisher'])] + + candidate.add(remove_commands, parents=parents) + result = execute_config(module, candidate) + time.sleep(20) + + candidate = CustomNetworkConfig(indent=3) + candidate.add(commands, parents=parents) + result = execute_config(module, candidate) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 3b266bf1e2fb766a1e830339d32028aac68e1a06 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:42:35 +0200 Subject: [PATCH 069/770] Adding nxos_file_copy --- network/nxos/nxos_file_copy.py | 911 +++++++++++++++++++++++++++++++++ 1 file changed, 911 insertions(+) create mode 100644 network/nxos/nxos_file_copy.py diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py new file mode 100644 index 00000000000..00c5757112b --- /dev/null +++ b/network/nxos/nxos_file_copy.py @@ -0,0 +1,911 @@ +#!/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_file_copy +version_added: "2.2" +short_description: Copy a file to a remote NXOS device over SCP. +description: + - Copy a file to the flash (or bootflash) remote network device on NXOS devices +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - The feature must be enabled with feature scp-server. + - If the file is already present (md5 sums match), no transfer will take place. + - Check mode will tell you if the file would be copied. +options: + local_file: + description: + - Path to local file. Local directory must exist. + required: true + remote_file: + description: + - Remote file path of the copy. Remote directories must exist. + If omitted, the name of the local file will be used. + required: false + default: null + file_system: + description: + - The remote file system of the device. If omitted, + devices that support a file_system parameter will use their default values. + required: false + default: null +''' + +EXAMPLES = ''' +- nxos_file_copy: + local_file: "./test_file.txt" + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +transfer_status: + description: Whether a file was transfered. "No Transfer" or "Sent". + returned: success + type: string + sample: 'Sent' +local_file: + description: The path of the local file. + returned: success + type: string + sample: '/path/to/local/file' +remote_file: + description: The path of the remote file. + returned: success + type: string + sample: '/path/to/remote/file' +''' + + +import os +from scp import SCPClient +import paramiko +import time + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +def execute_show(cmds, module, command_type=None): + 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)) + return response + + +def execute_show_command(command, module, command_type='cli_show'): + if module.params['transport'] == 'cli': + cmds = [command] + body = execute_show(cmds, module) + elif module.params['transport'] == 'nxapi': + cmds = [command] + body = execute_show(cmds, module, command_type=command_type) + + return body + + +def remote_file_exists(module, dst, file_system='bootflash:'): + command = 'dir {0}/{1}'.format(file_system, dst) + body = execute_show_command(command, module, command_type='cli_show_ascii') + if 'No such file' in body[0]: + return False + return True + + +def verify_remote_file_exists(module, dst, file_system='bootflash:'): + command = 'dir {0}/{1}'.format(file_system, dst) + body = execute_show_command(command, module, command_type='cli_show_ascii') + if 'No such file' in body[0]: + return 0 + return body[0].split()[0].strip() + + +def local_file_exists(module): + return os.path.isfile(module.params['local_file']) + + +def get_flash_size(module): + command = 'dir {}'.format(module.params['file_system']) + body = execute_show_command(command, module, command_type='cli_show_ascii') + + match = re.search(r'(\d+) bytes free', body[0]) + bytes_free = match.group(1) + + return int(bytes_free) + + +def enough_space(module): + flash_size = get_flash_size(module) + file_size = os.path.getsize(module.params['local_file']) + if file_size > flash_size: + return False + + return True + + +def transfer_file(module, dest): + file_size = os.path.getsize(module.params['local_file']) + + if not local_file_exists(module): + module.fail_json(msg='Could not transfer file. Local file doesn\'t exist.') + + if not enough_space(module): + module.fail_json(msg='Could not transfer file. Not enough space on device.') + + hostname = module.params['host'] + username = module.params['username'] + password = module.params['password'] + + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect( + hostname=hostname, + username=username, + password=password) + + full_remote_path = '{}{}'.format(module.params['file_system'], dest) + scp = SCPClient(ssh.get_transport()) + try: + scp.put(module.params['local_file'], full_remote_path) + except Exception as e: + time.sleep(10) + temp_size = verify_remote_file_exists( + module, dest, file_system=module.params['file_system']) + if int(temp_size) == int(file_size): + pass + else: + module.fail_json(msg='Could not transfer file. There was an error ' + 'during transfer. Please make sure remote ' + 'permissions are set.', temp_size=temp_size, file_size=file_size) + finally: + scp.close() + + return True + + +def main(): + argument_spec = dict( + local_file=dict(required=True), + remote_file=dict(required=False), + file_system=dict(required=False, default='bootflash:'), + ) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + local_file = module.params['local_file'] + remote_file = module.params['remote_file'] + file_system = module.params['file_system'] + + changed = False + transfer_status = 'No Transfer' + + if not os.path.isfile(local_file): + module.fail_json(msg="Local file {} not found".format(local_file)) + + dest = remote_file or os.path.basename(local_file) + remote_exists = remote_file_exists(module, dest, file_system=file_system) + + if not remote_exists: + changed = True + file_exists = False + else: + file_exists = True + + if not module.check_mode and not file_exists: + try: + transfer_file(module, dest) + transfer_status = 'Sent' + except Exception as e: + module.fail_json(msg=str(e)) + + if remote_file is None: + remote_file = os.path.basename(local_file) + + module.exit_json(changed=changed, + transfer_status=transfer_status, + local_file=local_file, + remote_file=remote_file, + file_system=file_system) + + +if __name__ == '__main__': + main() From 0f606122bcb593ec8e1e6702d5d9a78c76a0c646 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:51:11 +0200 Subject: [PATCH 070/770] Fix error handling --- network/nxos/nxos_file_copy.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index 00c5757112b..555aca5e9d2 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -846,7 +846,7 @@ def transfer_file(module, dest): scp = SCPClient(ssh.get_transport()) try: scp.put(module.params['local_file'], full_remote_path) - except Exception as e: + except: time.sleep(10) temp_size = verify_remote_file_exists( module, dest, file_system=module.params['file_system']) @@ -894,8 +894,9 @@ def main(): try: transfer_file(module, dest) transfer_status = 'Sent' - except Exception as e: - module.fail_json(msg=str(e)) + except ShellError: + clie = get_exception(): + module.fail_json(msg=str(clie)) if remote_file is None: remote_file = os.path.basename(local_file) From 53121eb647e99069a3946d34976369bf62f110dc Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:55:17 +0200 Subject: [PATCH 071/770] Fix try/except --- network/nxos/nxos_file_copy.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index 555aca5e9d2..a59bf2453f2 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -855,10 +855,9 @@ def transfer_file(module, dest): else: module.fail_json(msg='Could not transfer file. There was an error ' 'during transfer. Please make sure remote ' - 'permissions are set.', temp_size=temp_size, file_size=file_size) - finally: - scp.close() - + 'permissions are set.', temp_size=temp_size, + file_size=file_size) + scp.close() return True From d92d1feeaae35bd42648616710f5aaca0c94d386 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 18:58:06 +0200 Subject: [PATCH 072/770] Fix typo --- network/nxos/nxos_file_copy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index a59bf2453f2..8a06a96610f 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -894,7 +894,7 @@ def main(): transfer_file(module, dest) transfer_status = 'Sent' except ShellError: - clie = get_exception(): + clie = get_exception() module.fail_json(msg=str(clie)) if remote_file is None: From ffb613febe090f11dea612f5f77f7d839adf5a93 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 19:01:59 +0200 Subject: [PATCH 073/770] Adding nxos_hsrp --- network/nxos/nxos_hsrp.py | 1189 +++++++++++++++++++++++++++++++++++++ 1 file changed, 1189 insertions(+) create mode 100644 network/nxos/nxos_hsrp.py diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py new file mode 100644 index 00000000000..7967aac7b2c --- /dev/null +++ b/network/nxos/nxos_hsrp.py @@ -0,0 +1,1189 @@ +#!/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_hsrp +version_added: "2.2" +short_description: Manages HSRP configuration on NX-OS switches +description: + - Manages HSRP configuration on NX-OS switches +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - HSRP feature needs to be enabled first on the system + - SVIs must exist before using this module + - Interface must be a L3 port before using this module + - HSRP cannot be configured on loopback interfaces + - MD5 authentication is only possible with HSRPv2 while it is ignored if + HSRPv1 is used instead, while it will not raise any error. Here we allow + MD5 authentication only with HSRPv2 in order to enforce better practice. +options: + group: + description: + - HSRP group number + required: true + interface: + description: + - Full name of interface that is being managed for HSRP + required: true + version: + description: + - HSRP version + required: false + default: 2 + choices: ['1','2'] + priority: + description: + - HSRP priority + required: false + default: null + vip: + description: + - HSRP virtual IP address + required: false + default: null + auth_string: + description: + - Authentication string + required: false + default: null + auth_type: + description: + - Authentication type + required: false + default: null + choices: ['text','md5'] + state: + description: + - Specify desired state of the resource + required: false + choices: ['present','absent'] + default: 'present' +''' + +EXAMPLES = ''' +# ensure hsrp is configured with following params on a SVI +- nxos_hsrp: group=10 vip=10.1.1.1 priority=150 interface=vlan10 preempt=enabled host=68.170.147.165 +# ensure hsrp is configured with following params on a SVI +- nxos_hsrp: group=10 vip=10.1.1.1 priority=150 interface=vlan10 preempt=enabled host=68.170.147.165 auth_type=text auth_string=CISCO +# removing hsrp config for given interface, group, and vip +- nxos_hsrp: group=10 interface=vlan10 vip=10.1.1.1 host=68.170.147.165 state=absent +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"group": "30", "version": "2", "vip": "10.30.1.1"} +existing: + description: k/v pairs of existing hsrp info on the interface + type: dict + sample: {} +end_state: + description: k/v pairs of hsrp after module execution + returned: always + type: dict + sample: {"auth_string": "cisco", "auth_type": "text", + "group": "30", "interface": "vlan10", "preempt": "disabled", + "priority": "100", "version": "2", "vip": "10.30.1.1"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface vlan10", "hsrp version 2", "hsrp 30", "ip 10.30.1.1"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +def execute_config_command(commands, module): + try: + body = module.configure(commands) + except ShellError, clie: + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return body + + +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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + response = response[0].replace(command + '\n\n', '').strip() + body = [json.loads(response)] + except ValueError: + module.fail_json(msg='Command does not support JSON output', + command=command) + return body + + +def execute_show(cmds, module, command_type=None): + 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(command), + 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_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) + interface = {} + mode = 'unknown' + + if intf_type in ['ethernet', 'portchannel']: + body = execute_show_command(command, module)[0] + interface_table = body['TABLE_interface']['ROW_interface'] + mode = str(interface_table.get('eth_mode', 'layer3')) + if mode == 'access' or mode == 'trunk': + mode = 'layer2' + elif intf_type == 'svi': + mode = 'layer3' + return mode + + +def get_hsrp_groups_on_interfaces(device): + command = 'show hsrp all' + body = execute_show_command(command, module) + hsrp = {} + + try: + get_data = body[0]['TABLE_grp_detail']['ROW_grp_detail'] + except (KeyError, AttributeError): + return {} + + for entry in get_data: + interface = str(entry['sh_if_index'].lower()) + value = hsrp.get(interface, 'new') + if value == 'new': + hsrp[interface] = [] + group = str(entry['sh_group_num']) + hsrp[interface].append(group) + + return hsrp + + +def get_hsrp_group(group, interface, module): + command = 'show hsrp group {0}'.format(group) + body = execute_show_command(command, module) + hsrp = {} + + hsrp_key = { + 'sh_if_index': 'interface', + 'sh_group_num': 'group', + 'sh_group_version': 'version', + 'sh_cfg_prio': 'priority', + 'sh_preempt': 'preempt', + 'sh_vip': 'vip', + 'sh_authentication_type': 'auth_type', + 'sh_authentication_data': 'auth_string' + } + + try: + hsrp_table = body[0]['TABLE_grp_detail']['ROW_grp_detail'] + except (AttributeError, IndexError, TypeError): + return {} + + if isinstance(hsrp_table, dict): + hsrp_table = [hsrp_table] + + for hsrp_group in hsrp_table: + parsed_hsrp = apply_key_map(hsrp_key, hsrp_group) + + parsed_hsrp['interface'] = parsed_hsrp['interface'].lower() + + if parsed_hsrp['version'] == 'v1': + parsed_hsrp['version'] = '1' + elif parsed_hsrp['version'] == 'v2': + parsed_hsrp['version'] = '2' + + if parsed_hsrp['interface'] == interface: + return parsed_hsrp + + return hsrp + + +def get_commands_remove_hsrp(group, interface): + commands = [] + commands.append('interface {0}'.format(interface)) + commands.append('no hsrp {0}'.format(group)) + return commands + + +def get_commands_config_hsrp(delta, interface, args): + commands = [] + + config_args = { + 'group': 'hsrp {group}', + 'priority': 'priority {priority}', + 'preempt': '{preempt}', + 'vip': 'ip {vip}' + } + + preempt = delta.get('preempt', None) + group = delta.get('group', None) + if preempt: + if preempt == 'enabled': + delta['preempt'] = 'preempt' + elif preempt == 'disabled': + delta['preempt'] = 'no preempt' + + for key, value in delta.iteritems(): + command = config_args.get(key, 'DNE').format(**delta) + if command and command != 'DNE': + if key == 'group': + commands.insert(0, command) + else: + commands.append(command) + command = None + + auth_type = delta.get('auth_type', None) + auth_string = delta.get('auth_string', None) + if auth_type or auth_string: + if not auth_type: + auth_type = args['auth_type'] + elif not auth_string: + auth_string = args['auth_string'] + if auth_type == 'md5': + command = 'authentication md5 key-string {0}'.format(auth_string) + commands.append(command) + elif auth_type == 'text': + command = 'authentication text {0}'.format(auth_string) + commands.append(command) + + if commands and not group: + commands.insert(0, 'hsrp {0}'.format(args['group'])) + + version = delta.get('version', None) + if version: + if version == '2': + command = 'hsrp version 2' + elif version == '1': + command = 'hsrp version 1' + commands.insert(0, command) + commands.insert(0, 'interface {0}'.format(interface)) + + if commands: + if not commands[0].startswith('interface'): + commands.insert(0, 'interface {0}'.format(interface)) + + return commands + + +def is_default(interface, module): + command = 'show run interface {0}'.format(interface) + + try: + body = execute_show_command(command, module)[0] + if 'invalid' in body.lower(): + return 'DNE' + else: + raw_list = body.split('\n') + if raw_list[-1].startswith('interface'): + return True + else: + return False + except (KeyError): + return 'DNE' + + +def validate_config(body, vip, module): + new_body = ''.join(body) + if "invalid ip address" in new_body.lower(): + module.fail_json(msg="Invalid VIP. Possible duplicate IP address.", + vip=vip) + + +def validate_params(param, module): + value = module.params[param] + version = module.params['version'] + + if param == 'group': + try: + if (int(value) < 0 or int(value) > 255) and version == '1': + raise ValueError + elif int(value) < 0 or int(value) > 4095: + raise ValueError + except ValueError: + module.fail_json(msg="Warning! 'group' must be an integer between" + " 0 and 255 when version 1 and up to 4095 " + "when version 2.", group=value, + version=version) + elif param == 'priority': + try: + if (int(value) < 0 or int(value) > 255): + raise ValueError + except ValueError: + module.fail_json(msg="Warning! 'priority' must be an integer " + "between 0 and 255", priority=value) + + +def main(): + argument_spec = dict( + group=dict(required=True, type='str'), + interface=dict(required=True), + version=dict(choices=['1', '2'], default='2', required=False), + priority=dict(type='str', required=False), + preempt=dict(type='str', choices=['disabled', 'enabled'], + required=False), + vip=dict(type='str', required=False), + auth_type=dict(choices=['text', 'md5'], required=False), + auth_string=dict(type='str', required=False), + state=dict(choices=['absent', 'present'], required=False, + default='present'), + ) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + interface = module.params['interface'].lower() + group = module.params['group'] + version = module.params['version'] + state = module.params['state'] + priority = module.params['priority'] + preempt = module.params['preempt'] + vip = module.params['vip'] + auth_type = module.params['auth_type'] + auth_string = module.params['auth_string'] + + transport = module.params['transport'] + + if state == 'present' and not vip: + module.fail_json(msg='the "vip" param is required when state=present') + + for param in ['group', 'priority']: + if module.params[param] is not None: + validate_params(param, module) + + intf_type = get_interface_type(interface) + if (intf_type != 'ethernet' and transport == 'cli'): + if is_default(interface, module) == 'DNE': + module.fail_json(msg='That interface does not exist yet. Create ' + 'it first.', interface=interface) + if intf_type == 'loopback': + module.fail_json(msg="Loopback interfaces don't support HSRP.", + interface=interface) + + mode = get_interface_mode(interface, intf_type, module) + if mode == 'layer2': + module.fail_json(msg='That interface is a layer2 port.\nMake it ' + 'a layer 3 port first.', interface=interface) + + if auth_type or auth_string: + if not (auth_type and auth_string): + module.fail_json(msg='When using auth parameters, you need BOTH ' + 'auth_type AND auth_string.') + + args = dict(group=group, version=version, priority=priority, + preempt=preempt, vip=vip, auth_type=auth_type, + auth_string=auth_string) + + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + existing = get_hsrp_group(group, interface, module) + + # This will enforce better practice with md5 and hsrp version. + if proposed.get('auth_type', None) == 'md5': + if proposed['version'] == '1': + module.fail_json(msg="It's recommended to use HSRP v2 " + "when auth_type=md5") + + elif not proposed.get('auth_type', None) and existing: + if (proposed['version'] == '1' and + existing['auth_type'] == 'md5'): + module.fail_json(msg="Existing auth_type is md5. It's recommended " + "to use HSRP v2 when using md5") + + changed = False + end_state = existing + commands = [] + if state == 'present': + delta = dict( + set(proposed.iteritems()).difference(existing.iteritems())) + if delta: + command = get_commands_config_hsrp(delta, interface, args) + commands.extend(command) + + elif state == 'absent': + if existing: + command = get_commands_remove_hsrp(group, interface) + commands.extend(command) + + if commands: + if module.check_mode: + module.exit_json(changed=True, commands=commands) + else: + body = execute_config_command(commands, module) + if transport == 'cli': + validate_config(body, vip, module) + changed = True + end_state = get_hsrp_group(group, interface, module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = commands + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From cb9f28c0d648496a01fb7dc1afc8a6cfb6d81e10 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 19:07:38 +0200 Subject: [PATCH 074/770] Fix error handling --- network/nxos/nxos_hsrp.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index 7967aac7b2c..174915cb515 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -799,7 +799,8 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: body = module.configure(commands) - except ShellError, clie: + except ShellError: + clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) return body From 28e3e30d4143feb462d0a1688b580400acbf4693 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 19:20:12 +0200 Subject: [PATCH 075/770] Adding nxos_interface_ospf --- network/nxos/nxos_interface_ospf.py | 1207 +++++++++++++++++++++++++++ 1 file changed, 1207 insertions(+) create mode 100644 network/nxos/nxos_interface_ospf.py diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py new file mode 100644 index 00000000000..fc424e2805f --- /dev/null +++ b/network/nxos/nxos_interface_ospf.py @@ -0,0 +1,1207 @@ +#!/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_interface_ospf +version_added: "2.2" +short_description: Manages configuration of an OSPF interface instance. +description: + - Manages configuration of an OSPF interface instance. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - default, where supported, restores params default value + - To remove an existing authentication configuration you should use + message_digest_key_id=default plus all other options matching their + existing values. + - State absent remove the whole OSPF interface configuration +options: + interface: + description: + - Name of this cisco_interface resource. Valid value is a string. + required: true + ospf: + description: + - Name of the ospf instance. + required: true + area: + description: + - Ospf area associated with this cisco_interface_ospf instance. + Valid values are a string, formatted as an IP address + (i.e. "0.0.0.0") or as an integer. + required: true + cost: + description: + - The cost associated with this cisco_interface_ospf instance. + required: false + default: null + hello_interval: + description: + - Time between sending successive hello packets. + Valid values are an integer or the keyword 'default'. + required: false + default: null + dead_interval: + description: + - Time interval an ospf neighbor waits for a hello + packet before tearing down adjacencies. Valid values are an + integer or the keyword 'default'. + required: false + default: null + passive_interface: + description: + - Setting to true will prevent this interface from receiving + HELLO packets. Valid values are 'true' and 'false'. + required: false + choices: ['true','false'] + default: null + message_digest: + description: + - Enables or disables the usage of message digest authentication. + Valid values are 'true' and 'false'. + required: false + choices: ['true','false'] + default: null + message_digest_key_id: + description: + - md5 authentication key-id associated with the ospf instance. + If this is present, message_digest_encryption_type, + message_digest_algorithm_type and message_digest_password are + mandatory. Valid value is an integer and 'default'. + required: false + default: null + message_digest_algorithm_type: + description: + - Algorithm used for authentication among neighboring routers + within an area. Valid values is 'md5'. + required: false + choices: ['md5'] + default: null + message_digest_encryption_type: + description: + - Specifies the scheme used for encrypting message_digest_password. + Valid values are '3des' or 'cisco_type_7' encryption. + required: false + choices: ['cisco_type_7','3des'] + default: null + message_digest_password: + description: + - Specifies the message_digest password. Valid value is a string. + required: false + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_interface_ospf: + interface=ethernet1/32 + ospf=1 + area=1 + cost=default + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"area": "1", "interface": "ethernet1/32", "ospf": "1"} +existing: + description: k/v pairs of existing OSPF configuration + type: dict + sample: {"area": "", "cost": "", "dead_interval": "", + "hello_interval": "", "interface": "ethernet1/32", + "message_digest": false, "message_digest_algorithm_type": "", + "message_digest_encryption_type": "", + "message_digest_key_id": "", "message_digest_password": "", + "ospf": "", "passive_interface": false} +end_state: + description: k/v pairs of OSPF configuration after module execution + returned: always + type: dict + sample: {"area": "0.0.0.1", "cost": "", "dead_interval": "", + "hello_interval": "", "interface": "ethernet1/32", + "message_digest": false, "message_digest_algorithm_type": "", + "message_digest_encryption_type": "", "message_digest_key_id": "", + "message_digest_password": "", "ospf": "1", + "passive_interface": false} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface Ethernet1/32", "ip router ospf 1 area 0.0.0.1"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +BOOL_PARAMS = [ + 'passive_interface', + 'message_digest' +] +PARAM_TO_COMMAND_KEYMAP = { + 'cost': 'ip ospf cost', + 'ospf': 'ip router ospf', + 'area': 'ip router ospf', + 'hello_interval': 'ip ospf hello-interval', + 'dead_interval': 'ip ospf dead-interval', + 'passive_interface': 'ip ospf passive-interface', + 'message_digest': 'ip ospf authentication message-digest', + 'message_digest_key_id': 'ip ospf message-digest-key', + 'message_digest_algorithm_type': 'ip ospf message-digest-key options', + 'message_digest_encryption_type': 'ip ospf message-digest-key options', + 'message_digest_password': 'ip ospf message-digest-key options', +} +PARAM_TO_DEFAULT_KEYMAP = { +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_custom_value(arg, config, module): + value = '' + if arg == 'ospf': + REGEX = re.compile(r'(?:ip router ospf\s)(?P.*)$', re.M) + value = '' + if 'ip router ospf' in config: + parsed = REGEX.search(config).group('value').split() + value = parsed[0] + + elif arg == 'area': + REGEX = re.compile(r'(?:ip router ospf\s)(?P.*)$', re.M) + value = '' + if 'ip router ospf' in config: + parsed = REGEX.search(config).group('value').split() + value = parsed[2] + + elif arg.startswith('message_digest_'): + REGEX = re.compile(r'(?:ip ospf message-digest-key\s)(?P.*)$', re.M) + value = '' + if 'ip ospf message-digest-key' in config: + value_list = REGEX.search(config).group('value').split() + if arg == 'message_digest_key_id': + value = value_list[0] + elif arg == 'message_digest_algorithm_type': + value = value_list[1] + elif arg == 'message_digest_encryption_type': + value = value_list[2] + if value == '3': + value = '3des' + elif value == '7': + value = 'cisco_type_7' + elif arg == 'message_digest_password': + value = value_list[3] + + elif arg == 'passive_interface': + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + NO_REGEX = re.compile(r'\s+no\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if NO_REGEX.search(config): + value = False + elif REGEX.search(config): + value = True + except TypeError: + value = False + + return value + + +def get_value(arg, config, module): + custom = [ + 'ospf', + 'area', + 'message_digest_key_id', + 'message_digest_algorithm_type', + 'message_digest_encryption_type', + 'message_digest_password', + 'passive_interface' + ] + + if arg in custom: + value = get_custom_value(arg, config, module) + elif arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + parents = ['interface {0}'.format(module.params['interface'].capitalize())] + config = netcfg.get_section(parents) + if 'ospf' in config: + for arg in args: + if arg not in ['interface']: + existing[arg] = get_value(arg, config, module) + existing['interface'] = module.params['interface'] + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def get_default_commands(existing, proposed, existing_commands, key, module): + commands = list() + existing_value = existing_commands.get(key) + if key.startswith('ip ospf message-digest-key'): + check = False + for param in ['message_digest_encryption_type', + 'message_digest_algorithm_type', + 'message_digest_password']: + if existing[param] == proposed[param]: + check = True + if check: + if existing['message_digest_encryption_type'] == '3des': + encryption_type = '3' + elif existing['message_digest_encryption_type'] == 'cisco_type_7': + encryption_type = '7' + command = 'no {0} {1} {2} {3} {4}'.format( + key, + existing['message_digest_key_id'], + existing['message_digest_algorithm_type'], + encryption_type, + existing['message_digest_password']) + commands.append(command) + else: + commands.append('no {0} {1}'.format(key, existing_value)) + return commands + + +def get_custom_command(existing_cmd, proposed, key, module): + commands = list() + + if key == 'ip router ospf': + command = '{0} {1} area {2}'.format(key, proposed['ospf'], + proposed['area']) + if command not in existing_cmd: + commands.append(command) + + elif key.startswith('ip ospf message-digest-key'): + if (proposed['message_digest_key_id'] != 'default' and + 'options' not in key): + if proposed['message_digest_encryption_type'] == '3des': + encryption_type = '3' + elif proposed['message_digest_encryption_type'] == 'cisco_type_7': + encryption_type = '7' + command = '{0} {1} {2} {3} {4}'.format( + key, + proposed['message_digest_key_id'], + proposed['message_digest_algorithm_type'], + encryption_type, + proposed['message_digest_password']) + commands.append(command) + return commands + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + elif value is False: + commands.append('no {0}'.format(key)) + elif value == 'default': + if existing_commands.get(key): + commands.extend(get_default_commands(existing, proposed, + existing_commands, key, + module)) + else: + if (key == 'ip router ospf' or + key.startswith('ip ospf message-digest-key')): + commands.extend(get_custom_command(commands, proposed, + key, module)) + else: + command = '{0} {1}'.format(key, value.lower()) + commands.append(command) + + if commands: + parents = ['interface {0}'.format(module.params['interface'].capitalize())] + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ['interface {0}'.format(module.params['interface'].capitalize())] + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in existing_commands.iteritems(): + if value: + if key.startswith('ip ospf message-digest-key'): + if 'options' not in key: + if existing['message_digest_encryption_type'] == '3des': + encryption_type = '3' + elif existing['message_digest_encryption_type'] == 'cisco_type_7': + encryption_type = '7' + command = 'no {0} {1} {2} {3} {4}'.format( + key, + existing['message_digest_key_id'], + existing['message_digest_algorithm_type'], + encryption_type, + existing['message_digest_password']) + commands.append(command) + elif key in ['ip ospf authentication message-digest', + 'ip ospf passive-interface']: + if value: + commands.append('no {0}'.format(key)) + elif key == 'ip router ospf': + command = 'no {0} {1} area {2}'.format(key, proposed['ospf'], + proposed['area']) + if command not in commands: + commands.append(command) + else: + existing_value = existing_commands.get(key) + commands.append('no {0} {1}'.format(key, existing_value)) + + candidate.add(commands, parents=parents) + + +def normalize_area(area, module): + try: + area = int(area) + area = '0.0.0.{0}'.format(area) + except ValueError: + splitted_area = area.split('.') + if len(splitted_area) != 4: + module.fail_json(msg='Incorrect Area ID format', area=area) + return area + + +def main(): + argument_spec = dict( + interface=dict(required=True, type='str'), + ospf=dict(required=True, type='str'), + area=dict(required=True, type='str'), + cost=dict(required=False, type='str'), + hello_interval=dict(required=False, type='str'), + dead_interval=dict(required=False, type='str'), + passive_interface=dict(required=False, type='bool'), + message_digest=dict(required=False, type='bool'), + message_digest_key_id=dict(required=False, type='str'), + message_digest_algorithm_type=dict(required=False, type='str', + choices=['md5']), + message_digest_encryption_type=dict(required=False, type='str', + choices=['cisco_type_7','3des']), + message_digest_password=dict(required=False, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + required_together=[['message_digest_key_id', + 'message_digest_algorithm_type', + 'message_digest_encryption_type', + 'message_digest_password']], + supports_check_mode=True) + + for param in ['message_digest_encryption_type', + 'message_digest_algorithm_type', + 'message_digest_password']: + if module.params[param] == 'default': + module.exit_json(msg='Use message_digest_key_id=default to remove' + ' an existing authentication configuration') + + state = module.params['state'] + args = [ + 'interface', + 'ospf', + 'area', + 'cost', + 'hello_interval', + 'dead_interval', + 'passive_interface', + 'message_digest', + 'message_digest_key_id', + 'message_digest_algorithm_type', + 'message_digest_encryption_type', + 'message_digest_password' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'interface': + if str(value).lower() == 'true': + value = True + elif str(value).lower() == 'false': + value = False + elif str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + proposed['area'] = normalize_area(proposed['area'], module) + result = {} + if (state == 'present' or (state == 'absent' and + existing.get('ospf') == proposed['ospf'] and + existing.get('area') == proposed['area'])): + + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 42c74a0ce20f9e28ec0813b9cd8deeb30d32b8ac Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 2 Sep 2016 11:25:31 -0700 Subject: [PATCH 076/770] Python3 fixes to copy, file, and stat so that the connection integration tests can be run (#4632) * Python3 fixes to copy, file, and stat so that the connection integration tests can be run * Forgot to audit the helper functions as well. * Fix dest to refledt b_dest (found by @mattclay) --- files/copy.py | 98 ++++++++++++++----------- files/file.py | 196 ++++++++++++++++++++++++++------------------------ files/stat.py | 8 ++- 3 files changed, 163 insertions(+), 139 deletions(-) diff --git a/files/copy.py b/files/copy.py index 9ec8c659525..9066226540a 100644 --- a/files/copy.py +++ b/files/copy.py @@ -18,9 +18,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import os -import tempfile - DOCUMENTATION = ''' --- module: copy @@ -183,16 +180,28 @@ sample: "file" ''' +import os +import shutil +import tempfile +import traceback + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils._text import to_bytes, to_native + + def split_pre_existing_dir(dirname): ''' Return the first pre-existing directory and a list of the new directories that will be created. ''' head, tail = os.path.split(dirname) - if not os.path.exists(head): + b_head = to_bytes(head, errors='surrogate_or_strict') + if not os.path.exists(b_head): (pre_existing_dir, new_directory_list) = split_pre_existing_dir(head) else: - return (head, [ tail ]) + return (head, [tail]) new_directory_list.append(tail) return (pre_existing_dir, new_directory_list) @@ -215,10 +224,10 @@ def main(): module = AnsibleModule( # not checking because of daisy chain to file module argument_spec = dict( - src = dict(required=False), - original_basename = dict(required=False), # used to handle 'dest is a directory' via template, a slight hack + src = dict(required=False, type='path'), + original_basename = dict(required=False), # used to handle 'dest is a directory' via template, a slight hack content = dict(required=False, no_log=True), - dest = dict(required=True), + dest = dict(required=True, type='path'), backup = dict(default=False, type='bool'), force = dict(default=True, aliases=['thirsty'], type='bool'), validate = dict(required=False, type='str'), @@ -229,21 +238,23 @@ def main(): supports_check_mode=True, ) - src = os.path.expanduser(module.params['src']) - dest = os.path.expanduser(module.params['dest']) + src = module.params['src'] + b_src = to_bytes(src, errors='surrogate_or_strict') + dest = module.params['dest'] + b_dest = to_bytes(dest, errors='surrogate_or_strict') backup = module.params['backup'] - force = module.params['force'] - original_basename = module.params.get('original_basename',None) - validate = module.params.get('validate',None) + force = module.params['force'] + original_basename = module.params.get('original_basename', None) + validate = module.params.get('validate', None) follow = module.params['follow'] - mode = module.params['mode'] + mode = module.params['mode'] remote_src = module.params['remote_src'] - if not os.path.exists(src): + if not os.path.exists(b_src): module.fail_json(msg="Source %s not found" % (src)) - if not os.access(src, os.R_OK): + if not os.access(b_src, os.R_OK): module.fail_json(msg="Source %s not readable" % (src)) - if os.path.isdir(src): + if os.path.isdir(b_src): module.fail_json(msg="Remote copy does not support recursive copy of directory: %s" % (src)) checksum_src = module.sha1(src) @@ -259,10 +270,12 @@ def main(): # Special handling for recursive copy - create intermediate dirs if original_basename and dest.endswith(os.sep): dest = os.path.join(dest, original_basename) + b_dest = to_bytes(dest, errors='surrogate_or_strict') dirname = os.path.dirname(dest) - if not os.path.exists(dirname) and os.path.isabs(dirname): + b_dirname = to_bytes(dirname, errors='surrogate_or_strict') + if not os.path.exists(b_dirname) and os.path.isabs(b_dirname): (pre_existing_dir, new_directory_list) = split_pre_existing_dir(dirname) - os.makedirs(dirname) + os.makedirs(b_dirname) directory_args = module.load_file_common_arguments(module.params) directory_mode = module.params["directory_mode"] if directory_mode is not None: @@ -271,45 +284,47 @@ def main(): directory_args['mode'] = None adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed) - if os.path.exists(dest): - if os.path.islink(dest) and follow: - dest = os.path.realpath(dest) + if os.path.exists(b_dest): + if os.path.islink(b_dest) and follow: + b_dest = os.path.realpath(b_dest) + dest = to_native(b_dest, errors='surrogate_or_strict') if not force: module.exit_json(msg="file already exists", src=src, dest=dest, changed=False) - if (os.path.isdir(dest)): + if os.path.isdir(b_dest): basename = os.path.basename(src) if original_basename: basename = original_basename dest = os.path.join(dest, basename) - if os.access(dest, os.R_OK): + b_dest = to_bytes(dest, errors='surrogate_or_strict') + if os.access(b_dest, os.R_OK): checksum_dest = module.sha1(dest) else: - if not os.path.exists(os.path.dirname(dest)): + if not os.path.exists(os.path.dirname(b_dest)): try: # os.path.exists() can return false in some # circumstances where the directory does not have # the execute bit for the current user set, in # which case the stat() call will raise an OSError - os.stat(os.path.dirname(dest)) + os.stat(os.path.dirname(b_dest)) except OSError: e = get_exception() - if "permission denied" in str(e).lower(): + if "permission denied" in to_native(e).lower(): module.fail_json(msg="Destination directory %s is not accessible" % (os.path.dirname(dest))) module.fail_json(msg="Destination directory %s does not exist" % (os.path.dirname(dest))) - if not os.access(os.path.dirname(dest), os.W_OK): + if not os.access(os.path.dirname(b_dest), os.W_OK): module.fail_json(msg="Destination %s not writable" % (os.path.dirname(dest))) backup_file = None - if checksum_src != checksum_dest or os.path.islink(dest): + if checksum_src != checksum_dest or os.path.islink(b_dest): if not module.check_mode: try: if backup: - if os.path.exists(dest): + if os.path.exists(b_dest): backup_file = module.backup_local(dest) # allow for conversion from symlink. - if os.path.islink(dest): - os.unlink(dest) - open(dest, 'w').close() + if os.path.islink(b_dest): + os.unlink(b_dest) + open(b_dest, 'w').close() if validate: # if we have a mode, make sure we set it on the temporary # file source as some validations may require it @@ -318,14 +333,14 @@ def main(): module.set_mode_if_different(src, mode, False) if "%s" not in validate: module.fail_json(msg="validate must contain %%s: %s" % (validate)) - (rc,out,err) = module.run_command(validate % src) + (rc, out, err) = module.run_command(validate % src) if rc != 0: module.fail_json(msg="failed to validate", exit_status=rc, stdout=out, stderr=err) - mysrc = src + b_mysrc = b_src if remote_src: - _, mysrc = tempfile.mkstemp(dir=os.path.dirname(dest)) - shutil.copy2(src, mysrc) - module.atomic_move(mysrc, dest, unsafe_writes=module.params['unsafe_writes']) + _, b_mysrc = tempfile.mkstemp(dir=os.path.dirname(b_dest)) + shutil.copy2(b_src, b_mysrc) + module.atomic_move(b_mysrc, dest, unsafe_writes=module.params['unsafe_writes']) except IOError: module.fail_json(msg="failed to copy: %s to %s" % (src, dest), traceback=traceback.format_exc()) changed = True @@ -333,7 +348,7 @@ def main(): changed = False res_args = dict( - dest = dest, src = src, md5sum = md5sum_src, checksum = checksum_src, changed = changed + dest=dest, src=src, md5sum=md5sum_src, checksum=checksum_src, changed=changed ) if backup_file: res_args['backup_file'] = backup_file @@ -345,6 +360,5 @@ def main(): module.exit_json(**res_args) -# import module snippets -from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/files/file.py b/files/file.py index 11ff6cf85fc..f19a96355b7 100644 --- a/files/file.py +++ b/files/file.py @@ -18,12 +18,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import errno -import shutil -import stat -import grp -import pwd - DOCUMENTATION = ''' --- module: file @@ -37,7 +31,7 @@ notes: - See also M(copy), M(template), M(assemble) requirements: [ ] -author: +author: - "Ansible Core Team" - "Michael DeHaan" options: @@ -112,16 +106,27 @@ ''' +import errno +import os +import shutil +import time + +# import module snippets +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import b +from ansible.module_utils._text import to_bytes, to_native -def get_state(path): + +def get_state(b_path): ''' Find out current state ''' - if os.path.lexists(path): - if os.path.islink(path): + if os.path.lexists(b_path): + if os.path.islink(b_path): return 'link' - elif os.path.isdir(path): + elif os.path.isdir(b_path): return 'directory' - elif os.stat(path).st_nlink > 1: + elif os.stat(b_path).st_nlink > 1: return 'hard' else: # could be many other things, but defaulting to file @@ -129,70 +134,73 @@ def get_state(path): return 'absent' -def recursive_set_attributes(module, path, follow, file_args): + +def recursive_set_attributes(module, b_path, follow, file_args): changed = False - for root, dirs, files in os.walk(path): - for fsobj in dirs + files: - fsname = os.path.join(root, fsobj) - if not os.path.islink(fsname): + for b_root, b_dirs, b_files in os.walk(b_path): + for b_fsobj in b_dirs + b_files: + b_fsname = os.path.join(b_root, b_fsobj) + if not os.path.islink(b_fsname): tmp_file_args = file_args.copy() - tmp_file_args['path']=fsname + tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict') changed |= module.set_fs_attributes_if_different(tmp_file_args, changed) else: tmp_file_args = file_args.copy() - tmp_file_args['path']=fsname + tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict') changed |= module.set_fs_attributes_if_different(tmp_file_args, changed) if follow: - fsname = os.path.join(root, os.readlink(fsname)) - if os.path.isdir(fsname): - changed |= recursive_set_attributes(module, fsname, follow, file_args) + b_fsname = os.path.join(b_root, os.readlink(b_fsname)) + if os.path.isdir(b_fsname): + changed |= recursive_set_attributes(module, b_fsname, follow, file_args) tmp_file_args = file_args.copy() - tmp_file_args['path']=fsname + tmp_file_args['path'] = to_native(b_fsname, errors='surrogate_or_strict') changed |= module.set_fs_attributes_if_different(tmp_file_args, changed) return changed + def main(): module = AnsibleModule( - argument_spec = dict( - state = dict(choices=['file','directory','link','hard','touch','absent'], default=None), - path = dict(aliases=['dest', 'name'], required=True), - original_basename = dict(required=False), # Internal use only, for recursive ops - recurse = dict(default=False, type='bool'), - force = dict(required=False, default=False, type='bool'), - diff_peek = dict(default=None), # Internal use only, for internal checks in the action plugins - validate = dict(required=False, default=None), # Internal use only, for template and copy - src = dict(required=False, default=None), + argument_spec=dict( + state=dict(choices=['file', 'directory', 'link', 'hard', 'touch', 'absent'], default=None), + path=dict(aliases=['dest', 'name'], required=True, type='path'), + original_basename=dict(required=False), # Internal use only, for recursive ops + recurse=dict(default=False, type='bool'), + force=dict(required=False, default=False, type='bool'), + diff_peek=dict(default=None), # Internal use only, for internal checks in the action plugins + validate=dict(required=False, default=None), # Internal use only, for template and copy + src=dict(required=False, default=None, type='path'), ), add_file_common_args=True, supports_check_mode=True ) params = module.params - state = params['state'] + state = params['state'] force = params['force'] diff_peek = params['diff_peek'] src = params['src'] + b_src = to_bytes(src, errors='surrogate_or_strict') follow = params['follow'] # modify source as we later reload and pass, specially relevant when used by other modules. - params['path'] = path = os.path.expanduser(params['path']) + path = params['path'] + b_path = to_bytes(path, errors='surrogate_or_strict') # short-circuit for diff_peek if diff_peek is not None: appears_binary = False try: - f = open(path) - b = f.read(8192) + f = open(b_path, 'rb') + head = f.read(8192) f.close() - if "\x00" in b: + if b("\x00") in head: appears_binary = True except: pass module.exit_json(path=path, changed=False, appears_binary=appears_binary) - prev_state = get_state(path) - + prev_state = get_state(b_path) # state should default to file, but since that creates many conflicts, # default to 'current' when it exists. @@ -204,18 +212,16 @@ def main(): # source is both the source of a symlink or an informational passing of the src for a template module # or copy module, even if this module never uses it, it is needed to key off some things - if src is not None: - src = os.path.expanduser(src) - else: - if state in ['link','hard']: + if src is None: + if state in ('link', 'hard'): if follow and state == 'link': # use the current target of the link as the source - src = os.path.realpath(path) + src = to_native(os.path.realpath(b_path), errors='strict') else: module.fail_json(msg='src and dest are required for creating links') # original_basename is used by other modules that depend on file. - if os.path.isdir(path) and state not in ["link", "absent"]: + if os.path.isdir(b_path) and state not in ("link", "absent"): basename = None if params['original_basename']: basename = params['original_basename'] @@ -223,6 +229,7 @@ def main(): basename = os.path.basename(src) if basename: params['path'] = path = os.path.join(path, basename) + b_path = to_bytes(path, errors='surrogate_or_strict') # make sure the target path is a directory when we're doing a recursive operation recurse = params['recurse'] @@ -232,11 +239,8 @@ def main(): file_args = module.load_file_common_arguments(params) changed = False - diff = {'before': - {'path': path} - , - 'after': - {'path': path} + diff = {'before': {'path': path}, + 'after': {'path': path}, } state_change = False @@ -250,13 +254,13 @@ def main(): if not module.check_mode: if prev_state == 'directory': try: - shutil.rmtree(path, ignore_errors=False) + shutil.rmtree(b_path, ignore_errors=False) except Exception: e = get_exception() module.fail_json(msg="rmtree failed: %s" % str(e)) else: try: - os.unlink(path) + os.unlink(b_path) except Exception: e = get_exception() module.fail_json(path=path, msg="unlinking failed: %s " % str(e)) @@ -269,11 +273,12 @@ def main(): if state_change: if follow and prev_state == 'link': # follow symlink and operate on original - path = os.path.realpath(path) - prev_state = get_state(path) + b_path = os.path.realpath(b_path) + path = to_native(b_path, errors='strict') + prev_state = get_state(b_path) file_args['path'] = path - if prev_state not in ['file','hard']: + if prev_state not in ('file', 'hard'): # file is not absent and any other state is a conflict module.fail_json(path=path, msg='file (%s) is %s, cannot continue' % (path, prev_state)) @@ -282,8 +287,9 @@ def main(): elif state == 'directory': if follow and prev_state == 'link': - path = os.path.realpath(path) - prev_state = get_state(path) + b_path = os.path.realpath(b_path) + path = to_native(b_path, errors='strict') + prev_state = get_state(b_path) if prev_state == 'absent': if module.check_mode: @@ -301,17 +307,18 @@ def main(): # Remove leading slash if we're creating a relative path if not os.path.isabs(path): curpath = curpath.lstrip('/') - if not os.path.exists(curpath): + b_curpath = to_bytes(curpath, errors='surrogate_or_strict') + if not os.path.exists(b_curpath): try: - os.mkdir(curpath) + os.mkdir(b_curpath) except OSError: ex = get_exception() # Possibly something else created the dir since the os.path.exists # check above. As long as it's a dir, we don't need to error out. - if not (ex.errno == errno.EEXIST and os.path.isdir(curpath)): + if not (ex.errno == errno.EEXIST and os.path.isdir(b_curpath)): raise tmp_file_args = file_args.copy() - tmp_file_args['path']=curpath + tmp_file_args['path'] = curpath changed = module.set_fs_attributes_if_different(tmp_file_args, changed, diff) except Exception: e = get_exception() @@ -324,45 +331,47 @@ def main(): changed = module.set_fs_attributes_if_different(file_args, changed, diff) if recurse: - changed |= recursive_set_attributes(module, file_args['path'], follow, file_args) + changed |= recursive_set_attributes(module, to_bytes(file_args['path'], errors='surrogate_or_strict'), follow, file_args) module.exit_json(path=path, changed=changed, diff=diff) - elif state in ['link','hard']: + elif state in ('link', 'hard'): - if os.path.isdir(path) and not os.path.islink(path): + if os.path.isdir(b_path) and not os.path.islink(b_path): relpath = path else: - relpath = os.path.dirname(path) + b_relpath = os.path.dirname(b_path) + relpath = to_native(b_relpath, errors='strict') absrc = os.path.join(relpath, src) - if not os.path.exists(absrc) and not force: + b_absrc = to_bytes(absrc, errors='surrogate_or_strict') + if not os.path.exists(b_absrc) and not force: module.fail_json(path=path, src=src, msg='src file does not exist, use "force=yes" if you really want to create the link: %s' % absrc) if state == 'hard': - if not os.path.isabs(src): + if not os.path.isabs(b_src): module.fail_json(msg="absolute paths are required") elif prev_state == 'directory': if not force: module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path)) - elif len(os.listdir(path)) > 0: + elif len(os.listdir(b_path)) > 0: # refuse to replace a directory that has files in it module.fail_json(path=path, msg='the directory %s is not empty, refusing to convert it' % path) - elif prev_state in ['file', 'hard'] and not force: + elif prev_state in ('file', 'hard') and not force: module.fail_json(path=path, msg='refusing to convert between %s and %s for %s' % (prev_state, state, path)) if prev_state == 'absent': changed = True elif prev_state == 'link': - old_src = os.readlink(path) - if old_src != src: + b_old_src = os.readlink(b_path) + if b_old_src != b_src: changed = True elif prev_state == 'hard': - if not (state == 'hard' and os.stat(path).st_ino == os.stat(src).st_ino): + if not (state == 'hard' and os.stat(b_path).st_ino == os.stat(b_src).st_ino): changed = True if not force: module.fail_json(dest=path, src=src, msg='Cannot link, different hard link exists at destination') - elif prev_state in ['file', 'directory']: + elif prev_state in ('file', 'directory'): changed = True if not force: module.fail_json(dest=path, src=src, msg='Cannot link, %s exists at destination' % prev_state) @@ -372,31 +381,33 @@ def main(): if changed and not module.check_mode: if prev_state != 'absent': # try to replace atomically - tmppath = '/'.join([os.path.dirname(path), ".%s.%s.tmp" % (os.getpid(),time.time())]) + b_tmppath = to_bytes(os.path.sep).join( + [os.path.dirname(b_path), to_bytes(".%s.%s.tmp" % (os.getpid(), time.time()))] + ) try: if prev_state == 'directory' and (state == 'hard' or state == 'link'): - os.rmdir(path) + os.rmdir(b_path) if state == 'hard': - os.link(src,tmppath) + os.link(b_src, b_tmppath) else: - os.symlink(src, tmppath) - os.rename(tmppath, path) + os.symlink(b_src, b_tmppath) + os.rename(b_tmppath, b_path) except OSError: e = get_exception() - if os.path.exists(tmppath): - os.unlink(tmppath) - module.fail_json(path=path, msg='Error while replacing: %s' % str(e)) + if os.path.exists(b_tmppath): + os.unlink(b_tmppath) + module.fail_json(path=path, msg='Error while replacing: %s' % to_native(e, nonstring='simplerepr')) else: try: if state == 'hard': - os.link(src,path) + os.link(b_src, b_path) else: - os.symlink(src, path) + os.symlink(b_src, b_path) except OSError: e = get_exception() - module.fail_json(path=path, msg='Error while linking: %s' % str(e)) + module.fail_json(path=path, msg='Error while linking: %s' % to_native(e, nonstring='simplerepr')) - if module.check_mode and not os.path.exists(path): + if module.check_mode and not os.path.exists(b_path): module.exit_json(dest=path, src=src, changed=changed, diff=diff) changed = module.set_fs_attributes_if_different(file_args, changed, diff) @@ -407,16 +418,16 @@ def main(): if prev_state == 'absent': try: - open(path, 'w').close() + open(b_path, 'wb').close() except OSError: e = get_exception() - module.fail_json(path=path, msg='Error, could not touch target: %s' % str(e)) - elif prev_state in ['file', 'directory', 'hard']: + module.fail_json(path=path, msg='Error, could not touch target: %s' % to_native(e, nonstring='simplerepr')) + elif prev_state in ('file', 'directory', 'hard'): try: - os.utime(path, None) + os.utime(b_path, None) except OSError: e = get_exception() - module.fail_json(path=path, msg='Error while touching existing target: %s' % str(e)) + module.fail_json(path=path, msg='Error while touching existing target: %s' % to_native(e, nonstring='simplerepr')) else: module.fail_json(msg='Cannot touch other than files, directories, and hardlinks (%s is %s)' % (path, prev_state)) try: @@ -428,15 +439,12 @@ def main(): # somewhere in basic.py if prev_state == 'absent': # If we just created the file we can safely remove it - os.remove(path) + os.remove(b_path) raise e module.exit_json(dest=path, changed=True, diff=diff) module.fail_json(path=path, msg='unexpected position reached') -# import module snippets -from ansible.module_utils.basic import * if __name__ == '__main__': main() - diff --git a/files/stat.py b/files/stat.py index f28ddcc0cc1..38ede211643 100644 --- a/files/stat.py +++ b/files/stat.py @@ -328,6 +328,7 @@ # import module snippets from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils._text import to_bytes def format_output(module, path, st, follow, get_md5, get_checksum, @@ -369,7 +370,7 @@ def format_output(module, path, st, follow, get_md5, get_checksum, readable=os.access(path, os.R_OK), writeable=os.access(path, os.W_OK), excutable=os.access(path, os.X_OK), - ) + ) if stat.S_ISLNK(mode): output['lnk_source'] = os.path.realpath(path) @@ -417,6 +418,7 @@ def main(): ) path = module.params.get('path') + b_path = to_bytes(path, errors='surrogate_or_strict') follow = module.params.get('follow') get_mime = module.params.get('mime') get_md5 = module.params.get('get_md5') @@ -425,9 +427,9 @@ def main(): try: if follow: - st = os.stat(path) + st = os.stat(b_path) else: - st = os.lstat(path) + st = os.lstat(b_path) except OSError: e = get_exception() if e.errno == errno.ENOENT: From 9b37dcb5934f425988826237361bed6c70470784 Mon Sep 17 00:00:00 2001 From: Kenny Woodson Date: Fri, 2 Sep 2016 14:54:27 -0400 Subject: [PATCH 077/770] Getting rid of a None type error when no resource tags are defined. (#4638) --- cloud/amazon/ec2_vpc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_vpc.py b/cloud/amazon/ec2_vpc.py index 85efff50eb7..3df47c57b15 100644 --- a/cloud/amazon/ec2_vpc.py +++ b/cloud/amazon/ec2_vpc.py @@ -408,7 +408,7 @@ def create_vpc(module, vpc_conn): for subnet in subnets: add_subnet = True subnet_tags_current = True - new_subnet_tags = subnet.get('resource_tags', None) + new_subnet_tags = subnet.get('resource_tags', {}) subnet_tags_delete = [] for csn in current_subnets: @@ -444,7 +444,7 @@ def create_vpc(module, vpc_conn): if add_subnet: try: new_subnet = vpc_conn.create_subnet(vpc.id, subnet['cidr'], subnet.get('az', None)) - new_subnet_tags = subnet.get('resource_tags', None) + new_subnet_tags = subnet.get('resource_tags', {}) if new_subnet_tags: # Sometimes AWS takes its time to create a subnet and so using new subnets's id # to create tags results in exception. From f7b34c810a27d9c4c38845c828300436e1ea6b37 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Fri, 2 Sep 2016 11:55:03 -0700 Subject: [PATCH 078/770] Declare empty diff dict ahead of time (#4646) Fixes #4634 --- files/replace.py | 1 + 1 file changed, 1 insertion(+) diff --git a/files/replace.py b/files/replace.py index fa7058d70f9..b89e81390b6 100644 --- a/files/replace.py +++ b/files/replace.py @@ -131,6 +131,7 @@ def main(): params = module.params dest = os.path.expanduser(params['dest']) + diff = dict() if os.path.isdir(dest): module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) From 269c06a4c9b5f842e508fbe16d956e6c901896ac Mon Sep 17 00:00:00 2001 From: Kenny Woodson Date: Fri, 2 Sep 2016 15:09:58 -0400 Subject: [PATCH 079/770] Fix for validate rule. Ensure rule is a dict. (#4640) --- cloud/amazon/ec2_group.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cloud/amazon/ec2_group.py b/cloud/amazon/ec2_group.py index 4e05b7219b4..919edfae9b0 100644 --- a/cloud/amazon/ec2_group.py +++ b/cloud/amazon/ec2_group.py @@ -163,6 +163,10 @@ def validate_rule(module, rule): VALID_PARAMS = ('cidr_ip', 'group_id', 'group_name', 'group_desc', 'proto', 'from_port', 'to_port') + + if not isinstance(rule, dict): + module.fail_json(msg='Invalid rule parameter type [%s].' % type(rule)) + for k in rule: if k not in VALID_PARAMS: module.fail_json(msg='Invalid rule parameter \'{}\''.format(k)) From 1b86d838b6201ae8e5cc1acc2b555c4705a8959c Mon Sep 17 00:00:00 2001 From: Scott Butler Date: Fri, 2 Sep 2016 12:26:33 -0700 Subject: [PATCH 080/770] Added semicolon to invalid operators list redux. --- commands/command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/commands/command.py b/commands/command.py index 9892e7c541a..f3229278b41 100644 --- a/commands/command.py +++ b/commands/command.py @@ -28,7 +28,7 @@ - The M(command) module takes the command name followed by a list of space-delimited arguments. - The given command will be executed on all selected nodes. It will not be processed through the shell, so variables like C($HOME) and operations - like C("<"), C(">"), C("|"), and C("&") will not work (use the M(shell) + like C("<"), C(">"), C("|"), C(";") and C("&") will not work (use the M(shell) module if you need these features). options: free_form: From 9cf9e61da4cc3d3bf84b1e23e4d15fb930526c58 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 23:42:31 +0200 Subject: [PATCH 081/770] Addin nxos_overlay_global --- network/nxos/nxos_overlay_global.py | 905 ++++++++++++++++++++++++++++ 1 file changed, 905 insertions(+) create mode 100644 network/nxos/nxos_overlay_global.py diff --git a/network/nxos/nxos_overlay_global.py b/network/nxos/nxos_overlay_global.py new file mode 100644 index 00000000000..48cb4dc1b64 --- /dev/null +++ b/network/nxos/nxos_overlay_global.py @@ -0,0 +1,905 @@ +#!/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_overlay_global +version_added: "2.2" +short_description: Handles the detection of duplicate IP or MAC addresses +description: + - Handles the detection of duplicate IP or MAC addresses based on the + number of moves in a given time-interval (seconds). Also configures + anycast gateway MAC of the switch. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - 'default' restores params default value + - Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE", + "EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE" +options: + anycast_gateway_mac: + description: + - Anycast gateway mac of the switch. + required: true + default: null + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_overlay_global: + anycast_gateway_mac="b.b.b" + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"12:34:56:78:9a:bc"} +existing: + description: k/v pairs of existing configuration + type: dict + sample: {"anycast_gateway_mac": "000E.000E.000E"} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"anycast_gateway_mac": "1234.5678.9ABC"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["fabric forwarding anycast-gateway-mac 1234.5678.9ABC"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +PARAM_TO_COMMAND_KEYMAP = { + 'anycast_gateway_mac': 'fabric forwarding anycast-gateway-mac', +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_existing(module, args): + existing = {} + config = str(custom_get_config(module)) + + for arg in args: + existing[arg] = get_value(arg, config, module) + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def get_commands(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if value == 'default': + existing_value = existing_commands.get(key) + if existing_value: + commands.append('no {0} {1}'.format(key, existing_value)) + else: + if 'anycast-gateway-mac' in key: + value = normalize_mac(value, module) + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + candidate.add(commands, parents=[]) + + +def normalize_mac(proposed_mac, module): + try: + if '-' in proposed_mac: + splitted_mac = proposed_mac.split('-') + if len(splitted_mac) != 6: + raise ValueError + + for octect in splitted_mac: + if len(octect) != 2: + raise ValueError + + elif '.' in proposed_mac: + splitted_mac = [] + splitted_dot_mac = proposed_mac.split('.') + if len(splitted_dot_mac) != 3: + raise ValueError + + for octect in splitted_dot_mac: + if len(octect) > 4: + raise ValueError + else: + octect_len = len(octect) + padding = 4 - octect_len + splitted_mac.append(octect.zfill(padding+1)) + + elif ':' in proposed_mac: + splitted_mac = proposed_mac.split(':') + if len(splitted_mac) != 6: + raise ValueError + + for octect in splitted_mac: + if len(octect) != 2: + raise ValueError + else: + raise ValueError + except ValueError: + module.fail_json(msg='Invalid MAC address format', + proposed_mac=proposed_mac) + + joined_mac = ''.join(splitted_mac) + mac = [joined_mac[i:i+4] for i in range(0, len(joined_mac), 4)] + return '.'.join(mac).upper() + + +def main(): + argument_spec = dict( + anycast_gateway_mac=dict(required=True, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + args = [ + 'anycast_gateway_mac' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + result = {} + candidate = CustomNetworkConfig(indent=3) + invoke('get_commands', module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 7e78c5aad77ed127ba24fd14544b04b2855885b7 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 23:47:19 +0200 Subject: [PATCH 082/770] Fixing module description --- network/nxos/nxos_overlay_global.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_overlay_global.py b/network/nxos/nxos_overlay_global.py index 48cb4dc1b64..6e4f1317547 100644 --- a/network/nxos/nxos_overlay_global.py +++ b/network/nxos/nxos_overlay_global.py @@ -20,15 +20,13 @@ --- module: nxos_overlay_global version_added: "2.2" -short_description: Handles the detection of duplicate IP or MAC addresses +short_description: Configures anycast gateway MAC of the switch. description: - - Handles the detection of duplicate IP or MAC addresses based on the - number of moves in a given time-interval (seconds). Also configures - anycast gateway MAC of the switch. + - Configures anycast gateway MAC of the switch. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - 'default' restores params default value + - default, where supported, restores params default value - Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE", "EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE" options: From 689a117257abb9cbd6a568a4ab916ffd713f2f78 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Fri, 2 Sep 2016 23:59:17 +0200 Subject: [PATCH 083/770] Adding nxos_pim module --- network/nxos/nxos_pim.py | 859 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 859 insertions(+) create mode 100644 network/nxos/nxos_pim.py diff --git a/network/nxos/nxos_pim.py b/network/nxos/nxos_pim.py new file mode 100644 index 00000000000..063d6b5bffe --- /dev/null +++ b/network/nxos/nxos_pim.py @@ -0,0 +1,859 @@ +#!/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 +version_added: "2.2" +short_description: Manages configuration of an Protocol Independent Multicast (PIM) instance. +description: + - Manages configuration of an Protocol Independent Multicast (PIM) instance. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +options: + ssm_range: + description: + - Configure group ranges for Source Specific Multicast (SSM). + Valid values are multicast addresses or the keyword 'none'. + required: true + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_pim: + ssm_range="232.0.0.0/8" + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"ssm_range": "232.0.0.0/8"} +existing: + description: k/v pairs of existing PIM configuration + type: dict + sample: {"ssm_range": none} +end_state: + description: k/v pairs of BGP configuration after module execution + returned: always + type: dict + sample: {"ssm_range": "232.0.0.0/8"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ip pim ssm range 232.0.0.0/8"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +PARAM_TO_COMMAND_KEYMAP = { + 'ssm_range': 'ip pim ssm range' +} +PARAM_TO_DEFAULT_KEYMAP = {} +WARNINGS = [] + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_existing(module, args): + existing = {} + config = str(custom_get_config(module)) + for arg in args: + existing[arg] = get_value(arg, config, module) + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def get_commands(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + command = '{0} {1}'.format(key, value) + commands.append(command) + + if commands: + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + ssm_range=dict(required=True, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + include_defaults=dict(default=False) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + splitted_ssm_range = module.params['ssm_range'].split('.') + if len(splitted_ssm_range) != 4 and module.params['ssm_range'] != 'none': + module.fail_json(msg="Valid ssm_range values are multicast addresses " + "or the keyword 'none'.") + + args = [ + 'ssm_range' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + result = {} + candidate = CustomNetworkConfig(indent=3) + invoke('get_commands', module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From ffa9064c01125246a8eba7d0c7de9c46c03eeffa Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 00:16:58 +0200 Subject: [PATCH 084/770] Adding nxos_pim_rp_address --- network/nxos/nxos_pim_rp_address.py | 941 ++++++++++++++++++++++++++++ 1 file changed, 941 insertions(+) create mode 100644 network/nxos/nxos_pim_rp_address.py diff --git a/network/nxos/nxos_pim_rp_address.py b/network/nxos/nxos_pim_rp_address.py new file mode 100644 index 00000000000..984df580a42 --- /dev/null +++ b/network/nxos/nxos_pim_rp_address.py @@ -0,0 +1,941 @@ +#!/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_rp_address +version_added: "2.2" +short_description: Manages configuration of an Protocol Independent Multicast + (PIM) static rendezvous point (RP) address instance. +description: + - Manages configuration of an Protocol Independent Multicast (PIM) static + rendezvous point (RP) address instance. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - state=absent remove the whole rp-address configuration, if existing. +options: + rp_address: + description: + - Configures a Protocol Independent Multicast (PIM) static + rendezvous point (RP) address. Valid values are + unicast addresses. + required: true + group_list: + description: + - Group range for static RP. Valid values are multicast addresses. + required: false + default: null + prefix_list: + description: + - Prefix list policy for static RP. Valid values are prefix-list + policy names. + required: false + default: null + route_map: + description: + - Route map policy for static RP. Valid values are route-map + policy names. + required: false + default: null + bidir: + description: + - Group range is treated in PIM bidirectional mode. + required: false + choices: ['true','false'] + default: null + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_pim_rp_address: + rp_address: "10.1.1.20" + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"rp_address": "10.1.1.21"} +existing: + description: list of existing pim rp-address configuration entries + type: list + sample: [] +end_state: + description: pim rp-address configuration entries after module execution + returned: always + type: list + sample: [{"bidir": false, "group_list": "224.0.0.0/4", + "rp_address": "10.1.1.21"}] +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +BOOL_PARAMS = ['bidir'] +PARAM_TO_COMMAND_KEYMAP = { + 'rp_address': 'ip pim rp-address' +} +PARAM_TO_DEFAULT_KEYMAP = {} +WARNINGS = [] + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(config, module): + value_list = [] + splitted_config = config.splitlines() + for line in splitted_config: + tmp = {} + if 'ip pim rp-address' in line: + splitted_line = line.split() + tmp['rp_address'] = splitted_line[3] + if len(splitted_line) > 5: + value = splitted_line[5] + if splitted_line[4] == 'route-map': + tmp['route_map'] = value + elif splitted_line[4] == 'prefix-list': + tmp['prefix_list'] = value + elif splitted_line[4] == 'group-list': + tmp['group_list'] = value + if 'bidir' in line: + tmp['bidir'] = True + else: + tmp['bidir'] = False + value_list.append(tmp) + return value_list + + +def get_existing(module, args): + existing = {} + config = str(custom_get_config(module)) + existing = get_value(config, module) + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + command = 'ip pim rp-address {0}'.format(module.params['rp_address']) + commands = build_command(proposed, command) + if commands: + candidate.add(commands, parents=[]) + + +def build_command(param_dict, command): + for param in ['group_list', 'prefix_list', 'route_map']: + if param_dict.get(param): + command += ' {0} {1}'.format( + param.replace('_', '-'), param_dict.get(param)) + if param_dict.get('bidir'): + command += ' bidir' + return [command] + + +def state_absent(module, existing, proposed, candidate): + commands = list() + for each in existing: + if each.get('rp_address') == proposed['rp_address']: + command = 'no ip pim rp-address {0}'.format(proposed['rp_address']) + if each.get('group_list'): + commands = build_command(each, command) + else: + commands = [command] + if commands: + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + rp_address=dict(required=True, type='str'), + group_list=dict(required=False, type='str'), + prefix_list=dict(required=False, type='str'), + route_map=dict(required=False, type='str'), + bidir=dict(required=False, type='bool'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=False) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + mutually_exclusive=[['group_list', 'route_map'], + ['group_list', 'prefix_list'], + ['route_map', 'prefix_list']], + supports_check_mode=True) + + state = module.params['state'] + + args = [ + 'rp_address', + 'group_list', + 'prefix_list', + 'route_map', + 'bidir' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if str(value).lower() == 'true': + value = True + elif str(value).lower() == 'false': + value = False + for each in existing: + if each.get(key) or (not each.get(key) and value): + proposed[key] = value + + result = {} + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From b55a2e49e27f3b0a9d1d5aa62b46e7ec14eee1fc Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 00:29:46 +0200 Subject: [PATCH 085/770] Adding nxos_portchannel --- network/nxos/nxos_portchannel.py | 1237 ++++++++++++++++++++++++++++++ 1 file changed, 1237 insertions(+) create mode 100644 network/nxos/nxos_portchannel.py diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py new file mode 100644 index 00000000000..04e550859b6 --- /dev/null +++ b/network/nxos/nxos_portchannel.py @@ -0,0 +1,1237 @@ +#!/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_portchannel +version_added: "2.2" +short_description: Manages port-channel interfaces +description: + - Manages port-channel specific configuration parameters +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - Absent removes the portchannel config and interface if it + already exists. If members to be removed are not explicitly + passed, all existing members (if any), are removed. + - Members must be a list + - LACP needs to be enabled first if active/passive modes are used +options: + group: + description: + - channel-group number for the port-channel + required: true + mode: + description: + - Mode for the port-channel, i.e. on, active, passive + required: false + default: on + choices: ['active','passive','on'] + min_links: + description: + - min links required to keep portchannel up + required: false + default: null + members: + description: + - List of interfaces that will be managed in a given portchannel + required: false + default: null + force: + description: + - When true it forces port-channel members to match what is + declared in the members param. This can be used to remove + members. + required: false + choices: ['true', 'false'] + default: false + state: + description: + - Manage the state of the resource + required: false + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +# Ensure port-channel99 is created, add two members, and set to mode on +- nxos_portchannel: + group: 99 + members: ['Ethernet1/1','Ethernet1/2'] + mode: 'active' + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"group": "12", "members": ["Ethernet2/5", + "Ethernet2/6"], "mode": "on"} +existing: + description: + - k/v pairs of existing portchannel + type: dict + sample: {"group": "12", "members": ["Ethernet2/5", + "Ethernet2/6"], "members_detail": { + "Ethernet2/5": {"mode": "active", "status": "D"}, + "Ethernet2/6": {"mode": "active", "status": "D"}}, + "min_links": null, "mode": "active"} +end_state: + description: k/v pairs of portchannel info after module execution + returned: always + type: dict + sample: {"group": "12", "members": ["Ethernet2/5", + "Ethernet2/6"], "members_detail": { + "Ethernet2/5": {"mode": "on", "status": "D"}, + "Ethernet2/6": {"mode": "on", "status": "D"}}, + "min_links": null, "mode": "on"} +commands: + description: command string sent to the device + returned: always + type: string + sample: "interface Ethernet2/6 ; no channel-group 12 ; + interface Ethernet2/5 ; no channel-group 12 ; + interface Ethernet2/6 ; channel-group 12 mode on ; + interface Ethernet2/5 ; channel-group 12 mode on ;" +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +WARNINGS = [] +PARAM_TO_COMMAND_KEYMAP = { + 'min_links': 'lacp min-links' +} + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def check_interface(module, netcfg): + config = str(netcfg) + REGEX = re.compile(r'\s+interface port-channel{0}*$'.format(module.params['group']), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + + return value + + +def get_custom_value(arg, config, module): + REGEX = re.compile(r'\s+member vni {0} associate-vrf\s*$'.format( + module.params['vni']), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + return value + + +def execute_config_command(commands, module): + try: + output = module.configure(commands) + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return output + + +def get_cli_body_ssh(command, response, module): + 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): + 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)) + + return response + + +def execute_show_command(command, module, command_type='cli_show'): + if module.params['transport'] == 'cli': + if 'show port-channel summary' in command: + 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 get_portchannel_members(pchannel): + try: + members = pchannel['TABLE_member']['ROW_member'] + except KeyError: + members = [] + + return members + + +def get_portchannel_mode(interface, protocol, module, netcfg): + if protocol != 'LACP': + mode = 'on' + else: + netcfg = custom_get_config(module) + parents = ['interface {0}'.format(interface.capitalize())] + body = netcfg.get_section(parents) + + mode_list = body.split('\n') + + for line in mode_list: + this_line = line.strip() + if this_line.startswith('channel-group'): + find = this_line + if 'mode' in find: + if 'passive' in find: + mode = 'passive' + elif 'active' in find: + mode = 'active' + + return mode + + +def get_portchannel(module, netcfg=None): + command = 'show port-channel summary' + portchannel = {} + portchannel_table = {} + members = [] + + body = execute_show_command(command, module) + + try: + pc_table = body[0]['TABLE_channel']['ROW_channel'] + + if isinstance(pc_table, dict): + pc_table = [pc_table] + + for pc in pc_table: + if pc['group'] == module.params['group']: + portchannel_table = pc + except (KeyError, AttributeError, TypeError, IndexError): + return {} + + if portchannel_table: + portchannel['group'] = portchannel_table['group'] + protocol = portchannel_table['prtcl'] + members_list = get_portchannel_members(portchannel_table) + + if isinstance(members_list, dict): + members_list = [members_list] + + member_dictionary = {} + for each_member in members_list: + interface = each_member['port'] + members.append(interface) + + pc_member = {} + pc_member['status'] = str(each_member['port-status']) + pc_member['mode'] = get_portchannel_mode(interface, + protocol, module, netcfg) + + member_dictionary[interface] = pc_member + portchannel['members'] = members + portchannel['members_detail'] = member_dictionary + + # Ensure each member have the same mode. + modes = set() + for each, value in member_dictionary.iteritems(): + modes.update([value['mode']]) + if len(modes) == 1: + portchannel['mode'] = value['mode'] + else: + portchannel['mode'] = 'unknown' + return portchannel + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + + interface_exist = check_interface(module, netcfg) + if interface_exist: + parents = ['interface port-channel{0}'.format(module.params['group'])] + config = netcfg.get_section(parents) + + if config: + existing['min_links'] = get_value('min_links', config, module) + existing.update(get_portchannel(module, netcfg=netcfg)) + + return existing, interface_exist + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def config_portchannel(proposed, mode, group): + commands = [] + config_args = { + 'mode': 'channel-group {group} mode {mode}', + 'min_links': 'lacp min-links {min_links}', + } + + for member in proposed.get('members', []): + commands.append('interface {0}'.format(member)) + commands.append(config_args.get('mode').format(group=group, mode=mode)) + + min_links = proposed.get('min_links', None) + if min_links: + command = 'interface port-channel {0}'.format(group) + commands.append(command) + commands.append(config_args.get('min_links').format( + min_links=min_links)) + + return commands + + +def get_commands_to_add_members(proposed, existing, module): + try: + proposed_members = proposed['members'] + except KeyError: + proposed_members = [] + + try: + existing_members = existing['members'] + except KeyError: + existing_members = [] + + members_to_add = list(set(proposed_members).difference(existing_members)) + + commands = [] + if members_to_add: + for member in members_to_add: + commands.append('interface {0}'.format(member)) + commands.append('channel-group {0} mode {1}'.format( + existing['group'], proposed['mode'])) + + return commands + + +def get_commands_to_remove_members(proposed, existing, module): + try: + proposed_members = proposed['members'] + except KeyError: + proposed_members = [] + + try: + existing_members = existing['members'] + except KeyError: + existing_members = [] + + members_to_remove = list(set(existing_members).difference(proposed_members)) + commands = [] + if members_to_remove: + for member in members_to_remove: + commands.append('interface {0}'.format(member)) + commands.append('no channel-group {0}'.format(existing['group'])) + + return commands + + +def get_commands_if_mode_change(proposed, existing, group, mode, module): + try: + proposed_members = proposed['members'] + except KeyError: + proposed_members = [] + + try: + existing_members = existing['members'] + except KeyError: + existing_members = [] + + try: + members_dict = existing['members_detail'] + except KeyError: + members_dict = {} + + members_to_remove = set(existing_members).difference(proposed_members) + members_with_mode_change = [] + if members_dict: + for interface, values in members_dict.iteritems(): + if (interface in proposed_members and + (interface not in members_to_remove)): + if values['mode'] != mode: + members_with_mode_change.append(interface) + + commands = [] + if members_with_mode_change: + for member in members_with_mode_change: + commands.append('interface {0}'.format(member)) + commands.append('no channel-group {0}'.format(group)) + + for member in members_with_mode_change: + commands.append('interface {0}'.format(member)) + commands.append('channel-group {0} mode {1}'.format(group, mode)) + + return commands + + +def get_commands_min_links(existing, proposed, group, min_links, module): + commands = [] + try: + if (existing['min_links'] is None or + (existing['min_links'] != proposed['min_links'])): + commands.append('interface port-channel{0}'.format(group)) + commands.append('lacp min-link {0}'.format(min_links)) + except KeyError: + commands.append('interface port-channel{0}'.format(group)) + commands.append('lacp min-link {0}'.format(min_links)) + return commands + + +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 main(): + argument_spec = dict( + group=dict(required=True, type='str'), + mode=dict(required=False, choices=['on', 'active', 'passive'], + default='on', type='str'), + min_links=dict(required=False, default=None, type='str'), + members=dict(required=False, default=None, type='list'), + force=dict(required=False, default='false', type='str', + choices=['true', 'false']), + state=dict(required=False, choices=['absent', 'present'], + default='present'), + include_defaults=dict(default=False) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + group = str(module.params['group']) + mode = module.params['mode'] + min_links = module.params['min_links'] + members = module.params['members'] + state = module.params['state'] + + if str(module.params['force']).lower() == 'true': + force = True + elif module.params['force'] == 'false': + force = False + + if ((min_links or mode) and + (not members and state == 'present')): + module.fail_json(msg='"members" is required when state=present and ' + '"min_links" or "mode" are provided') + + changed = False + args = [ + 'group', + 'members', + 'min_links', + 'mode' + ] + + existing, interface_exist = invoke('get_existing', module, args) + end_state = existing + proposed = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + result = {} + commands = [] + if state == 'absent': + if existing: + commands.append(['no interface port-channel{0}'.format(group)]) + elif state == 'present': + if not interface_exist: + command = config_portchannel(proposed, mode, group) + commands.append(command) + commands.insert(0, 'interface port-channel{0}'.format(group)) + WARNINGS.append("The proposed port-channel interface did not " + "exist. It's recommended to use nxos_interface to " + "create all logical interfaces.") + + elif existing and interface_exist: + if force: + command = get_commands_to_remove_members(proposed, existing, module) + commands.append(command) + + command = get_commands_to_add_members(proposed, existing, module) + commands.append(command) + + mode_command = get_commands_if_mode_change(proposed, existing, + group, mode, module) + + commands.insert(0, mode_command) + + if min_links: + command = get_commands_min_links(existing, proposed, + group, min_links, module) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + output = execute_config_command(cmds, module) + changed = True + end_state, interface_exist = get_existing(module, args) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['state'] = state + results['updates'] = cmds + results['changed'] = changed + + if WARNINGS: + results['warnings'] = WARNINGS + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From 47ce78de3e156bcb8648ffe0bd6090edbc06466f Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 00:47:45 +0200 Subject: [PATCH 086/770] Adding nxos_smu --- network/nxos/nxos_smu.py | 866 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 866 insertions(+) create mode 100644 network/nxos/nxos_smu.py diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py new file mode 100644 index 00000000000..e7584de313f --- /dev/null +++ b/network/nxos/nxos_smu.py @@ -0,0 +1,866 @@ +#!/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_smu +version_added: "2.2" +short_description: Perform SMUs on Cisco NX-OS devices. +description: + - Perform software maintenance upgrades (SMUs) on Cisco NX-OS devices. +extends_documentation_fragment: nxos +author: Gabriele Gerbino (@GGabriele) +notes: + - The module can only activate and commit a package, + not remove or deactivate it. + - Use I(transport)=nxapi to avoid connection timeout +options: + pkg: + description: + - Name of the remote package + required: true + file_system: + description: + - The remote file system of the device. If omitted, + devices that support a file_system parameter will use + their default values. + required: false + default: null +''' + +EXAMPLES = ''' +- nxos_smu: + pkg: "nxos.CSCuz65185-n9k_EOR-1.0.0-7.0.3.I2.2d.lib32_n9000.rpm" + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +file_system: + description: The remote file system of the device. + returned: always + type: string + sample: "bootflash:" +pkg: + description: Name of the remote package + type: string + returned: always + sample: "nxos.CSCuz65185-n9k_EOR-1.0.0-7.0.3.I2.2d.lib32_n9000.rpm" +updates: + description: commands sent to the device + returned: always + type: list + sample: ["install add bootflash:nxos.CSCuz65185-n9k_EOR-1.0.0-7.0.3.I2.2d.lib32_n9000.rpm", + "install activate bootflash:nxos.CSCuz65185-n9k_EOR-1.0.0-7.0.3.I2.2d.lib32_n9000.rpm force", + "install commit bootflash:nxos.CSCuz65185-n9k_EOR-1.0.0-7.0.3.I2.2d.lib32_n9000.rpm"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import time +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +def execute_show(cmds, module, command_type=None): + 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)) + return response + + +def execute_show_command(command, module, command_type='cli_show'): + if module.params['transport'] == 'cli': + cmds = [command] + body = execute_show(cmds, module) + elif module.params['transport'] == 'nxapi': + cmds = [command] + body = execute_show(cmds, module, command_type=command_type) + + return body + + +def remote_file_exists(module, dst, file_system='bootflash:'): + command = 'dir {0}/{1}'.format(file_system, dst) + body = execute_show_command(command, module, command_type='cli_show_ascii') + if 'No such file' in body[0]: + return False + return True + + +def execute_config_command(commands, module): + try: + response = module.configure(commands) + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return response + + +def apply_patch(module, commands): + for command in commands: + response = execute_config_command([command], module) + time.sleep(5) + if 'failed' in response: + module.fail_json(msg="Operation failed!", response=response) + + +def get_commands(module, pkg, file_system): + commands = [] + splitted_pkg = pkg.split('.') + fixed_pkg = '.'.join(splitted_pkg[0:-1]) + + command = 'show install inactive' + inactive_body = execute_show_command(command, module, + command_type='cli_show_ascii') + command = 'show install active' + active_body = execute_show_command(command, module, + command_type='cli_show_ascii') + + if fixed_pkg not in inactive_body[0] and fixed_pkg not in active_body[0]: + commands.append('install add {0}{1}'.format(file_system, pkg)) + + if fixed_pkg not in active_body[0]: + commands.append('install activate {0}{1} force'.format( + file_system, pkg)) + command = 'show install committed' + install_body = execute_show_command(command, module, + command_type='cli_show_ascii') + if fixed_pkg not in install_body[0]: + commands.append('install commit {0}{1}'.format(file_system, pkg)) + + return commands + + +def main(): + argument_spec = dict( + pkg=dict(required=True), + file_system=dict(required=False, default='bootflash:'), + ) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + pkg = module.params['pkg'] + file_system = module.params['file_system'] + changed = False + remote_exists = remote_file_exists(module, pkg, file_system=file_system) + + if not remote_exists: + module.fail_json(msg="The requested package does't exist " + "on the device") + + commands = get_commands(module, pkg, file_system) + if not module.check_mode and commands: + try: + apply_patch(module, commands) + changed=True + except Exception as e: + module.fail_json(msg=str(e)) + + module.exit_json(changed=changed, + pkg=pkg, + file_system=file_system, + updates=commands) + + +if __name__ == '__main__': + main() From c6c0ff42ede8e7566cce32606fc171c2ae6e7855 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 00:52:37 +0200 Subject: [PATCH 087/770] Fix error handling --- network/nxos/nxos_smu.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index e7584de313f..ddab9f0d2b0 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -853,7 +853,8 @@ def main(): try: apply_patch(module, commands) changed=True - except Exception as e: + except ShellError: + e = get_exception() module.fail_json(msg=str(e)) module.exit_json(changed=changed, From 61642cce45f8272fdeb63fabb31290c03556acb8 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 00:59:29 +0200 Subject: [PATCH 088/770] Adding nxos_static_route --- network/nxos/nxos_static_route.py | 995 ++++++++++++++++++++++++++++++ 1 file changed, 995 insertions(+) create mode 100644 network/nxos/nxos_static_route.py diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py new file mode 100644 index 00000000000..b92a483489b --- /dev/null +++ b/network/nxos/nxos_static_route.py @@ -0,0 +1,995 @@ +#!/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_static_route +version_added: "2.2" +short_description: Manages static route configuration +description: + - Manages static route configuration +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - If no vrf is supplied, vrf is set to default + - If state=absent, the route will be removed, regardless of the non-required parameters. +options: + prefix: + description: + - Destination prefix of static route + required: true + next_hop: + description: + - Next hop address or interface of static route. + If interface, it must be the fully-qualified interface name. + required: true + vrf: + description: + - VRF for static route + required: false + default: default + tag: + description: + - Route tag value (numeric). + required: false + default: null + route_name: + description: + - Name of the route. Used with the name parameter on the CLI. + required: false + default: null + pref: + description: + - Preference or administrative difference of route (range 1-255) + required: false + default: null + state: + description: + - Manage the state of the resource + required: true + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' + +EXAMPLES = ''' +- nxos_static_route: + prefix="192.168.20.64/24" + next_hop="3.3.3.3" + route_name=testing + pref=100 + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"next_hop": "3.3.3.3", "pref": "100", + "prefix": "192.168.20.64/24", "route_name": "testing", + "vrf": "default"} +existing: + description: k/v pairs of existing configuration + type: dict + sample: {} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"next_hop": "3.3.3.3", "pref": "100", + "prefix": "192.168.20.0/24", "route_name": "testing", + "tag": null} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ip route 192.168.20.0/24 3.3.3.3 name testing 100"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def state_present(module, candidate, prefix): + commands = list() + invoke('set_route', module, commands, prefix) + if commands: + if module.params['vrf'] == 'default': + candidate.add(commands, parents=[]) + else: + candidate.add(commands, parents=['vrf context {0}'.format(module.params['vrf'])]) + + +def state_absent(module, candidate, prefix): + netcfg = custom_get_config(module) + commands = list() + parents = 'vrf context {0}'.format(module.params['vrf']) + invoke('set_route', module, commands, prefix) + if module.params['vrf'] == 'default': + config = netcfg.get_section(commands[0]) + if config: + invoke('remove_route', module, commands, config, prefix) + candidate.add(commands, parents=[]) + else: + config = netcfg.get_section(parents) + splitted_config = config.split('\n') + splitted_config = map(str.strip, splitted_config) + if commands[0] in splitted_config: + invoke('remove_route', module, commands, config, prefix) + candidate.add(commands, parents=[parents]) + + +def fix_prefix_to_regex(prefix): + prefix = prefix.split('.') + prefix = '\.'.join(prefix) + prefix = prefix.split('/') + prefix = '\/'.join(prefix) + + return prefix + + +def get_existing(module, prefix, warnings): + key_map = ['tag', 'pref', 'route_name', 'next_hop'] + netcfg = custom_get_config(module) + parents = 'vrf context {0}'.format(module.params['vrf']) + prefix_to_regex = fix_prefix_to_regex(prefix) + + route_regex = '.*ip\sroute\s{0}\s(?P\S+)(\sname\s(?P\S+))?(\stag\s(?P\d+))?(\s(?P\d+)).*'.format(prefix_to_regex) + + if module.params['vrf'] == 'default': + config = str(netcfg) + else: + config = netcfg.get_section(parents) + try: + match_route = re.match(route_regex, config, re.DOTALL) + group_route = match_route.groupdict() + + for key in key_map: + if key not in group_route.keys(): + group_route['key'] = None + group_route['prefix'] = prefix + except (AttributeError, TypeError): + group_route = {} + if module.params['state'] == 'present': + msg = ("VRF {0} doesn't exist.".format(module.params['vrf'])) + warnings.append(msg) + + return group_route + + +def remove_route(module, commands, config, prefix): + commands.append('no ip route {0} {1}'.format(prefix, module.params['next_hop'])) + + +def set_route(module, commands, prefix): + route_cmd = 'ip route {0} {1}'.format(prefix, module.params['next_hop']) + + if module.params['route_name']: + route_cmd += ' name {0}'.format(module.params['route_name']) + if module.params['tag']: + route_cmd += ' tag {0}'.format(module.params['tag']) + if module.params['pref']: + route_cmd += ' {0}'.format(module.params['pref']) + commands.append(route_cmd) + + +def get_dotted_mask(mask): + bits = 0 + for i in xrange(32-mask,32): + bits |= (1 << i) + mask = ("%d.%d.%d.%d" % ((bits & 0xff000000) >> 24, + (bits & 0xff0000) >> 16, (bits & 0xff00) >> 8 , (bits & 0xff))) + return mask + + +def get_network_start(address, netmask): + address = address.split('.') + netmask = netmask.split('.') + return [str(int(address[x]) & int(netmask[x])) for x in range(0, 4)] + + +def network_from_string(address, mask, module): + octects = address.split('.') + + if len(octects) > 4: + module.fail_json(msg='Incorrect address format.', address=address) + + for octect in octects: + try: + if int(octect) < 0 or int(octect) > 255: + module.fail_json(msg='Address may contain invalid values.', + address=address) + except ValueError: + module.fail_json(msg='Address may contain non-integer values.', + address=address) + + try: + if int(mask) < 0 or int(mask) > 32: + module.fail_json(msg='Incorrect mask value.', mask=mask) + except ValueError: + module.fail_json(msg='Mask may contain non-integer values.', mask=mask) + + netmask = get_dotted_mask(int(mask)) + return '.'.join(get_network_start(address, netmask)) + + +def normalize_prefix(module, prefix): + splitted_prefix = prefix.split('/') + + if len(splitted_prefix) > 2: + module.fail_json(msg='Incorrect address format.', address=address) + elif len(splitted_prefix) == 2: + address = splitted_prefix[0] + mask = splitted_prefix[1] + network = network_from_string(address, mask, module) + + normalized_prefix = str(network) + '/' + str(mask) + else: + normalized_prefix = prefix + '/' + str(32) + + return normalized_prefix + + +def main(): + argument_spec = dict( + prefix=dict(required=True, type='str'), + next_hop=dict(required=True, type='str'), + vrf=dict(type='str', default='default'), + tag=dict(type='str'), + route_name=dict(type='str'), + pref=dict(type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['absent', 'present'], + default='present'), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + m_facts = module.params['m_facts'] + state = module.params['state'] + + result = dict(changed=False) + warnings = list() + prefix = invoke('normalize_prefix', module, module.params['prefix']) + + existing = invoke('get_existing', module, prefix, warnings) + end_state = existing + + args = ['route_name', 'vrf', 'pref', 'tag', 'next_hop', 'prefix'] + proposed = dict((k, v) for k, v in module.params.iteritems() if v is not None and k in args) + + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, candidate, prefix) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['warnings'] = warnings + result['connected'] = module.connected + + if module.params['m_facts']: + end_state = invoke('get_existing', module, prefix, warnings) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From a7ea96f61eaa5ad79327a7d56f559519b33dc1af Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 01:09:55 +0200 Subject: [PATCH 089/770] Adding nxos_vpc --- network/nxos/nxos_vpc.py | 1145 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1145 insertions(+) create mode 100644 network/nxos/nxos_vpc.py diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py new file mode 100644 index 00000000000..1b1f9b8ad7a --- /dev/null +++ b/network/nxos/nxos_vpc.py @@ -0,0 +1,1145 @@ +#!/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_vpc +version_added: "2.2" +short_description: Manages global VPC configuration +description: + - Manages global VPC configuration +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - The feature vpc must be enabled before this module can be used + - If not using management vrf, vrf must be globally on the device + before using in the pkl config + - Although source IP isn't required on the command line it is + required when using this module. The PKL VRF must also be configured + prior to using this module. + - Both pkl_src and pkl_dest are needed when changing PKL VRF. +options: + domain: + description: + - VPC domain + required: true + role_priority: + description: + - Role priority for device. Remember lower is better. + required: false + default: null + system_priority: + description: + - System priority device. Remember they must match between peers. + required: false + default: null + pkl_src: + description: + - Source IP address used for peer keepalive link + required: false + default: null + pkl_dest: + description: + - Destination (remote) IP address used for peer keepalive link + required: false + default: null + pkl_vrf: + description: + - VRF used for peer keepalive link + required: false + default: management + peer_gw: + description: + - Enables/Disables peer gateway + required: true + choices: ['true','false'] + auto_recovery: + description: + - Enables/Disables auto recovery + required: true + choices: ['true','false'] + delay_restore: + description: + - manages delay restore command and config value in seconds + required: false + default: null + state: + description: + - Manages desired state of the resource + required: true + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# configure a simple asn +- nxos_vpc: + domain=100 + role_priority=1000 + system_priority=2000 + pkl_dest=192.168.100.4 + pkl_src=10.1.100.20 + peer_gw=true + auto_recovery=true + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"auto_recovery": true, "domain": "100", + "peer_gw": true, "pkl_dest": "192.168.100.4", + "pkl_src": "10.1.100.20", "pkl_vrf": "management", + "role_priority": "1000", "system_priority": "2000"} +existing: + description: k/v pairs of existing VPC configuration + type: dict + sample: {"auto_recovery": true, "delay_restore": null, + "domain": "100", "peer_gw": true, + "pkl_dest": "192.168.100.2", "pkl_src": "10.1.100.20", + "pkl_vrf": "management", "role_priority": "1000", + "system_priority": "2000"} +end_state: + description: k/v pairs of VPC configuration after module execution + returned: always + type: dict + sample: {"auto_recovery": true, "domain": "100", + "peer_gw": true, "pkl_dest": "192.168.100.4", + "pkl_src": "10.1.100.20", "pkl_vrf": "management", + "role_priority": "1000", "system_priority": "2000"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["vpc domain 100", + "peer-keepalive destination 192.168.100.4 source 10.1.100.20 vrf management", + "auto-recovery", "peer-gateway"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_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) + + +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. + """ + if '^' == response[0]: + body = [] + elif 'running' in command: + body = response + else: + if command in response[0]: + response = [response[0].split(command)[1]] + 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): + 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)) + return response + + +def execute_show_command(command, module, command_type='cli_show'): + if module.params['transport'] == 'cli': + if "section" not in command: + 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 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 get_vrf_list(module): + command = 'show vrf all' + vrf_table = None + + body = execute_show_command(command, module) + + try: + vrf_table = body[0]['TABLE_vrf']['ROW_vrf'] + except (KeyError, AttributeError): + return [] + + vrf_list = [] + if vrf_table: + for each in vrf_table: + vrf_list.append(str(each['vrf_name'].lower())) + + return vrf_list + + +def get_autorecovery(auto): + auto_recovery = auto.split(' ')[0] + if 'enabled' in auto_recovery.lower(): + return True + else: + return False + + +def get_vpc_running_config(module): + command = 'show running section vpc' + body = execute_show_command(command, module, command_type='cli_show_ascii') + + return body + + +def get_vpc(module): + vpc = {} + + command = 'show vpc' + body = execute_show_command(command, module)[0] + domain = str(body['vpc-domain-id']) + auto_recovery = get_autorecovery(str( + body['vpc-auto-recovery-status'])) + + if domain != 'not configured': + delay_restore = None + pkl_src = None + role_priority = None + system_priority = None + pkl_dest = None + pkl_vrf = None + peer_gw = False + + run = get_vpc_running_config(module)[0] + if run: + vpc_list = run.split('\n') + for each in vpc_list: + if 'delay restore' in each: + line = each.split() + if len(line) == 5: + delay_restore = line[-1] + if 'peer-keepalive destination' in each: + line = each.split() + pkl_dest = line[2] + for word in line: + if 'source' in word: + index = line.index(word) + pkl_src = line[index + 1] + if 'role priority' in each: + line = each.split() + role_priority = line[-1] + if 'system-priority' in each: + line = each.split() + system_priority = line[-1] + if 'peer-gateway' in each: + peer_gw = True + + + command = 'show vpc peer-keepalive' + body = execute_show_command(command, module)[0] + + if body: + pkl_dest = body['vpc-keepalive-dest'] + if 'N/A' in pkl_dest: + pkl_dest = None + elif len(pkl_dest) == 2: + pkl_dest = pkl_dest[0] + pkl_vrf = str(body['vpc-keepalive-vrf']) + + vpc['domain'] = domain + vpc['auto_recovery'] = auto_recovery + vpc['delay_restore'] = delay_restore + vpc['pkl_src'] = pkl_src + vpc['role_priority'] = role_priority + vpc['system_priority'] = system_priority + vpc['pkl_dest'] = pkl_dest + vpc['pkl_vrf'] = pkl_vrf + vpc['peer_gw'] = peer_gw + else: + vpc = {} + + return vpc + + +def get_commands_to_config_vpc(module, vpc, domain, existing): + vpc = dict(vpc) + + domain_only = vpc.get('domain') + pkl_src = vpc.get('pkl_src') + pkl_dest = vpc.get('pkl_dest') + pkl_vrf = vpc.get('pkl_vrf') or existing.get('pkl_vrf') + vpc['pkl_vrf'] = pkl_vrf + + commands = [] + if pkl_src or pkl_dest: + if pkl_src is None: + vpc['pkl_src'] = existing.get('pkl_src') + elif pkl_dest is None: + vpc['pkl_dest'] = existing.get('pkl_dest') + pkl_command = 'peer-keepalive destination {pkl_dest}'.format(**vpc) \ + + ' source {pkl_src} vrf {pkl_vrf}'.format(**vpc) + commands.append(pkl_command) + elif pkl_vrf: + pkl_src = existing.get('pkl_src') + pkl_dest = existing.get('pkl_dest') + if pkl_src and pkl_dest: + pkl_command = ('peer-keepalive destination {0}' + ' source {1} vrf {2}'.format(pkl_dest, pkl_src, pkl_vrf)) + commands.append(pkl_command) + + if vpc.get('auto_recovery') == False: + vpc['auto_recovery'] = 'no' + else: + vpc['auto_recovery'] = '' + + if vpc.get('peer_gw') == False: + vpc['peer_gw'] = 'no' + else: + vpc['peer_gw'] = '' + + CONFIG_ARGS = { + 'role_priority': 'role priority {role_priority}', + 'system_priority': 'system-priority {system_priority}', + 'delay_restore': 'delay restore {delay_restore}', + 'peer_gw': '{peer_gw} peer-gateway', + 'auto_recovery': '{auto_recovery} auto-recovery', + } + + for param, value in vpc.iteritems(): + command = CONFIG_ARGS.get(param, 'DNE').format(**vpc) + if command and command != 'DNE': + commands.append(command.strip()) + command = None + + if commands or domain_only: + commands.insert(0, 'vpc domain {0}'.format(domain)) + return commands + + +def get_commands_to_remove_vpc_interface(portchannel, config_value): + commands = [] + command = 'no vpc {0}'.format(config_value) + commands.append(command) + commands.insert(0, 'interface port-channel{0}'.format(portchannel)) + return commands + + +def main(): + argument_spec = dict( + domain=dict(required=True, type='str'), + role_priority=dict(required=False, type='str'), + system_priority=dict(required=False, type='str'), + pkl_src=dict(required=False), + pkl_dest=dict(required=False), + pkl_vrf=dict(required=False, default='management'), + peer_gw=dict(required=True, type='bool'), + auto_recovery=dict(required=True, type='bool'), + delay_restore=dict(required=False, type='str'), + state=dict(choices=['absent', 'present'], default='present'), + include_defaults=dict(default=False) + ) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + domain = module.params['domain'] + role_priority = module.params['role_priority'] + system_priority = module.params['system_priority'] + pkl_src = module.params['pkl_src'] + pkl_dest = module.params['pkl_dest'] + pkl_vrf = module.params['pkl_vrf'] + peer_gw = module.params['peer_gw'] + auto_recovery = module.params['auto_recovery'] + delay_restore = module.params['delay_restore'] + state = module.params['state'] + + args = dict(domain=domain, role_priority=role_priority, + system_priority=system_priority, pkl_src=pkl_src, + pkl_dest=pkl_dest, pkl_vrf=pkl_vrf, peer_gw=peer_gw, + auto_recovery=auto_recovery, + delay_restore=delay_restore) + + if not (pkl_src and pkl_dest and pkl_vrf): + # if only the source or dest is set, it'll fail and ask to set the + # other + if pkl_src or pkl_dest: + module.fail_json(msg='source AND dest IP for pkl are required at ' + 'this time (although source is technically not ' + ' required by the device.)') + + args.pop('pkl_src') + args.pop('pkl_dest') + args.pop('pkl_vrf') + + if pkl_vrf: + if pkl_vrf.lower() not in get_vrf_list(module): + module.fail_json(msg='The VRF you are trying to use for the peer ' + 'keepalive link is not on device yet. Add it' + ' first, please.') + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + changed = False + existing = get_vpc(module) + end_state = existing + + commands = [] + if state == 'present': + delta = set(proposed.iteritems()).difference(existing.iteritems()) + if delta: + command = get_commands_to_config_vpc(module, delta, domain, existing) + commands.append(command) + elif state == 'absent': + if existing: + if domain != existing['domain']: + module.fail_json(msg="You are trying to remove a domain that " + "does not exist on the device") + else: + commands.append('no vpc domain {0}'.format(domain)) + + cmds = flatten_list(commands) + + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_vpc(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From 8bf651afaa6150632fb618381a2e9736296b9009 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 01:20:37 +0200 Subject: [PATCH 090/770] Adding nxos_portchannel --- network/nxos/nxos_vpc_interface.py | 1077 ++++++++++++++++++++++++++++ 1 file changed, 1077 insertions(+) create mode 100644 network/nxos/nxos_vpc_interface.py diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py new file mode 100644 index 00000000000..f5b5dd1d5c6 --- /dev/null +++ b/network/nxos/nxos_vpc_interface.py @@ -0,0 +1,1077 @@ +#!/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_vpc_interface +version_added: "2.2" +short_description: Manages interface VPC configuration +description: + - Manages interface VPC configuration +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - Either vpc or peer_link param is required, but not both. + - C(state)=absent removes whatever VPC config is on a port-channel + if one exists. + - Re-assigning a vpc or peerlink from one portchannel to another is not + supported. The module will force the user to unconfigure an existing + vpc/pl before configuring the same value on a new portchannel +options: + portchannel: + description: + - group number of the portchannel that will be configured + required: true + vpc: + description: + - vpc group/id that will be configured on associated portchannel + required: false + default: null + peer_link: + description: + - Set to true/false for peer link config on assoicated portchannel + required: false + default: null + state: + description: + - Manages desired state of the resource + required: true + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- nxos_vpc_portchannel: + portchannel=10 + vpc=100 + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"portchannel": "100", "vpc": "10"} +existing: + description: k/v pairs of existing configuration + type: dict + sample: {} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"peer-link": false, "portchannel": "100", "vpc": "10"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface port-channel100", "vpc 10"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +def execute_config_command(commands, module): + try: + output = module.configure(commands) + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return output + + +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. + """ + if '^' == response[0]: + body = [] + elif 'running' in command or 'xml' in response[0]: + 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): + 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)) + 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 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 get_portchannel_list(module): + command = 'show port-channel summary' + portchannels = [] + pc_list = [] + + body = execute_show_command(command, module) + + try: + pc_list = body[0]['TABLE_channel']['ROW_channel'] + except (KeyError, AttributeError): + return portchannels + + if pc_list: + if isinstance(pc_list, dict): + pc_list = [pc_list] + + for pc in pc_list: + portchannels.append(pc['group']) + + return portchannels + + +def get_existing_portchannel_to_vpc_mappings(module): + command = 'show vpc brief' + pc_vpc_mapping = {} + + body = execute_show_command(command, module) + + try: + vpc_table = body[0]['TABLE_vpc']['ROW_vpc'] + except (KeyError, AttributeError, TypeError): + vpc_table = None + + if vpc_table: + if isinstance(vpc_table, dict): + vpc_table = [vpc_table] + + for vpc in vpc_table: + pc_vpc_mapping[str(vpc['vpc-id'])] = str(vpc['vpc-ifindex']) + + return pc_vpc_mapping + + +def peer_link_exists(module): + found = False + run = get_vpc_running_config(module) + + vpc_list = run.split('\n') + for each in vpc_list: + if 'peer-link' in each: + found = True + return found + + +def get_vpc_running_config(module): + command = 'show running section vpc' + body = execute_show_command(command, module, + command_type='cli_show_ascii')[0] + + return body + + +def get_active_vpc_peer_link(module): + command = 'show vpc brief' + peer_link = None + body = execute_show_command(command, module) + try: + peer_link = body[0]['TABLE_peerlink']['ROW_peerlink']['peerlink-ifindex'] + except (KeyError, AttributeError): + return peer_link + + return peer_link + + +def get_portchannel_vpc_config(module, portchannel): + command = 'show vpc brief' + peer_link_pc = None + peer_link = False + vpc = "" + pc = "" + config = {} + + body = execute_show_command(command, module) + + try: + table = body[0]['TABLE_peerlink']['ROW_peerlink'] + except (KeyError, AttributeError, TypeError): + table = {} + + if table: + peer_link_pc = table.get('peerlink-ifindex', None) + + if peer_link_pc: + plpc = str(peer_link_pc[2:]) + if portchannel == plpc: + config['portchannel'] = portchannel + config['peer-link'] = True + config['vpc'] = vpc + + mapping = get_existing_portchannel_to_vpc_mappings(module) + + for existing_vpc, port_channel in mapping.iteritems(): + port_ch = str(port_channel[2:]) + if port_ch == portchannel: + pc = port_ch + vpc = str(existing_vpc) + + config['portchannel'] = pc + config['peer-link'] = peer_link + config['vpc'] = vpc + + return config + + +def get_commands_to_config_vpc_interface(portchannel, delta, config_value, existing): + commands = [] + + if delta.get('peer-link') is False and existing.get('peer-link') is True: + command = 'no vpc peer-link' + commands.append('no vpc peer-link') + commands.insert(0, 'interface port-channel{0}'.format(portchannel)) + + elif delta.get('peer-link') or not existing.get('vpc'): + command = 'vpc {0}'.format(config_value) + commands.append(command) + commands.insert(0, 'interface port-channel{0}'.format(portchannel)) + + return commands + + +def main(): + argument_spec = dict( + portchannel=dict(required=True, type='str'), + vpc=dict(required=False, type='str'), + peer_link=dict(required=False, type='bool'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_module(argument_spec=argument_spec, + mutually_exclusive=[['vpc', 'peer_link']], + supports_check_mode=True) + + portchannel = module.params['portchannel'] + vpc = module.params['vpc'] + peer_link = module.params['peer_link'] + state = module.params['state'] + + changed = False + args = {'portchannel': portchannel, 'vpc': vpc, 'peer-link': peer_link} + active_peer_link = None + + if portchannel not in get_portchannel_list(module): + module.fail_json(msg="The portchannel you are trying to make a" + " VPC or PL is not created yet. " + "Create it first!") + if vpc: + mapping = get_existing_portchannel_to_vpc_mappings(module) + + if vpc in mapping.keys() and portchannel != mapping[vpc].strip('Po'): + module.fail_json(msg="This vpc is already configured on " + "another portchannel. Remove it first " + "before trying to assign it here. ", + existing_portchannel=mapping[vpc]) + + for vpcid, existing_pc in mapping.iteritems(): + if portchannel == existing_pc.strip('Po') and vpcid != vpc: + module.fail_json(msg="This portchannel already has another" + " VPC configured. Remove it first " + "before assigning this one", + existing_vpc=vpcid) + + if peer_link_exists(module): + active_peer_link = get_active_vpc_peer_link(module) + if active_peer_link[-2:] == portchannel: + module.fail_json(msg="That port channel is the current " + "PEER LINK. Remove it if you want it" + " to be a VPC") + config_value = vpc + + elif peer_link is not None: + if peer_link_exists(module): + active_peer_link = get_active_vpc_peer_link(module)[2::] + if active_peer_link != portchannel: + if peer_link: + module.fail_json(msg="A peer link already exists on" + " the device. Remove it first", + current_peer_link='Po{0}'.format( + active_peer_link)) + config_value = 'peer-link' + + + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + existing = get_portchannel_vpc_config(module, portchannel) + end_state = existing + commands = [] + + if state == 'present': + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + if delta: + command = get_commands_to_config_vpc_interface( + portchannel, + delta, + config_value, + existing + ) + commands.append(command) + + elif state == 'absent': + if existing.get('vpc'): + command = ['no vpc'] + commands.append(command) + elif existing.get('peer-link'): + command = ['no vpc peer-link'] + commands.append(command) + if commands: + commands.insert(0, ['interface port-channel{0}'.format(portchannel)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + output = execute_config_command(cmds, module) + if module.params['transport'] == 'cli': + output = ' '.join(output) + if 'error' in output.lower(): + module.fail_json(msg=output.replace('\n', '')) + end_state = get_portchannel_vpc_config(module, portchannel) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From 5de77d4e6e505adba18ad1186dfb68637fc79e5e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 01:32:38 +0200 Subject: [PATCH 091/770] Adding nxos_vrf_af --- network/nxos/nxos_vrf_af.py | 960 ++++++++++++++++++++++++++++++++++++ 1 file changed, 960 insertions(+) create mode 100644 network/nxos/nxos_vrf_af.py diff --git a/network/nxos/nxos_vrf_af.py b/network/nxos/nxos_vrf_af.py new file mode 100644 index 00000000000..67e3c3dc39f --- /dev/null +++ b/network/nxos/nxos_vrf_af.py @@ -0,0 +1,960 @@ +#!/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_vxlan_vtep_vni +version_added: "2.2" +short_description: Creates a Virtual Network Identifier member (VNI) +description: + - Creates a Virtual Network Identifier member (VNI) for an NVE + overlay interface. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - default, where supported, restores params default value +options: + vrf: + description: + - Name of the VRF. + required: true + afi: + description: + - Address-Family Identifier (AFI). + required: true + choices: ['ipv4', 'ipv6'] + default: null + safi: + description: + - Sub Address-Family Identifier (SAFI). + required: true + choices: ['unicast', 'multicast'] + default: null + route_target_both_auto_evpn: + description: + - Enable/Disable the EVPN route-target 'auto' setting for both + import and export target communities. + required: false + choices: ['true', 'false'] + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_vrf_af: + interface=nve1 + vni=6000 + ingress_replication=true + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"afi": "ipv4", "route_target_both_auto_evpn": true, + "safi": "unicast", "vrf": "test"} +existing: + description: k/v pairs of existing configuration + type: dict + sample: {"afi": "ipv4", "route_target_both_auto_evpn": false, + "safi": "unicast", "vrf": "test"} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"afi": "ipv4", "route_target_both_auto_evpn": true, + "safi": "unicast", "vrf": "test"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["vrf context test", "address-family ipv4 unicast", + "route-target both auto evpn"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +BOOL_PARAMS = ['route_target_both_auto_evpn'] +PARAM_TO_COMMAND_KEYMAP = { + 'route_target_both_auto_evpn': 'route-target both auto evpn', +} +PARAM_TO_DEFAULT_KEYMAP = {} +WARNINGS = [] + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + + parents = ['vrf context {0}'.format(module.params['vrf'])] + parents.append('address-family {0} {1}'.format(module.params['afi'], + module.params['safi'])) + config = netcfg.get_section(parents) + if config: + splitted_config = config.splitlines() + vrf_index = False + for index in range(0, len(splitted_config) - 1): + if 'vrf' in splitted_config[index].strip(): + vrf_index = index + break + if vrf_index: + config = '\n'.join(splitted_config[0:vrf_index]) + + for arg in args: + if arg not in ['afi', 'safi', 'vrf']: + existing[arg] = get_value(arg, config, module) + + existing['afi'] = module.params['afi'] + existing['safi'] = module.params['safi'] + existing['vrf'] = module.params['vrf'] + + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + + elif value is False: + commands.append('no {0}'.format(key)) + + elif value == 'default': + if existing_commands.get(key): + existing_value = existing_commands.get(key) + commands.append('no {0} {1}'.format(key, existing_value)) + else: + command = '{0} {1}'.format(key, value.lower()) + commands.append(command) + + if commands: + parents = ['vrf context {0}'.format(module.params['vrf'])] + parents.append('address-family {0} {1}'.format(module.params['afi'], + module.params['safi'])) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = [] + parents = ['vrf context {0}'.format(module.params['vrf'])] + commands.append('no address-family {0} {1}'.format(module.params['afi'], + module.params['safi'])) + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + vrf=dict(required=True, type='str'), + safi=dict(required=True, type='str', choices=['unicast','multicast']), + afi=dict(required=True, type='str', choices=['ipv4','ipv6']), + route_target_both_auto_evpn=dict(required=False, type='bool'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=False) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + + args = [ + 'vrf', + 'safi', + 'afi', + 'route_target_both_auto_evpn' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'interface': + if str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From af7fb1a40ba0c57f4807ed3087cda3ee75edfb9e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 01:34:10 +0200 Subject: [PATCH 092/770] Fix docstring --- network/nxos/nxos_vrf_af.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/network/nxos/nxos_vrf_af.py b/network/nxos/nxos_vrf_af.py index 67e3c3dc39f..b1c4d8d02f2 100644 --- a/network/nxos/nxos_vrf_af.py +++ b/network/nxos/nxos_vrf_af.py @@ -18,12 +18,11 @@ DOCUMENTATION = ''' --- -module: nxos_vxlan_vtep_vni +module: nxos_vrf_af version_added: "2.2" -short_description: Creates a Virtual Network Identifier member (VNI) +short_description: Manages VRF AF description: - - Creates a Virtual Network Identifier member (VNI) for an NVE - overlay interface. + - Manages VRF AF author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: From 15d4fa42075fa1489d6e2dfd11be30c0a88f5f43 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 01:46:42 +0200 Subject: [PATCH 093/770] Adding nxos_vxlan_vtep --- network/nxos/nxos_vxlan_vtep.py | 1042 +++++++++++++++++++++++++++++++ 1 file changed, 1042 insertions(+) create mode 100644 network/nxos/nxos_vxlan_vtep.py diff --git a/network/nxos/nxos_vxlan_vtep.py b/network/nxos/nxos_vxlan_vtep.py new file mode 100644 index 00000000000..0e30cdbfca4 --- /dev/null +++ b/network/nxos/nxos_vxlan_vtep.py @@ -0,0 +1,1042 @@ +#!/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_vxlan_vtep +version_added: "2.2" +short_description: Manages VXLAN Network Virtualization Endpoint (NVE) +description: + - Manages VXLAN Network Virtualization Endpoint (NVE) overlay interface + that terminates VXLAN tunnels. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - The module is used to manage NVE properties, not to create NVE + interfaces. Use nxos_interface if you wish to do so. + - State 'absent' removes the interface + - default, where supported, restores params default value +options: + interface: + description: + - Interface name for the VXLAN Network Virtualization Endpoint + required: true + description: + description: + - Description of the NVE interface. + required: false + default: null + host_reachability: + description: + - Specify mechanism for host reachability advertisement. + required: false + choices: ['true', 'false'] + default: null + shutdown: + description: + - Administratively shutdown the NVE interface. + required: false + choices: ['true','false'] + default: false + source_interface: + description: + - Specify the loopback interface whose IP address should be + used for the NVE interface. + required: false + default: null + source_interface_hold_down_time: + description: + - Suppresses advertisement of the NVE loopback address until + the overlay has converged. + required: false + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_vxlan_vtep: + interface=nve1 + description=default + host_reachability=default + source_interface=Loopback0 + source_interface_hold_down_time=30 + shutdown=default + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"description": "simple description", "host_reachability": true, + "interface": "nve1", "shutdown": true, "source_interface": "loopback0", + "source_interface_hold_down_time": "30"} +existing: + description: k/v pairs of existing VXLAN VTEP configuration + type: dict + sample: {} +end_state: + description: k/v pairs of BGP configuration after module execution + returned: always + type: dict + sample: {"description": "simple description", "host_reachability": true, + "interface": "nve1", "shutdown": true, "source_interface": "loopback0", + "source_interface_hold_down_time": "30"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface nve1", "source-interface loopback0", + "source-interface hold-down-time 30", "description simple description", + "shutdown", "host-reachability protocol bgp"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +BOOL_PARAMS = [ + 'shutdown', + 'host_reachability' +] +PARAM_TO_COMMAND_KEYMAP = { + 'description': 'description', + 'host_reachability': 'host-reachability protocol bgp', + 'interface': 'interface', + 'shutdown': 'shutdown', + 'source_interface': 'source-interface', + 'source_interface_hold_down_time': 'source-interface hold-down-time' +} +PARAM_TO_DEFAULT_KEYMAP = { + 'description': False, + 'shutdown': True, +} + +WARNINGS = [] + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + NO_SHUT_REGEX = re.compile(r'\s+no shutdown\s*$', re.M) + value = False + if arg == 'shutdown': + try: + if NO_SHUT_REGEX.search(config): + value = False + elif REGEX.search(config): + value = True + except TypeError: + value = False + else: + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + NO_DESC_REGEX = re.compile(r'\s+{0}\s*$'.format('no description'), re.M) + SOURCE_INTF_REGEX = re.compile(r'(?:{0}\s)(?P\S+)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if arg == 'description': + if NO_DESC_REGEX.search(config): + value = '' + elif PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value').strip() + elif arg == 'source_interface': + for line in config.splitlines(): + try: + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = SOURCE_INTF_REGEX.search(config).group('value').strip() + break + except AttributeError: + value = '' + else: + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value').strip() + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + + parents = ['interface {0}'.format(module.params['interface'].lower())] + config = netcfg.get_section(parents) + + if config: + for arg in args: + existing[arg] = get_value(arg, config, module) + + existing['interface'] = module.params['interface'].lower() + return existing + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def fix_commands(commands, module): + source_interface_command = '' + no_source_interface_command = '' + + for command in commands: + if 'no source-interface hold-down-time' in command: + pass + elif 'source-interface hold-down-time' in command: + pass + elif 'no source-interface' in command: + no_source_interface_command = command + elif 'source-interface' in command: + source_interface_command = command + + if source_interface_command: + commands.pop(commands.index(source_interface_command)) + commands.insert(0, source_interface_command) + + if no_source_interface_command: + commands.pop(commands.index(no_source_interface_command)) + commands.append(no_source_interface_command) + return commands + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + + elif value is False: + commands.append('no {0}'.format(key)) + + elif value == 'default': + if existing_commands.get(key): + existing_value = existing_commands.get(key) + commands.append('no {0} {1}'.format(key, existing_value)) + else: + if key.replace(' ', '_').replace('-', '_') in BOOL_PARAMS: + commands.append('no {0}'.format(key.lower())) + module.exit_json(commands=commands) + else: + command = '{0} {1}'.format(key, value.lower()) + commands.append(command) + + if commands: + commands = fix_commands(commands, module) + parents = ['interface {0}'.format(module.params['interface'].lower())] + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + commands = ['no interface {0}'.format(module.params['interface'].lower())] + candidate.add(commands, parents=[]) + + +def main(): + argument_spec = dict( + interface=dict(required=True, type='str'), + description=dict(required=False, type='str'), + host_reachability=dict(required=False, type='bool'), + shutdown=dict(required=False, type='bool'), + source_interface=dict(required=False, type='str'), + source_interface_hold_down_time=dict(required=False, type='str'), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + interface = module.params['interface'].lower() + + args = [ + 'interface', + 'description', + 'host_reachability', + 'shutdown', + 'source_interface', + 'source_interface_hold_down_time' + ] + + existing = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'interface': + if str(value).lower() == 'true': + value = True + elif str(value).lower() == 'false': + value = False + elif str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + if key in BOOL_PARAMS: + value = False + else: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + if not existing: + WARNINGS.append("The proposed NVE interface did not exist. " + "It's recommended to use nxos_interface to create " + "all logical interfaces.") + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From a98f17db15d5852b5964eb1ae94601ba2b9a279d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 01:59:20 +0200 Subject: [PATCH 094/770] Adding nxos_vxlan_vtep_vni --- network/nxos/nxos_vxlan_vtep_vni.py | 1093 +++++++++++++++++++++++++++ 1 file changed, 1093 insertions(+) create mode 100644 network/nxos/nxos_vxlan_vtep_vni.py diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py new file mode 100644 index 00000000000..ab8c8815abc --- /dev/null +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -0,0 +1,1093 @@ +#!/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_vxlan_vtep_vni +version_added: "2.2" +short_description: Creates a Virtual Network Identifier member (VNI) +description: + - Creates a Virtual Network Identifier member (VNI) for an NVE + overlay interface. +author: Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - default, where supported, restores params default value +options: + interface: + description: + - Interface name for the VXLAN Network Virtualization Endpoint + required: true + vni: + description: + - ID of the Virtual Network Identifier. + required: true + assoc_vrf: + description: + - This attribute is used to identify and separate processing VNIs + that are associated with a VRF and used for routing. The VRF + and VNI specified with this command must match the configuration + of the VNI under the VRF. + required: false + choices: ['true','false'] + default: null + ingress_replication: + description: + - Specifies mechanism for host reachability advertisement. + required: false + choices: ['bgp','static'] + default: null + multicast_group: + description: + - The multicast group (range) of the VNI. Valid values are + string and keyword 'default'. + required: false + default: null + peer_list: + description: + - Set the ingress-replication static peer list. Valid values + are an array, a space-separated string of ip addresses, + or the keyword 'default'. + required: false + default: null + suppress_arp: + description: + - Suppress arp under layer 2 VNI. + required: false + choices: ['true','false'] + default: null + state: + description: + - Determines whether the config should be present or not on the device. + required: false + default: present + choices: ['present','absent'] + m_facts: + description: + - Used to print module facts + required: false + default: false + choices: ['true','false'] +''' +EXAMPLES = ''' +- nxos_vxlan_vtep_vni: + interface=nve1 + vni=6000 + ingress_replication=default + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: when C(m_facts)=true + type: dict + sample: {"ingress_replication": "default", "interface": "nve1", "vni": "6000"} +existing: + description: k/v pairs of existing configuration + returned: when C(m_facts)=true + type: dict + sample: {} +end_state: + description: k/v pairs of configuration after module execution + returned: when C(m_facts)=true + type: dict + sample: {"assoc_vrf": false, "ingress_replication": "", "interface": "nve1", + "multicast_group": "", "peer_list": [], + "suppress_arp": false, "vni": "6000"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["interface nve1", "member vni 6000"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +BOOL_PARAMS = ['suppress_arp'] +PARAM_TO_COMMAND_KEYMAP = { + 'assoc_vrf': 'associate-vrf', + 'interface': 'interface', + 'vni': 'member vni', + 'ingress_replication': 'ingress-replication protocol', + 'multicast_group': 'mcast-group', + 'peer_list': 'peer-ip', + 'suppress_arp': 'suppress-arp' +} +PARAM_TO_DEFAULT_KEYMAP = {} +WARNINGS = [] + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_value(arg, config, module): + if arg in BOOL_PARAMS: + REGEX = re.compile(r'\s+{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + else: + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in config: + value = REGEX.search(config).group('value') + return value + + +def check_interface(module, netcfg): + config = str(netcfg) + + REGEX = re.compile(r'(?:interface nve)(?P.*)$', re.M) + value = '' + if 'interface nve' in config: + value = 'nve{0}'.format(REGEX.search(config).group('value')) + + return value + + +def get_custom_value(arg, config, module): + splitted_config = config.splitlines() + if arg == 'assoc_vrf': + value = False + if 'associate-vrf' in config: + value = True + elif arg == 'peer_list': + value = [] + REGEX = re.compile(r'(?:peer-ip\s)(?P.*)$', re.M) + for line in splitted_config: + peer_value = '' + if PARAM_TO_COMMAND_KEYMAP[arg] in line: + peer_value = REGEX.search(line).group('peer_value') + if peer_value: + value.append(peer_value) + return value + + +def get_existing(module, args): + existing = {} + netcfg = custom_get_config(module) + + custom = [ + 'assoc_vrf', + 'peer_list' + ] + + interface_exist = check_interface(module, netcfg) + if interface_exist: + parents = ['interface {0}'.format(interface_exist)] + temp_config = netcfg.get_section(parents) + + if 'associate-vrf' in temp_config: + parents.append('member vni {0} associate-vrf'.format( + module.params['vni'])) + config = netcfg.get_section(parents) + elif 'member vni' in temp_config: + parents.append('member vni {0}'.format(module.params['vni'])) + config = netcfg.get_section(parents) + else: + config = {} + + if config: + for arg in args: + if arg not in ['interface', 'vni']: + if arg in custom: + existing[arg] = get_custom_value(arg, config, module) + else: + existing[arg] = get_value(arg, config, module) + existing['interface'] = interface_exist + existing['vni'] = module.params['vni'] + + return existing, interface_exist + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def state_present(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + + for key, value in proposed_commands.iteritems(): + if key == 'associate-vrf': + command = 'member vni {0} {1}'.format(module.params['vni'], key) + + if value: + commands.append(command) + else: + commands.append('no {0}'.format(command)) + + elif key == 'peer-ip' and value != 'default': + for peer in value: + commands.append('{0} {1}'.format(key, peer)) + + elif value is True: + commands.append(key) + + elif value is False: + commands.append('no {0}'.format(key)) + + elif value == 'default': + if existing_commands.get(key): + existing_value = existing_commands.get(key) + if key == 'peer-ip': + for peer in existing_value: + commands.append('no {0} {1}'.format(key, peer)) + else: + commands.append('no {0} {1}'.format(key, existing_value)) + else: + if key.replace(' ', '_').replace('-', '_') in BOOL_PARAMS: + commands.append('no {0}'.format(key.lower())) + else: + command = '{0} {1}'.format(key, value.lower()) + commands.append(command) + + if commands: + vni_command = 'member vni {0}'.format(module.params['vni']) + ingress_replication_command = 'ingress-replication protocol static' + interface_command = 'interface {0}'.format(module.params['interface']) + + if ingress_replication_command in commands: + static_level_cmds = [cmd for cmd in commands if 'peer' in cmd] + parents = [interface_command, vni_command, ingress_replication_command] + candidate.add(static_level_cmds, parents=parents) + commands = [cmd for cmd in commands if 'peer' not in cmd] + + if vni_command in commands: + parents = [interface_command] + commands.remove(vni_command) + if module.params['assoc_vrf'] is None: + parents.append(vni_command) + candidate.add(commands, parents=parents) + + +def state_absent(module, existing, proposed, candidate): + if existing['assoc_vrf']: + commands = ['no member vni {0} associate-vrf'.format( + module.params['vni'])] + else: + commands = ['no member vni {0}'.format(module.params['vni'])] + parents = ['interface {0}'.format(module.params['interface'])] + candidate.add(commands, parents=parents) + + +def main(): + argument_spec = dict( + interface=dict(required=True, type='str'), + vni=dict(required=True, type='str'), + assoc_vrf=dict(required=False, type='bool'), + multicast_group=dict(required=False, type='str'), + peer_list=dict(required=False, type='list'), + suppress_arp=dict(required=False, type='bool'), + ingress_replication=dict(required=False, type='str', + choices=['bgp', 'static', 'default']), + m_facts=dict(required=False, default=False, type='bool'), + state=dict(choices=['present', 'absent'], default='present', + required=False), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + if module.params['assoc_vrf']: + mutually_exclusive_params = ['multicast_group', + 'suppress_arp', + 'ingress_replication'] + for param in mutually_exclusive_params: + if module.params[param]: + module.fail_json(msg='assoc_vrf cannot be used with ' + '{0} param'.format(param)) + if module.params['peer_list']: + if module.params['ingress_replication'] != 'static': + module.fail_json(msg='ingress_replication=static is required ' + 'when using peer_list param') + else: + peer_list = module.params['peer_list'] + if peer_list[0] == 'default': + module.params['peer_list'] = 'default' + else: + stripped_peer_list = map(str.strip, peer_list) + module.params['peer_list'] = stripped_peer_list + + state = module.params['state'] + args = [ + 'assoc_vrf', + 'interface', + 'vni', + 'ingress_replication', + 'multicast_group', + 'peer_list', + 'suppress_arp' + ] + + existing, interface_exist = invoke('get_existing', module, args) + end_state = existing + proposed_args = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed = {} + for key, value in proposed_args.iteritems(): + if key != 'interface': + if str(value).lower() == 'default': + value = PARAM_TO_DEFAULT_KEYMAP.get(key) + if value is None: + value = 'default' + if existing.get(key) or (not existing.get(key) and value): + proposed[key] = value + + result = {} + if state == 'present' or (state == 'absent' and existing): + if not interface_exist: + WARNINGS.append("The proposed NVE interface does not exist. " + "Use nxos_interface to create it first.") + elif interface_exist != module.params['interface']: + module.fail_json(msg='Only 1 NVE interface is allowed on ' + 'the switch.') + elif (existing and state == 'absent' and + existing['vni'] != module.params['vni']): + module.fail_json(msg="ERROR: VNI delete failed: Could not find" + " vni node for {0}".format( + module.params['vni']), + existing_vni=existing['vni']) + else: + candidate = CustomNetworkConfig(indent=3) + invoke('state_%s' % state, module, existing, proposed, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + result['connected'] = module.connected + if module.params['m_facts']: + end_state, interface_exist = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed_args + + if WARNINGS: + result['warnings'] = WARNINGS + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 2c7e9a03307dc619706641efdd05e73ea7597756 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 02:11:06 +0200 Subject: [PATCH 095/770] Adding nxos_rollback --- network/nxos/nxos_rollback.py | 812 ++++++++++++++++++++++++++++++++++ 1 file changed, 812 insertions(+) create mode 100644 network/nxos/nxos_rollback.py diff --git a/network/nxos/nxos_rollback.py b/network/nxos/nxos_rollback.py new file mode 100644 index 00000000000..d93cf44020b --- /dev/null +++ b/network/nxos/nxos_rollback.py @@ -0,0 +1,812 @@ +#!/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_rollback +short_description: Set a checkpoint or rollback to a checkpoint +description: + - This module offers the ability to set a configuration checkpoint file or rollback + to a configuration checkpoint file on Cisco NXOS switches- +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - Sometimes C(transport)=nxapi may cause a timeout error. +options: + checkpoint_file: + description: + - Name of checkpoint file to create. Mutually exclusive with rollback_to. + required: false + default: null + rollback_to: + description: + - Name of checkpoint file to rollback to. Mutually exclusive with checkpoint_file. + required: false + default: null +''' + +EXAMPLES = ''' +- nxos_rollback: + checkpoint_file: backup.cfg + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +- nxos_rollback: + rollback_to: backup.cfg + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +filename: + description: The filename of the checkpoint/rollback file. + returned: success + type: string + sample: 'backup.cfg' +status: + description: Which operation took place and whether it was successful. + returned: success + type: string + sample: 'rollback executed' +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +def execute_commands(cmds, module, command_type=None): + try: + if command_type: + module.execute(cmds, command_type=command_type) + else: + module.execute(cmds) + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) + + +def prepare_show_command(command, module): + if module.params['transport'] == 'cli': + execute_commands(command, module) + elif module.params['transport'] == 'nxapi': + execute_commands(command, module, command_type='cli_show_ascii') + + +def checkpoint(filename, module): + commands = ['terminal dont-ask', 'checkpoint file %s' % filename] + prepare_show_command(commands, module) + + +def rollback(filename, module): + commands = ['rollback running-config file %s' % filename] + module.configure(commands) + + +def main(): + argument_spec = dict( + checkpoint_file=dict(required=False), + rollback_to=dict(required=False), + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + mutually_exclusive=[['checkpoint_file', + 'rollback_to']], + supports_check_mode=False) + + checkpoint_file = module.params['checkpoint_file'] + rollback_to = module.params['rollback_to'] + + status = None + filename = None + changed = False + try: + if checkpoint_file: + checkpoint(checkpoint_file, module) + status = 'checkpoint file created' + elif rollback_to: + rollback(rollback_to, module) + status = 'rollback executed' + changed = True + filename = rollback_to or checkpoint_file + except ShellError: + clie = get_exception() + module.fail_json(msg=str(clie)) + + module.exit_json(changed=changed, status=status, filename=filename) + + +if __name__ == '__main__': + main() From b89467ada452c2b70083958c14dea1a0c828b3a9 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 02:15:43 +0200 Subject: [PATCH 096/770] Fixed docstring --- network/nxos/nxos_rollback.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/network/nxos/nxos_rollback.py b/network/nxos/nxos_rollback.py index d93cf44020b..1ffe25c665e 100644 --- a/network/nxos/nxos_rollback.py +++ b/network/nxos/nxos_rollback.py @@ -19,10 +19,13 @@ DOCUMENTATION = ''' --- module: nxos_rollback +version_added: "2.2" short_description: Set a checkpoint or rollback to a checkpoint description: - - This module offers the ability to set a configuration checkpoint file or rollback - to a configuration checkpoint file on Cisco NXOS switches- + - This module offers the ability to set a configuration checkpoint + file or rollback to a configuration checkpoint file on Cisco NXOS + switches +extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) @@ -31,12 +34,14 @@ options: checkpoint_file: description: - - Name of checkpoint file to create. Mutually exclusive with rollback_to. + - Name of checkpoint file to create. Mutually exclusive + with rollback_to. required: false default: null rollback_to: description: - - Name of checkpoint file to rollback to. Mutually exclusive with checkpoint_file. + - Name of checkpoint file to rollback to. Mutually exclusive + with checkpoint_file. required: false default: null ''' From 65c6f5079c74ceabd29194f58a8ebb123ba87ff9 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 02:27:21 +0200 Subject: [PATCH 097/770] Increase timeout --- network/nxos/nxos_rollback.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_rollback.py b/network/nxos/nxos_rollback.py index 1ffe25c665e..50e2b76cd77 100644 --- a/network/nxos/nxos_rollback.py +++ b/network/nxos/nxos_rollback.py @@ -568,7 +568,7 @@ def send(self, commands, command_type='cli_show_ascii', encoding='json'): headers['Cookie'] = self._nxapi_auth response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') + headers=headers, method='POST', timeout=20) self._nxapi_auth = headers.get('set-cookie') From 4f0259ff9b4767e67bbe93374e9e413a4bb20468 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 02:40:23 +0200 Subject: [PATCH 098/770] Added python object for 2.1 and 2.2 support --- network/nxos/nxos_feature.py | 896 +++++++++++++++++++++++++---------- 1 file changed, 648 insertions(+), 248 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index b8625707cea..b7269fe66a9 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -1,276 +1,676 @@ -#!/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_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 = ''' -# Ensure lacp is enabled -- nxos_feature: feature=lacp state=enabled host={{ inventory_hostname }} -# Ensure ospf is disabled -- nxos_feature: feature=ospf state=disabled host={{ inventory_hostname }} -# 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"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "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" -''' - - -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) - - -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: +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + def to_block(self, section): + return '\n'.join([item.raw for item in section]) + + def get_section(self, path): try: - body = [json.loads(response[0])] + section = self.get_section_objects(path) + if self._device_os == 'junos': + return self.to_lines(section) + return self.to_block(section) except ValueError: - module.fail_json(msg='Command does not support JSON output', - command=command) - return body + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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) -def execute_show(cmds, module, command_type=None): - 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)) - 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) + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) else: - new_dict[new_key] = value - return new_dict + result.append(item['body']) + + return result -def get_available_features(feature, module): - available_features = {} - command = 'show feature' - body = execute_show_command(command, module) +class Cli(object): - try: - body = body[0]['TABLE_cfcFeatureCtrlTable']['ROW_cfcFeatureCtrlTable'] - except (TypeError, IndexError): - return available_features + def __init__(self, module): + self.module = module + self.shell = None - for each_feature in body: - feature = each_feature['cfcFeatureCtrlName2'] - state = each_feature['cfcFeatureCtrlOpStatus2'] + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 - if 'enabled' in state: - state = 'enabled' + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] - if feature not in available_features.keys(): - available_features[feature] = state + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) else: - if (available_features[feature] == 'disabled' and - state == 'enabled'): - available_features[feature] = state + return self.execute(commands, command_type='cli_conf') - return available_features + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) -def get_commands(proposed, existing, state, module): - feature = validate_feature(module, mode='config') + def disconnect(self): + self.connection.close() + self._connected = False - 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 parse_config(self, cfg): + return parse(cfg, indent=2) + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] -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'] +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec - feature_to_be_mapped = { - 'show': { - 'nv overlay': 'nve'}, - 'config': - { - 'nve': 'nv overlay'} - } + module = NetworkModule(**kwargs) - if feature in feature_to_be_mapped[mode]: - feature = feature_to_be_mapped[mode][feature] + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') - return feature + return module -def main(): - argument_spec = dict( - feature=dict(type='str', required=True), - state=dict(choices=['enabled', 'disabled'], default='enabled', - required=False), - ) - module = get_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.keys(): - module.fail_json( - msg='Invalid feature name.', - features_currently_supported=available_features, - invalid_feature=feature) - else: - existstate = available_features[feature] +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] - existing = dict(state=existstate) - proposed = dict(state=state) - changed = False - end_state = existing + return CustomNetworkConfig(indent=2, contents=config) - cmds = get_commands(proposed, existing, state, module) +def load_config(module, candidate): + config = custom_get_config(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) - - results = {} - results['proposed'] = proposed - results['existing'] = existing - results['end_state'] = end_state - results['state'] = state - results['updates'] = cmds - results['changed'] = changed - results['feature'] = module.params['feature'] - - module.exit_json(**results) - - -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * -if __name__ == '__main__': - main() + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE From 373ebaeeb4548e095646fa4580abcfeb90865027 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 02:43:43 +0200 Subject: [PATCH 099/770] Fix PR --- network/nxos/nxos_feature.py | 276 +++++++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index b7269fe66a9..04b7a6ce64e 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -1,3 +1,93 @@ +#!/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_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 = ''' +# Ensure lacp is enabled +- nxos_feature: feature=lacp state=enabled host={{ inventory_hostname }} +# Ensure ospf is disabled +- nxos_feature: feature=ospf state=disabled host={{ inventory_hostname }} +# 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"} +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "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" +''' + + # COMMON CODE FOR MIGRATION import re @@ -5,6 +95,7 @@ import collections import itertools import shlex +import json from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE @@ -674,3 +765,188 @@ def load_config(module, candidate): 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) + + +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): + 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)) + 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 = {} + command = 'show feature' + body = execute_show_command(command, module) + + try: + body = body[0]['TABLE_cfcFeatureCtrlTable']['ROW_cfcFeatureCtrlTable'] + except (TypeError, IndexError): + return available_features + + for each_feature in body: + feature = each_feature['cfcFeatureCtrlName2'] + state = each_feature['cfcFeatureCtrlOpStatus2'] + + if 'enabled' in state: + state = 'enabled' + + if feature not in available_features.keys(): + 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'}, + 'config': + { + 'nve': 'nv overlay', + 'vnseg_vlan': 'vn-segment-vlan-based'} + } + + 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) + ) + module = get_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.keys(): + 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) + + 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() From 814eb0609db15e41bc4e4001d247ab8ebd63460c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 02:47:22 +0200 Subject: [PATCH 100/770] Adding nxos_reboot --- network/nxos/nxos_reboot.py | 797 ++++++++++++++++++++++++++++++++++++ 1 file changed, 797 insertions(+) create mode 100644 network/nxos/nxos_reboot.py diff --git a/network/nxos/nxos_reboot.py b/network/nxos/nxos_reboot.py new file mode 100644 index 00000000000..5bf5eb3626e --- /dev/null +++ b/network/nxos/nxos_reboot.py @@ -0,0 +1,797 @@ +#!/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_reboot +version_added: 2.2 +short_description: Reboot a network device. +description: + - Reboot a network device +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - The module will fail due to timeout issues, but the reboot will be + performed anyway. +options: + confirm: + description: + - Safeguard boolean. Set to true if you're sure you want to reboot. + required: false + default: false +''' + +EXAMPLES = ''' +- nxos_reboot: + confirm: true + host: "{{ inventory_hostname }}" + username: "{{ username }}" + password: "{{ password }}" +''' + +RETURN = ''' +rebooted: + description: Whether the device was instructed to reboot. + returned: success + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + + +def reboot(module): + disable_confirmation(module) + execute_show_command(['reload'], module, command_type='cli_show_ascii') + + +def execute_show(cmds, module, command_type=None): + 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)) + return response + + +def execute_show_command(command, module, command_type='cli_show'): + if module.params['transport'] == 'cli': + body = execute_show(command, module, reboot=reboot) + elif module.params['transport'] == 'nxapi': + body = execute_show(command, module, command_type=command_type) + + return body + + +def disable_confirmation(module): + command = 'terminal dont-ask' + body = execute_show_command(command, module, command_type='cli_show_ascii')[0] + + +def main(): + argument_spec = dict( + confirm=dict(required=True, type='bool'), + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + confirm = module.params['confirm'] + if not confirm: + module.fail_json(msg='confirm must be set to true for this ' + 'module to work.') + + changed = False + rebooted = False + + reboot(module) + + changed = True + rebooted = True + + results = {} + results['changed'] = changed + results['rebooted'] = rebooted + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From 811ce0217837cae3c5a00cc80d5964df0463a7d0 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 03:08:19 +0200 Subject: [PATCH 101/770] nxos_ip_interface support for 2.1 and 2.2 --- network/nxos/nxos_ip_interface.py | 694 +++++++++++++++++++++++++++++- 1 file changed, 683 insertions(+), 11 deletions(-) diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index 39dd7226b47..376a45e326e 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -37,21 +37,21 @@ options: interface: description: - - Full name of interface, i.e. Ethernet1/1, vlan10. + - Full name of interface, i.e. Ethernet1/1, vlan10 required: true addr: description: - - IPv4 or IPv6 Address. + - IPv4 or IPv6 Address required: false default: null mask: description: - - Subnet mask for IPv4 or IPv6 Address in decimal format. + - Subnet mask for IPv4 or IPv6 Address in decimal format required: false default: null state: description: - - Specify desired state of the resource. + - Specify desired state of the resource required: false default: present choices: ['present','absent'] @@ -100,6 +100,683 @@ sample: true ''' +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE def execute_config_command(commands, module): try: @@ -120,7 +797,7 @@ def get_cli_body_ssh(command, response, module): """ if 'xml' in response[0]: body = [] - elif '^' in response[0] or 'show run' in response[0]: + elif '^' in response[0] or 'show run' in response[0] or response[0] == '\n': body = response else: try: @@ -442,6 +1119,7 @@ def main(): mask=dict(type='str', required=False), state=dict(required=False, default='present', choices=['present', 'absent']), + include_defaults=dict(default=True) ) module = get_module(argument_spec=argument_spec, supports_check_mode=True) @@ -516,15 +1194,9 @@ def main(): results['end_state'] = end_state results['updates'] = cmds results['changed'] = changed - results['state'] = state module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 87839953db4b8bea31574d1881b1db979841808c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 03:13:23 +0200 Subject: [PATCH 102/770] nxos_ping support for 2.1 and 2.2 --- network/nxos/nxos_ping.py | 700 +++++++++++++++++++++++++++++++++++++- 1 file changed, 685 insertions(+), 15 deletions(-) diff --git a/network/nxos/nxos_ping.py b/network/nxos/nxos_ping.py index 1e29aebd5bd..b8d9526639d 100644 --- a/network/nxos/nxos_ping.py +++ b/network/nxos/nxos_ping.py @@ -22,38 +22,36 @@ version_added: "2.1" short_description: Tests reachability using ping from Nexus switch description: - - Tests reachability using ping from switch to a remote destination. + - Tests reachability using ping from switch to a remote destination extends_documentation_fragment: nxos -author: - - Jason Edelman (@jedelman8) - - Gabriele Gerbino (@GGabriele) +author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) options: dest: description: - - IP address or hostname (resolvable by switch) of remote node. + - IP address or hostname (resolvable by switch) of remote node required: true count: description: - - Number of packets to send. + - Number of packets to send required: false default: 2 source: description: - - Source IP Address. + - Source IP Address required: false default: null vrf: description: - - Outgoing VRF. + - Outgoing VRF required: false default: null ''' EXAMPLES = ''' # test reachability to 8.8.8.8 using mgmt vrf -- nxos_ping: dest=8.8.8.8 vrf=management host={{ inventory_hostname }} +- nxos_ping: dest=8.8.8.8 vrf=management host=68.170.147.165 # Test reachability to a few different public IPs using mgmt vrf -- nxos_ping: dest={{ item }} vrf=management host={{ inventory_hostname }} +- nxos_ping: dest=nxos_ping vrf=management host=68.170.147.165 with_items: - 8.8.8.8 - 4.4.4.4 @@ -105,6 +103,683 @@ sample: "0.00%" ''' +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE def get_summary(results_list, reference_point): summary_string = results_list[reference_point+1] @@ -254,10 +929,5 @@ def main(): module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 05c6707a32a88eecdf7df381800d953bf607a9c1 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 2 Sep 2016 18:19:29 -0700 Subject: [PATCH 103/770] Make async_wrapper ignore '_' as an argsfile. (#4678) This provides support for passing additional positional parameters to async_wrapper. Additional parameters will be used by the upcoming async support for Windows. --- utilities/logic/async_wrapper.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 05452b4e2cc..320b3c5895a 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -120,7 +120,7 @@ def _run_module(wrapped_cmd, jid, job_path): #################### if __name__ == '__main__': - if len(sys.argv) < 3: + if len(sys.argv) < 5: print(json.dumps({ "failed" : True, "msg" : "usage: async_wrapper . Humans, do not call directly!" @@ -130,8 +130,9 @@ def _run_module(wrapped_cmd, jid, job_path): jid = "%s.%d" % (sys.argv[1], os.getpid()) time_limit = sys.argv[2] wrapped_module = sys.argv[3] - if len(sys.argv) >= 5: - argsfile = sys.argv[4] + argsfile = sys.argv[4] + # consider underscore as no argsfile so we can support passing of additional positional parameters + if argsfile != '_': cmd = "%s %s" % (wrapped_module, argsfile) else: cmd = wrapped_module From 7922796e925111eb0d7f5c963fc79a5bdc851193 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 03:20:14 +0200 Subject: [PATCH 104/770] nxos_vlan support 2.1 and 2.2 --- network/nxos/nxos_vlan.py | 761 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 732 insertions(+), 29 deletions(-) diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index 0c7db5d26e5..7f6eac86b31 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -28,36 +28,43 @@ options: vlan_id: description: - - Single VLAN ID. + - single vlan id required: false default: null vlan_range: description: - - A range of VLANs such as I(2-10) or I(2,5,10-15) etc. + - range of VLANs such as 2-10 or 2,5,10-15, etc. required: false default: null name: description: - - The name of VLAN. + - name of VLAN required: false default: null vlan_state: description: - - Manage the VLAN operational state of the VLAN - (equivalent to state {active | suspend} command. + - Manage the vlan operational state of the VLAN + (equivalent to state {active | suspend} command required: false default: active choices: ['active','suspend'] admin_state: description: - - Manage the VLAN admin state of the VLAN equivalent - to shut/no shut in VLAN config mode. + - Manage the vlan admin state of the VLAN equivalent + to shut/no shut in vlan config mode required: false default: up choices: ['up','down'] + mapped_vni: + description: + - The Virtual Network Identifier (VNI) id that is mapped to the + VLAN. Valid values are integer and keyword 'default'. + required: false + default: null + version_added: "2.2" state: description: - - Manage the state of the resource. + - Manage the state of the resource required: false default: present choices: ['present','absent'] @@ -65,13 +72,13 @@ ''' EXAMPLES = ''' # Ensure a range of VLANs are not present on the switch -- nxos_vlan: vlan_range="2-10,20,50,55-60,100-150" host={{ inventory_hostname }} username=cisco password=cisco state=absent transport=nxapi +- nxos_vlan: vlan_range="2-10,20,50,55-60,100-150" host=68.170.147.165 username=cisco password=cisco state=absent transport=nxapi # Ensure VLAN 50 exists with the name WEB and is in the shutdown state -- nxos_vlan: vlan_id=50 host={{ inventory_hostname }} admin_state=down name=WEB transport=nxapi username=cisco password=cisco +- nxos_vlan: vlan_id=50 host=68.170.147.165 admin_state=down name=WEB transport=nxapi username=cisco password=cisco # Ensure VLAN is NOT on the device -- nxos_vlan: vlan_id=50 host={{ inventory_hostname }} state=absent transport=nxapi username=cisco password=cisco +- nxos_vlan: vlan_id=50 host=68.170.147.165 state=absent transport=nxapi username=cisco password=cisco ''' @@ -99,20 +106,20 @@ returned: always type: dict or null sample: {"admin_state": "down", "name": "app_vlan", - "vlan_state": "suspend"} + "vlan_state": "suspend", "mapped_vni": "5000"} existing: description: k/v pairs of existing vlan or null when using vlan_range returned: always type: dict sample: {"admin_state": "down", "name": "app_vlan", - "vlan_id": "20", "vlan_state": "suspend"} + "vlan_id": "20", "vlan_state": "suspend", "mapped_vni": ""} end_state: description: k/v pairs of the VLAN after executing module or null when using vlan_range returned: always type: dict or null sample: {"admin_state": "down", "name": "app_vlan", "vlan_id": "20", - "vlan_state": "suspend"} + "vlan_state": "suspend", "mapped_vni": "5000"} state: description: state as sent in from the playbook returned: always @@ -122,7 +129,7 @@ description: command string sent to the device returned: always type: list - sample: ["vlan 20", "vlan 55"] + sample: ["vlan 20", "vlan 55", "vn-segment 5000"] changed: description: check to see if a change was made on the device returned: always @@ -131,6 +138,683 @@ ''' +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE def vlan_range_to_list(vlans): result = [] @@ -196,13 +880,17 @@ def get_vlan_config_commands(vlan, vid): 'name': 'name {0}', 'vlan_state': 'state {0}', 'admin_state': '{0}', - 'mode': 'mode {0}' + 'mode': 'mode {0}', + 'mapped_vni': 'vn-segment {0}' } commands = [] for param, value in vlan.iteritems(): - command = VLAN_ARGS.get(param).format(vlan.get(param)) + if param == 'mapped_vni' and value == 'default': + command = 'no vn-segment' + else: + command = VLAN_ARGS.get(param).format(vlan.get(param)) if command: commands.append(command) @@ -213,7 +901,6 @@ def get_vlan_config_commands(vlan, vid): def get_list_of_vlans(module): - command = 'show vlan' body = execute_show_command(command, module) vlan_list = [] @@ -228,6 +915,17 @@ def get_list_of_vlans(module): return vlan_list +def get_vni(vlanid, module): + command = 'show run all | section vlan.{0}'.format(vlanid) + body = execute_show_command(command, module, command_type='cli_show_ascii')[0] + value = '' + if body: + REGEX = re.compile(r'(?:vn-segment\s)(?P.*)$', re.M) + if 'vn-segment' in body: + value = REGEX.search(body).group('value') + return value + + def get_vlan(vlanid, module): """Get instance of VLAN as a dictionary """ @@ -258,7 +956,7 @@ def get_vlan(vlanid, module): } vlan = apply_value_map(value_map, vlan) - + vlan['mapped_vni'] = get_vni(vlanid, module) return vlan @@ -293,7 +991,9 @@ def get_cli_body_ssh(command, response, module): if | json returns an XML string, it is a valid command, but that the resource doesn't exist yet. """ - if 'xml' in response[0]: + if 'show run' in command or response[0] == '\n': + body = response + elif 'xml' in response[0]: body = [] else: try: @@ -318,9 +1018,9 @@ def execute_show(cmds, module, command_type=None): def execute_show_command(command, module, command_type='cli_show'): - if module.params['transport'] == 'cli': - command += ' | json' + if 'show run' not in command: + command += ' | json' cmds = [command] response = execute_show(cmds, module) body = get_cli_body_ssh(command, response, module) @@ -337,6 +1037,7 @@ def main(): vlan_range=dict(required=False), name=dict(required=False), vlan_state=dict(choices=['active', 'suspend'], required=False), + mapped_vni=dict(required=False, type='str'), state=dict(choices=['present', 'absent'], default='present', required=False), admin_state=dict(choices=['up', 'down'], required=False), @@ -351,6 +1052,7 @@ def main(): name = module.params['name'] vlan_state = module.params['vlan_state'] admin_state = module.params['admin_state'] + mapped_vni = module.params['mapped_vni'] state = module.params['state'] changed = False @@ -360,7 +1062,7 @@ def main(): module.fail_json(msg='vlan_id must be a valid VLAN ID') args = dict(name=name, vlan_state=vlan_state, - admin_state=admin_state) + admin_state=admin_state, mapped_vni=mapped_vni) proposed = dict((k, v) for k, v in args.iteritems() if v is not None) @@ -390,6 +1092,9 @@ def main(): if existing: commands = ['no vlan ' + vlan_id] elif state == 'present': + if (existing.get('mapped_vni') == '0' and + proposed.get('mapped_vni') == 'default'): + proposed.pop('mapped_vni') delta = dict(set( proposed.iteritems()).difference(existing.iteritems())) if delta or not existing: @@ -399,6 +1104,10 @@ def main(): end_state_vlans_list = existing_vlans_list if commands: + if existing: + if (existing.get('mapped_vni') != proposed.get('mapped_vni') and + existing.get('mapped_vni') != '0' and proposed.get('mapped_vni') != 'default'): + commands.insert(1, 'no vn-segment') if module.check_mode: module.exit_json(changed=True, commands=commands) @@ -416,17 +1125,11 @@ def main(): results['existing'] = existing results['end_state'] = end_state results['end_state_vlans_list'] = end_state_vlans_list - results['state'] = state results['updates'] = commands results['changed'] = changed module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 6d7326cb03fd148d295923b0bca2f06487c8acb5 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 03:29:18 +0200 Subject: [PATCH 105/770] Updating nxos_vrf --- network/nxos/nxos_vrf.py | 760 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 732 insertions(+), 28 deletions(-) diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index 4b1f8911375..3cbc7d5425f 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -39,17 +39,32 @@ options: vrf: description: - - Name of VRF to be managed. + - Name of VRF to be managed required: true admin_state: description: - - Administrative state of the VRF. + - Administrative state of the VRF required: false default: up choices: ['up','down'] + vni: + description: + - Specify virtual network identifier. Valid values are Integer + or keyword 'default'. + required: false + default: null + version_added: "2.2" + route_distinguisher: + description: + - VPN Route Distinguisher (RD). Valid values are a string in + one of the route-distinguisher formats (ASN2:NN, ASN4:NN, or + IPV4:NN); the keyword 'auto', or the keyword 'default'. + required: false + default: null + version_added: "2.2" state: description: - - Manages desired state of the resource. + - Manages desired state of the resource required: false default: present choices: ['present','absent'] @@ -62,9 +77,9 @@ EXAMPLES = ''' # ensure ntc VRF exists on switch -- nxos_vrf: vrf=ntc host={{ inventory_hostname }} +- nxos_vrf: vrf=ntc host=68.170.147.165 # ensure ntc VRF does not exist on switch -- nxos_vrf: vrf=ntc host={{ inventory_hostname }} state=absent +- nxos_vrf: vrf=ntc host=68.170.147.165 state=absent ''' RETURN = ''' @@ -85,11 +100,6 @@ type: dict sample: {"admin_state": "Up", "description": "Test test", "vrf": "ntc"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: commands sent to the device returned: always @@ -102,6 +112,683 @@ sample: true ''' +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE def execute_config_command(commands, module): try: @@ -112,7 +799,7 @@ def execute_config_command(commands, module): error=str(clie), commands=commands) -def get_cli_body_ssh_vrf(command, response): +def get_cli_body_ssh_vrf(module, command, response): """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 @@ -121,9 +808,9 @@ def get_cli_body_ssh_vrf(command, response): when using multiple |. """ command_splitted = command.split('|') - if len(command_splitted) > 2: + if len(command_splitted) > 2 or 'show run' in command: body = response - elif 'xml' in response[0]: + elif 'xml' in response[0] or response[0] == '\n': body = [] else: body = [json.loads(response[0])] @@ -145,10 +832,11 @@ def execute_show(cmds, module, command_type=None): def execute_show_command(command, module, command_type='cli_show'): if module.params['transport'] == 'cli': - command += ' | json' + if 'show run' not in command: + command += ' | json' cmds = [command] response = execute_show(cmds, module) - body = get_cli_body_ssh_vrf(command, response) + body = get_cli_body_ssh_vrf(module, command, response) elif module.params['transport'] == 'nxapi': cmds = [command] body = execute_show(cmds, module, command_type=command_type) @@ -180,6 +868,10 @@ def get_commands_to_config_vrf(delta, vrf): command = 'no shutdown' elif value.lower() == 'down': command = 'shutdown' + elif param == 'rd': + command = 'rd {0}'.format(value) + elif param == 'vni': + command = 'vni {0}'.format(value) if command: commands.append(command) if commands: @@ -188,13 +880,13 @@ def get_commands_to_config_vrf(delta, vrf): def get_vrf_description(vrf, module): - cmd_type = 'cli_show_ascii' + command_type = 'cli_show_ascii' command = ('show run section vrf | begin ^vrf\scontext\s{0} ' '| end ^vrf.*'.format(vrf)) description = '' descr_regex = ".*description\s(?P[\S+\s]+).*" - body = execute_show_command(command, module, cmd_type) + body = execute_show_command(command, module, command_type) try: body = body[0] @@ -212,6 +904,14 @@ def get_vrf_description(vrf, module): return description +def get_value(arg, config, module): + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(arg), re.M) + value = '' + if arg in config: + value = REGEX.search(config).group('value') + return value + + def get_vrf(vrf, module): command = 'show vrf {0}'.format(vrf) vrf_key = { @@ -220,15 +920,18 @@ def get_vrf(vrf, module): } body = execute_show_command(command, module) - try: vrf_table = body[0]['TABLE_vrf']['ROW_vrf'] except (TypeError, IndexError): return {} parsed_vrf = apply_key_map(vrf_key, vrf_table) - parsed_vrf['description'] = get_vrf_description( - parsed_vrf['vrf'], module) + + command = 'show run all | section vrf.context.{0}'.format(vrf) + body = execute_show_command(command, module, 'cli_show_ascii') + extra_params = ['vni', 'rd', 'description'] + for param in extra_params: + parsed_vrf[param] = get_value(param, body[0], module) return parsed_vrf @@ -237,6 +940,8 @@ def main(): argument_spec = dict( vrf=dict(required=True), description=dict(default=None, required=False), + vni=dict(required=False, type='str'), + rd=dict(required=False, type='str'), admin_state=dict(default='up', choices=['up', 'down'], required=False), state=dict(default='present', choices=['present', 'absent'], @@ -248,6 +953,8 @@ def main(): vrf = module.params['vrf'] admin_state = module.params['admin_state'].lower() description = module.params['description'] + rd = module.params['rd'] + vni = module.params['vni'] state = module.params['state'] if vrf == 'default': @@ -257,8 +964,8 @@ def main(): vrf=vrf) existing = get_vrf(vrf, module) - args = dict(vrf=vrf, description=description, - admin_state=admin_state) + args = dict(vrf=vrf, description=description, vni=vni, + admin_state=admin_state, rd=rd) end_state = existing changed = False @@ -289,6 +996,9 @@ def main(): commands.extend(command) if commands: + if proposed.get('vni'): + if existing.get('vni') and existing.get('vni') != '': + commands.insert(1, 'no vni {0}'.format(existing['vni'])) if module.check_mode: module.exit_json(changed=True, commands=cmds) else: @@ -300,17 +1010,11 @@ def main(): results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['state'] = state results['updates'] = commands results['changed'] = changed module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 73b5841b2b85a9700e559563f73ba1c8d5af523f Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 03:33:49 +0200 Subject: [PATCH 106/770] nxos_vrf_interface support for 2.1 and 2.2 --- network/nxos/nxos_vrf_interface.py | 726 +++++++++++++++++++++++++++-- 1 file changed, 697 insertions(+), 29 deletions(-) diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index 4930aff7115..df273bca566 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -24,28 +24,26 @@ description: - Manages interface specific VRF configuration extends_documentation_fragment: nxos -author: - - Jason Edelman (@jedelman8) - - Gabriele Gerbino (@GGabriele) +author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) notes: - VRF needs to be added globally with M(nxos_vrf) before - adding a VRF to an interface. + adding a VRF to an interface - Remove a VRF from an interface will still remove - all L3 attributes just as it does from CLI. + all L3 attributes just as it does from CLI - VRF is not read from an interface until IP address is - configured on that interface. + configured on that interface options: vrf: description: - - Name of VRF to be managed. + - Name of VRF to be managed required: true interface: description: - - Full name of interface to be managed, i.e. I(Ethernet1/1). + - Full name of interface to be managed, i.e. Ethernet1/1 required: true state: description: - - Manages desired state of the resource. + - Manages desired state of the resource required: false default: present choices: ['present','absent'] @@ -53,9 +51,9 @@ EXAMPLES = ''' # ensure vrf ntc exists on Eth1/1 -- nxos_vrf_interface: vrf=ntc interface=Ethernet1/1 host={{ inventory_hostname }} state=present +- nxos_vrf_interface: vrf=ntc interface=Ethernet1/1 host=68.170.147.165 state=present # ensure ntc VRF does not exist on Eth1/1 -- nxos_vrf_interface: vrf=ntc interface=Ethernet1/1 host={{ inventory_hostname }} state=absent +- nxos_vrf_interface: vrf=ntc interface=Ethernet1/1 host=68.170.147.165 state=absent ''' RETURN = ''' @@ -73,11 +71,6 @@ returned: always type: dict sample: {"interface": "loopback16", "vrf": "ntc"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: commands sent to the device returned: always @@ -90,6 +83,685 @@ sample: true ''' +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +WARNINGS = [] def execute_config_command(commands, module): try: @@ -130,7 +802,8 @@ def execute_show(cmds, module, command_type=None): def execute_show_command(command, module, command_type='cli_show'): if module.params['transport'] == 'cli': - command += ' | json' + if 'show run' not in command: + command += ' | json' cmds = [command] response = execute_show(cmds, module) body = get_cli_body_ssh_vrf_interface(command, response, module) @@ -191,17 +864,16 @@ def get_vrf_list(module): def get_interface_info(interface, module): - command = 'show run interface {0}'.format(interface) + command = 'show run | section interface.{0}'.format(interface.capitalize()) vrf_regex = ".*vrf\s+member\s+(?P\S+).*" try: body = execute_show_command(command, module, command_type='cli_show_ascii')[0] - match_vrf = re.match(vrf_regex, body, re.DOTALL) group_vrf = match_vrf.groupdict() vrf = group_vrf["vrf"] - except AttributeError: + except (AttributeError, TypeError): return "" return vrf @@ -239,9 +911,8 @@ def main(): current_vrfs = get_vrf_list(module) if vrf not in current_vrfs: - module.fail_json(msg="Ensure the VRF you're trying to config/remove on" - " an interface is created globally on the device" - " first.") + WARNINGS.append("The VRF is not present/active on the device. " + "Use nxos_vrf to fix this.") intf_type = get_interface_type(interface) if (intf_type != 'ethernet' and module.params['transport'] == 'cli'): @@ -298,17 +969,14 @@ def main(): results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['state'] = state results['updates'] = commands results['changed'] = changed + if WARNINGS: + results['warnings'] = WARNINGS + module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 982c4557d2b8218ea7be53402b242228a8ea800a Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 2 Sep 2016 18:35:14 -0700 Subject: [PATCH 107/770] Python 3 fixes for async_wrapper and async_status. (#4671) --- utilities/logic/async_status.py | 2 +- utilities/logic/async_wrapper.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/utilities/logic/async_status.py b/utilities/logic/async_status.py index 2e11a301a42..507780b46a1 100644 --- a/utilities/logic/async_status.py +++ b/utilities/logic/async_status.py @@ -79,7 +79,7 @@ def main(): data = None try: - data = file(log_path).read() + data = open(log_path).read() data = json.loads(data) except Exception: if not data: diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 320b3c5895a..b1adb9c5bcd 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -32,6 +32,8 @@ import time import syslog +PY3 = sys.version_info[0] == 3 + syslog.openlog('ansible-%s' % os.path.basename(__file__)) syslog.syslog(syslog.LOG_NOTICE, 'Invoked with %s' % " ".join(sys.argv[1:])) @@ -64,7 +66,7 @@ def daemonize_self(): e = sys.exc_info()[1] sys.exit("fork #2 failed: %d (%s)\n" % (e.errno, e.strerror)) - dev_null = file('/dev/null','rw') + dev_null = open('/dev/null', 'w') os.dup2(dev_null.fileno(), sys.stdin.fileno()) os.dup2(dev_null.fileno(), sys.stdout.fileno()) os.dup2(dev_null.fileno(), sys.stderr.fileno()) @@ -85,6 +87,9 @@ def _run_module(wrapped_cmd, jid, job_path): cmd = shlex.split(wrapped_cmd) script = subprocess.Popen(cmd, shell=False, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (outdata, stderr) = script.communicate() + if PY3: + outdata = outdata.decode('utf-8', 'surrogateescape') + stderr = stderr.decode('utf-8', 'surrogateescape') result = json.loads(outdata) if stderr: result['stderr'] = stderr From 0838736b1aca46207d53cefb7857c30d5b495a75 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 03:42:24 +0200 Subject: [PATCH 108/770] Adding nxos_igmp --- network/nxos/nxos_igmp.py | 921 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 921 insertions(+) create mode 100644 network/nxos/nxos_igmp.py diff --git a/network/nxos/nxos_igmp.py b/network/nxos/nxos_igmp.py new file mode 100644 index 00000000000..36d59ae75a4 --- /dev/null +++ b/network/nxos/nxos_igmp.py @@ -0,0 +1,921 @@ +#!/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_igmp +version_added: "2.2" +short_description: Manages IGMP global configuration +description: + - Manages IGMP global configuration configuration settings +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - When state=default, all supported params will be reset to a default state + - If restart is set to true with other params set, the restart will happen + last, i.e. after the configuration takes place +options: + flush_routes: + description: + - Removes routes when the IGMP process is restarted. By default, + routes are not flushed. + required: false + default: null + choices: ['true', 'false'] + enforce_rtr_alert: + description: + - Enables or disables the enforce router alert option check for + IGMPv2 and IGMPv3 packets + required: false + default: null + choices: ['true', 'false'] + restart: + description: + - restarts the igmp process (using an exec config command) + required: false + default: null + choices: ['true', 'false'] + state: + description: + - Manages desired state of the resource + required: false + default: present + choices: ['present', 'default'] +''' +EXAMPLES = ''' +# default igmp global params (all params except restart) +- nxos_igmp: state=default host={{ inventory_hostname }} +# ensure the following igmp global config exists on the device +- nxos_igmp: flush_routes=true enforce_rtr_alert=true host={{ inventory_hostname }} +# restart the igmp process +- nxos_igmp: restart=true host={{ inventory_hostname }} +''' + +EXAMPLES = ''' +# configure a simple asn +- nxos_bgp: + asn=65535 + vrf=test + router_id=1.1.1.1 + state=present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: when C(m_facts)=true + type: dict + sample: {"enforce_rtr_alert": true, "flush_routes": true} +existing: + description: k/v pairs of existing IGMP configuration + returned: when C(m_facts)=true + type: dict + sample: {"enforce_rtr_alert": true, "flush_routes": false} +end_state: + description: k/v pairs of IGMP configuration after module execution + returned: when C(m_facts)=true + type: dict + sample: {"enforce_rtr_alert": true, "flush_routes": true} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["ip igmp flush-routes"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + +PARAM_TO_COMMAND_KEYMAP = { + 'flush_routes': 'ip igmp flush-routes', + 'enforce_rtr_alert': 'ip igmp enforce-router-alert' +} + + +def get_value(arg, config): + REGEX = re.compile(r'{0}\s*$'.format(PARAM_TO_COMMAND_KEYMAP[arg]), re.M) + value = False + try: + if REGEX.search(config): + value = True + except TypeError: + value = False + return value + + +def get_existing(module, args): + existing = {} + config = str(custom_get_config(module)) + + for arg in args: + existing[arg] = get_value(arg, config) + return existing + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +def get_commands(module, existing, proposed, candidate): + commands = list() + proposed_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, proposed) + existing_commands = apply_key_map(PARAM_TO_COMMAND_KEYMAP, existing) + if module.params['state'] == 'default': + for key, value in proposed_commands.iteritems(): + if existing_commands.get(key): + commands.append('no {0}'.format(key)) + else: + for key, value in proposed_commands.iteritems(): + if value is True: + commands.append(key) + else: + if existing_commands.get(key): + commands.append('no {0}'.format(key)) + + if module.params['restart']: + commands.append('restart igmp') + + if commands: + parents = [] + candidate.add(commands, parents=parents) + + +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] = value + else: + new_dict[new_key] = value + return new_dict + + +def main(): + argument_spec = dict( + flush_routes=dict(type='bool'), + enforce_rtr_alert=dict(type='bool'), + restart=dict(type='bool', default=False), + state=dict(choices=['present', 'default'], default='present'), + m_facts=dict(required=False, default=False, type='bool'), + include_defaults=dict(default=False) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + restart = module.params['restart'] + + if (state == 'default' and (module.params['flush_routes'] is not None or + module.params['enforce_rtr_alert'] is not None)): + module.fail_json(msg='When state=default other params have no effect.') + + args = [ + "flush_routes", + "enforce_rtr_alert", + ] + + existing = invoke('get_existing', module, args) + end_state = existing + + proposed = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + proposed_args = proposed.copy() + if state == 'default': + proposed_args = dict((k, False) for k in args) + + result = {} + if (state == 'present' or (state == 'default' and + True in existing.values()) or restart): + candidate = CustomNetworkConfig(indent=3) + invoke('get_commands', module, existing, proposed_args, candidate) + + try: + response = load_config(module, candidate) + result.update(response) + except ShellError: + exc = get_exception() + module.fail_json(msg=str(exc)) + else: + result['updates'] = [] + + if restart: + proposed['restart'] = restart + result['connected'] = module.connected + if module.params['m_facts']: + end_state = invoke('get_existing', module, args) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = proposed + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From a9dc536174c5ba1ad57e38573bf02b181facc182 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 04:09:02 +0200 Subject: [PATCH 109/770] Adding nxos_igmp_interface --- network/nxos/nxos_igmp_interface.py | 1402 +++++++++++++++++++++++++++ 1 file changed, 1402 insertions(+) create mode 100644 network/nxos/nxos_igmp_interface.py diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py new file mode 100644 index 00000000000..714fb9f649a --- /dev/null +++ b/network/nxos/nxos_igmp_interface.py @@ -0,0 +1,1402 @@ +#!/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_igmp_interface +version_added: "2.2" +short_description: Manages IGMP interface configuration +description: + - Manages IGMP interface configuration settings +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - When state=default, supported params will be reset to a default state. + These include: version, startup_query_interval, startup_query_count, + robustness, querier_timeout, query_mrt, query_interval, last_member_qrt, + last_member_query_count, group_timeout, report_llg, and immediate_leave + - When state=absent, all configs for oif_prefix, oif_source, and + oif_routemap will be removed. + - PIM must be enabled to use this module + - This module is for Layer 3 interfaces + - Route-map check not performed (same as CLI) check when configuring + route-map with 'static-oif' + - If restart is set to true with other params set, the restart will happen + last, i.e. after the configuration takes place + - While username and password are not required params, they are + if you are not using the .netauth file. .netauth file is recommended + as it will clean up the each task in the playbook by not requiring + the username and password params for every tasks. + - Using the username and password params will override the .netauth file +options: + interface: + description: + - The FULL interface name for IGMP configuration. + required: true + version: + description: + - IGMP version. It can be 2 or 3. + required: false + default: null + choices: ['2', '3'] + startup_query_interval: + description: + - Query interval used when the IGMP process starts up. + The range is from 1 to 18000. The default is 31. + required: false + default: null + startup_query_count: + description: + - Query count used when the IGMP process starts up. + The range is from 1 to 10. The default is 2. + required: false + default: null + robustness: + description: + - Sets the robustness variable. Values can range from 1 to 7. + The default is 2. + required: false + default: null + querier_timeout: + description: + - Sets the querier timeout that the software uses when deciding + to take over as the querier. Values can range from 1 to 65535 + seconds. The default is 255 seconds. + required: false + default: null + query_mrt: + description: + - Sets the response time advertised in IGMP queries. + Values can range from 1 to 25 seconds. The default is 10 seconds. + required: false + default: null + query_interval: + description: + - Sets the frequency at which the software sends IGMP host query + messages. Values can range from 1 to 18000 seconds. + he default is 125 seconds. + required: false + default: null + last_member_qrt: + description: + - Sets the query interval waited after sending membership reports + before the software deletes the group state. Values can range + from 1 to 25 seconds. The default is 1 second + required: false + default: null + last_member_query_count: + description: + - Sets the number of times that the software sends an IGMP query + in response to a host leave message. + Values can range from 1 to 5. The default is 2. + required: false + default: null + group_timeout: + description: + - Sets the group membership timeout for IGMPv2. + Values can range from 3 to 65,535 seconds. + The default is 260 seconds. + required: false + default: null + report_llg: + description: + - Configures report-link-local-groups. + Enables sending reports for groups in 224.0.0.0/24. + Reports are always sent for nonlink local groups. + By default, reports are not sent for link local groups. + required: false + choices: ['true', 'false'] + default: false + immediate_leave: + description: + - Enables the device to remove the group entry from the multicast + routing table immediately upon receiving a leave message for + the group. Use this command to minimize the leave latency of + IGMPv2 group memberships on a given IGMP interface because the + device does not send group-specific queries. + The default is disabled. + required: false + choices: ['true', 'false'] + default: false + oif_routemap: + description: + - Configure a routemap for static outgoing interface (OIF). + required: false + default: null + oif_prefix: + description: + - Configure a prefix for static outgoing interface (OIF). + required: false + default: null + oif_source: + description: + - Configure a source for static outgoing interface (OIF). + required: false + default: null + restart: + description: + - Restart IGMP + required: false + choices: ['true', 'false'] + default: null + state: + description: + - Manages desired state of the resource + required: false + default: present + choices: ['present', 'default'] +''' +EXAMPLES = ''' +- nxos_igmp_interface: + interface: ethernet1/32 + startup_query_interval: 30 + state: present + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"} +existing: + description: k/v pairs of existing BGP configuration + type: dict + sample: {"asn": "65535", "bestpath_always_compare_med": false, + "bestpath_aspath_multipath_relax": false, + "bestpath_compare_neighborid": false, + "bestpath_compare_routerid": false, + "bestpath_cost_community_ignore": false, + "bestpath_med_confed": false, + "bestpath_med_missing_as_worst": false, + "bestpath_med_non_deterministic": false, "cluster_id": "", + "confederation_id": "", "confederation_peers": "", + "graceful_restart": true, "graceful_restart_helper": false, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", "local_as": "", + "log_neighbor_changes": false, "maxas_limit": "", + "neighbor_down_fib_accelerate": false, "reconnect_interval": "60", + "router_id": "11.11.11.11", "suppress_fib_pending": false, + "timer_bestpath_limit": "", "timer_bgp_hold": "180", + "timer_bgp_keepalive": "60", "vrf": "test"} +end_state: + description: k/v pairs of BGP configuration after module execution + returned: always + type: dict + sample: {"asn": "65535", "bestpath_always_compare_med": false, + "bestpath_aspath_multipath_relax": false, + "bestpath_compare_neighborid": false, + "bestpath_compare_routerid": false, + "bestpath_cost_community_ignore": false, + "bestpath_med_confed": false, + "bestpath_med_missing_as_worst": false, + "bestpath_med_non_deterministic": false, "cluster_id": "", + "confederation_id": "", "confederation_peers": "", + "graceful_restart": true, "graceful_restart_helper": false, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", "local_as": "", + "log_neighbor_changes": false, "maxas_limit": "", + "neighbor_down_fib_accelerate": false, "reconnect_interval": "60", + "router_id": "1.1.1.1", "suppress_fib_pending": false, + "timer_bestpath_limit": "", "timer_bgp_hold": "180", + "timer_bgp_keepalive": "60", "vrf": "test"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# COMMON CODE FOR MIGRATION + +import re +import time +import collections +import itertools +import shlex +import json + +from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception +from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE +from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO +from ansible.module_utils.netcfg import parse +from ansible.module_utils.urls import fetch_url + + +DEFAULT_COMMENT_TOKENS = ['#', '!'] + +class ConfigLine(object): + + def __init__(self, text): + self.text = text + self.children = list() + self.parents = list() + self.raw = None + + @property + def line(self): + line = ['set'] + line.extend([p.text for p in self.parents]) + line.append(self.text) + return ' '.join(line) + + def __str__(self): + return self.raw + + def __eq__(self, other): + if self.text == other.text: + return self.parents == other.parents + + def __ne__(self, other): + return not self.__eq__(other) + +def ignore_line(text, tokens=None): + for item in (tokens or DEFAULT_COMMENT_TOKENS): + if text.startswith(item): + return True + +def get_next(iterable): + item, next_item = itertools.tee(iterable, 2) + next_item = itertools.islice(next_item, 1, None) + return itertools.izip_longest(item, next_item) + +def parse(lines, indent, comment_tokens=None): + toplevel = re.compile(r'\S') + childline = re.compile(r'^\s*(.+)$') + + ancestors = list() + config = list() + + for line in str(lines).split('\n'): + text = str(re.sub(r'([{};])', '', line)).strip() + + cfg = ConfigLine(text) + cfg.raw = line + + if not text or ignore_line(text, comment_tokens): + continue + + # handle top level commands + if toplevel.match(line): + ancestors = [cfg] + + # handle sub level commands + else: + match = childline.match(line) + line_indent = match.start(1) + level = int(line_indent / indent) + parent_level = level - 1 + + cfg.parents = ancestors[:level] + + if level > len(ancestors): + config.append(cfg) + continue + + for i in range(level, len(ancestors)): + ancestors.pop() + + ancestors.append(cfg) + ancestors[parent_level].children.append(cfg) + + config.append(cfg) + + return config + + +class CustomNetworkConfig(object): + + def __init__(self, indent=None, contents=None, device_os=None): + self.indent = indent or 1 + self._config = list() + self._device_os = device_os + + if contents: + self.load(contents) + + @property + def items(self): + return self._config + + @property + def lines(self): + lines = list() + for item, next_item in get_next(self.items): + if next_item is None: + lines.append(item.line) + elif not next_item.line.startswith(item.line): + lines.append(item.line) + return lines + + def __str__(self): + text = '' + for item in self.items: + if not item.parents: + expand = self.get_section(item.text) + text += '%s\n' % self.get_section(item.text) + return str(text).strip() + + def load(self, contents): + self._config = parse(contents, indent=self.indent) + + def load_from_file(self, filename): + self.load(open(filename).read()) + + def get(self, path): + if isinstance(path, basestring): + path = [path] + for item in self._config: + if item.text == path[-1]: + parents = [p.text for p in item.parents] + if parents == path[:-1]: + return item + + def search(self, regexp, path=None): + regex = re.compile(r'^%s' % regexp, re.M) + + if path: + parent = self.get(path) + if not parent or not parent.children: + return + children = [c.text for c in parent.children] + data = '\n'.join(children) + else: + data = str(self) + + match = regex.search(data) + if match: + if match.groups(): + values = match.groupdict().values() + groups = list(set(match.groups()).difference(values)) + return (groups, match.groupdict()) + else: + return match.group() + + def findall(self, regexp): + regexp = r'%s' % regexp + return re.findall(regexp, str(self)) + + def expand(self, obj, items): + block = [item.raw for item in obj.parents] + block.append(obj.raw) + + current_level = items + for b in block: + if b not in current_level: + current_level[b] = collections.OrderedDict() + current_level = current_level[b] + for c in obj.children: + if c.raw not in current_level: + current_level[c.raw] = collections.OrderedDict() + + def to_lines(self, section): + lines = list() + for entry in section[1:]: + line = ['set'] + line.extend([p.text for p in entry.parents]) + line.append(entry.text) + lines.append(' '.join(line)) + return lines + + 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) + if self._device_os == 'junos': + return self.to_lines(section) + 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 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 flatten(self, data, obj=None): + if obj is None: + obj = list() + for k, v in data.items(): + obj.append(k) + self.flatten(v, obj) + return obj + + 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 get_children(self, path): + obj = self.get_object(path) + if obj: + return obj.children + + def difference(self, other, path=None, match='line', replace='line'): + updates = list() + + config = self.items + if path: + config = self.get_children(path) or list() + + if match == 'line': + for item in config: + if item not in other.items: + updates.append(item) + + elif match == 'strict': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + for index, item in enumerate(config): + try: + if item != current[index]: + updates.append(item) + except IndexError: + updates.append(item) + + elif match == 'exact': + if path: + current = other.get_children(path) or list() + else: + current = other.items + + if len(current) != len(config): + updates.extend(config) + else: + for ours, theirs in itertools.izip(config, current): + if ours != theirs: + updates.extend(config) + break + + if self._device_os == 'junos': + return updates + + diffs = collections.OrderedDict() + for update in updates: + if replace == 'block' and update.parents: + update = update.parents[-1] + self.expand(update, diffs) + + return self.flatten(diffs) + + def replace(self, replace, text=None, regex=None, parents=None, + add_if_missing=False, ignore_whitespace=False): + match = None + + parents = parents or list() + if text is None and regex is None: + raise ValueError('missing required arguments') + + if not regex: + regex = ['^%s$' % text] + + patterns = [re.compile(r, re.I) for r in to_list(regex)] + + for item in self.items: + for regexp in patterns: + if ignore_whitespace is True: + string = item.text + else: + string = item.raw + if regexp.search(item.text): + if item.text != replace: + if parents == [p.text for p in item.parents]: + match = item + break + + if match: + match.text = replace + indent = len(match.raw) - len(match.raw.lstrip()) + match.raw = replace.rjust(len(replace) + indent) + + elif add_if_missing: + self.add(replace, parents=parents) + + + 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 argument_spec(): + return dict( + # config options + running_config=dict(aliases=['config']), + save_config=dict(type='bool', default=False, aliases=['save']) + ) +nxos_argument_spec = argument_spec() + + +NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) + +NET_COMMON_ARGS = dict( + host=dict(required=True), + port=dict(type='int'), + username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), + password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), + ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), + transport=dict(default='cli', choices=['cli', 'nxapi']), + use_ssl=dict(default=False, type='bool'), + validate_certs=dict(default=True, type='bool'), + provider=dict(type='dict'), + timeout=dict(default=10, type='int') +) + +NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] + +NXAPI_ENCODINGS = ['json', 'xml'] + +CLI_PROMPTS_RE = [ + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), + re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') +] + +CLI_ERRORS_RE = [ + re.compile(r"% ?Error"), + re.compile(r"^% \w+", re.M), + re.compile(r"% ?Bad secret"), + re.compile(r"invalid input", re.I), + re.compile(r"(?:incomplete|ambiguous) command", re.I), + re.compile(r"connection timed out", re.I), + re.compile(r"[^\r\n]+ not found", re.I), + re.compile(r"'[^']' +returned error code: ?\d+"), + re.compile(r"syntax error"), + re.compile(r"unknown command") +] + + +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() + + +class Nxapi(object): + + def __init__(self, module): + self.module = module + + # sets the module_utils/urls.py req parameters + self.module.params['url_username'] = module.params['username'] + self.module.params['url_password'] = module.params['password'] + + self.url = None + self._nxapi_auth = None + + def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): + """Encodes a NXAPI JSON request message + """ + if isinstance(commands, (list, set, tuple)): + commands = ' ;'.join(commands) + + if encoding not in NXAPI_ENCODINGS: + msg = 'invalid encoding, received %s, exceped one of %s' % \ + (encoding, ','.join(NXAPI_ENCODINGS)) + self.module_fail_json(msg=msg) + + msg = { + 'version': version, + 'type': command_type, + 'chunk': chunk, + 'sid': sid, + 'input': commands, + 'output_format': encoding + } + return dict(ins_api=msg) + + def connect(self): + host = self.module.params['host'] + port = self.module.params['port'] + + if self.module.params['use_ssl']: + proto = 'https' + if not port: + port = 443 + else: + proto = 'http' + if not port: + port = 80 + + self.url = '%s://%s:%s/ins' % (proto, host, port) + + def send(self, commands, command_type='cli_show_ascii', encoding='json'): + """Send commands to the device. + """ + clist = to_list(commands) + + if command_type not in NXAPI_COMMAND_TYPES: + msg = 'invalid command_type, received %s, exceped one of %s' % \ + (command_type, ','.join(NXAPI_COMMAND_TYPES)) + self.module_fail_json(msg=msg) + + data = self._get_body(clist, command_type, encoding) + data = self.module.jsonify(data) + + headers = {'Content-Type': 'application/json'} + if self._nxapi_auth: + headers['Cookie'] = self._nxapi_auth + + response, headers = fetch_url(self.module, self.url, data=data, + headers=headers, method='POST') + + self._nxapi_auth = headers.get('set-cookie') + + if headers['status'] != 200: + self.module.fail_json(**headers) + + response = self.module.from_json(response.read()) + result = list() + + output = response['ins_api']['outputs']['output'] + for item in to_list(output): + if item['code'] != '200': + self.module.fail_json(**item) + else: + result.append(item['body']) + + return result + + +class Cli(object): + + def __init__(self, module): + self.module = module + self.shell = None + + def connect(self, **kwargs): + host = self.module.params['host'] + port = self.module.params['port'] or 22 + + username = self.module.params['username'] + password = self.module.params['password'] + timeout = self.module.params['timeout'] + key_filename = self.module.params['ssh_keyfile'] + + allow_agent = (key_filename is not None) or (key_filename is None and password is None) + + try: + self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, + errors_re=CLI_ERRORS_RE) + self.shell.open(host, port=port, username=username, + password=password, key_filename=key_filename, + allow_agent=allow_agent, timeout=timeout) + except ShellError: + e = get_exception() + msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) + self.module.fail_json(msg=msg) + + def send(self, commands, encoding='text'): + try: + return self.shell.send(commands) + except ShellError: + e = get_exception() + self.module.fail_json(msg=e.message, commands=commands) + + +class NetworkModule(AnsibleModule): + + def __init__(self, *args, **kwargs): + super(NetworkModule, self).__init__(*args, **kwargs) + self.connection = None + self._config = None + self._connected = False + + @property + def connected(self): + return self._connected + + @property + def config(self): + if not self._config: + self._config = self.get_config() + return self._config + + def _load_params(self): + super(NetworkModule, self)._load_params() + provider = self.params.get('provider') or dict() + for key, value in provider.items(): + if key in NET_COMMON_ARGS: + if self.params.get(key) is None and value is not None: + self.params[key] = value + + def connect(self): + cls = globals().get(str(self.params['transport']).capitalize()) + try: + self.connection = cls(self) + except TypeError: + e = get_exception() + self.fail_json(msg=e.message) + + self.connection.connect() + + if self.params['transport'] == 'cli': + self.connection.send('terminal length 0') + + self._connected = True + + def configure(self, commands): + commands = to_list(commands) + if self.params['transport'] == 'cli': + return self.configure_cli(commands) + else: + return self.execute(commands, command_type='cli_conf') + + def configure_cli(self, commands): + commands = to_list(commands) + commands.insert(0, 'configure') + responses = self.execute(commands) + responses.pop(0) + return responses + + def execute(self, commands, **kwargs): + if not self.connected: + self.connect() + return self.connection.send(commands, **kwargs) + + def disconnect(self): + self.connection.close() + self._connected = False + + def parse_config(self, cfg): + return parse(cfg, indent=2) + + def get_config(self): + cmd = 'show running-config' + if self.params.get('include_defaults'): + cmd += ' all' + response = self.execute(cmd) + return response[0] + + +def get_module(**kwargs): + """Return instance of NetworkModule + """ + argument_spec = NET_COMMON_ARGS.copy() + if kwargs.get('argument_spec'): + argument_spec.update(kwargs['argument_spec']) + kwargs['argument_spec'] = argument_spec + + module = NetworkModule(**kwargs) + + if module.params['transport'] == 'cli' and not HAS_PARAMIKO: + module.fail_json(msg='paramiko is required but does not appear to be installed') + + return module + + +def custom_get_config(module, include_defaults=False): + config = module.params['running_config'] + if not config: + cmd = 'show running-config' + if module.params['include_defaults']: + cmd += ' all' + if module.params['transport'] == 'nxapi': + config = module.execute([cmd], command_type='cli_show_ascii')[0] + else: + config = module.execute([cmd])[0] + + return CustomNetworkConfig(indent=2, contents=config) + +def load_config(module, candidate): + config = custom_get_config(module) + + commands = candidate.difference(config) + commands = [str(c).strip() for c in commands] + + save_config = module.params['save_config'] + + result = dict(changed=False) + + if commands: + if not module.check_mode: + module.configure(commands) + if save_config: + module.config.save_config() + + result['changed'] = True + result['updates'] = commands + + return result +# END OF COMMON CODE + + +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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + response = response[0].replace(command + '\n\n', '').strip() + body = [json.loads(response)] + except ValueError: + module.fail_json(msg='Command does not support JSON output', + command=command) + return body + + +def execute_show(cmds, module, command_type=None): + 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(command), + 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 get_interface_mode(interface, intf_type, module): + command = 'show interface {0}'.format(interface) + interface = {} + mode = 'unknown' + + if intf_type in ['ethernet', 'portchannel']: + body = execute_show_command(command, module)[0] + interface_table = body['TABLE_interface']['ROW_interface'] + mode = str(interface_table.get('eth_mode', 'layer3')) + if mode == 'access' or mode == 'trunk': + mode = 'layer2' + elif intf_type == 'loopback' or intf_type == 'svi': + mode = 'layer3' + return mode + + +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 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] = value + else: + new_dict[new_key] = value + return new_dict + + +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 get_igmp_interface(module, interface): + command = 'show ip igmp interface {0}'.format(interface) + igmp = {} + + key_map = { + 'IGMPVersion': 'version', + 'ConfiguredStartupQueryInterval': 'startup_query_interval', + 'StartupQueryCount': 'startup_query_count', + 'RobustnessVariable': 'robustness', + 'QuerierTimeout': 'querier_timeout', + 'ConfiguredMaxResponseTime': 'query_mrt', + 'ConfiguredQueryInterval': 'query_interval', + 'LastMemberMTR': 'last_member_qrt', + 'LastMemberQueryCount': 'last_member_query_count', + 'ConfiguredGroupTimeout': 'group_timeout' + } + + body = execute_show_command(command, module)[0] + + if body: + resource = body['TABLE_vrf']['ROW_vrf']['TABLE_if']['ROW_if'] + igmp = apply_key_map(key_map, resource) + report_llg = str(resource['ReportingForLinkLocal']) + if report_llg == 'true': + igmp['report_llg'] = True + elif report_llg == 'false': + igmp['report_llg'] = False + + immediate_leave = str(resource['ImmediateLeave']) # returns en or dis + if immediate_leave == 'en': + igmp['immediate_leave'] = True + elif immediate_leave == 'dis': + igmp['immediate_leave'] = False + + # the next block of code is used to retrieve anything with: + # ip igmp static-oif *** i.e.. could be route-map ROUTEMAP + # or PREFIX source , etc. + command = 'show run interface {0} | inc oif'.format(interface) + + body = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + + staticoif = [] + if body: + split_body = body.split('\n') + route_map_regex = ('.*ip igmp static-oif route-map\s+' + '(?P\S+).*') + prefix_source_regex = ('.*ip igmp static-oif\s+(?P' + '((\d+.){3}\d+))(\ssource\s' + '(?P\S+))?.*') + + for line in split_body: + temp = {} + try: + match_route_map = re.match(route_map_regex, line, re.DOTALL) + route_map = match_route_map.groupdict()['route_map'] + except AttributeError: + route_map = '' + + try: + match_prefix_source = re.match( + prefix_source_regex, line, re.DOTALL) + prefix_source_group = match_prefix_source.groupdict() + prefix = prefix_source_group['prefix'] + source = prefix_source_group['source'] + except AttributeError: + prefix = '' + source = '' + + if route_map: + temp['route_map'] = route_map + if prefix: + temp['prefix'] = prefix + if source: + temp['source'] = source + if temp: + staticoif.append(temp) + + igmp['oif_routemap'] = None + igmp['oif_prefix_source'] = [] + + if staticoif: + if len(staticoif) == 1 and staticoif[0].get('route_map'): + igmp['oif_routemap'] = staticoif[0]['route_map'] + else: + igmp['oif_prefix_source'] = staticoif + + return igmp + + +def config_igmp_interface(delta, found_both, found_prefix): + CMDS = { + 'version': 'ip igmp version {0}', + 'startup_query_interval': 'ip igmp startup-query-interval {0}', + 'startup_query_count': 'ip igmp startup-query-count {0}', + 'robustness': 'ip igmp robustness-variable {0}', + 'querier_timeout': 'ip igmp querier-timeout {0}', + 'query_mrt': 'ip igmp query-max-response-time {0}', + 'query_interval': 'ip igmp query-interval {0}', + 'last_member_qrt': 'ip igmp last-member-query-response-time {0}', + 'last_member_query_count': 'ip igmp last-member-query-count {0}', + 'group_timeout': 'ip igmp group-timeout {0}', + 'report_llg': 'ip igmp report-link-local-groups', + 'immediate_leave': 'ip igmp immediate-leave', + 'oif_prefix_source': 'ip igmp static-oif {0} source {1} ', + 'oif_routemap': 'ip igmp static-oif route-map {0}', + 'oif_prefix': 'ip igmp static-oif {0}', + } + + commands = [] + command = None + + for key, value in delta.iteritems(): + if key == 'oif_source' or found_both or found_prefix: + pass + elif key == 'oif_prefix': + if delta.get('oif_source'): + command = CMDS.get('oif_prefix_source').format( + delta.get('oif_prefix'), delta.get('oif_source')) + else: + command = CMDS.get('oif_prefix').format( + delta.get('oif_prefix')) + elif value: + command = CMDS.get(key).format(value) + elif not value: + command = 'no {0}'.format(CMDS.get(key).format(value)) + + if command: + if command not in commands: + commands.append(command) + command = None + + return commands + + +def get_igmp_interface_defaults(): + version = '2' + startup_query_interval = '31' + startup_query_count = '2' + robustness = '2' + querier_timeout = '255' + query_mrt = '10' + query_interval = '125' + last_member_qrt = '1' + last_member_query_count = '2' + group_timeout = '260' + report_llg = False + immediate_leave = False + + args = dict(version=version, startup_query_interval=startup_query_interval, + startup_query_count=startup_query_count, robustness=robustness, + querier_timeout=querier_timeout, query_mrt=query_mrt, + query_interval=query_interval, last_member_qrt=last_member_qrt, + last_member_query_count=last_member_query_count, + group_timeout=group_timeout, report_llg=report_llg, + immediate_leave=immediate_leave) + + default = dict((param, value) for (param, value) in args.iteritems() + if value is not None) + + return default + + +def config_default_igmp_interface(existing, delta, found_both, found_prefix): + commands = [] + proposed = get_igmp_interface_defaults() + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + if delta: + command = config_igmp_interface(delta, found_both, found_prefix) + + if command: + for each in command: + commands.append(each) + + return commands + + +def config_remove_oif(existing, existing_oif_prefix_source): + commands = [] + command = None + if existing.get('routemap'): + command = 'no ip igmp static-oif route-map {0}'.format( + existing.get('routemap')) + if existing_oif_prefix_source: + for each in existing_oif_prefix_source: + if each.get('prefix') and each.get('source'): + command = 'no ip igmp static-oif {0} source {1} '.format( + each.get('prefix'), each.get('source') + ) + elif each.get('prefix'): + command = 'no ip igmp static-oif {0}'.format( + each.get('prefix') + ) + if command: + commands.append(command) + command = None + + return commands + + +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) + + +def main(): + argument_spec = dict( + interface=dict(required=True, type='str'), + version=dict(required=False, type='str'), + startup_query_interval=dict(required=False, type='str'), + startup_query_count=dict(required=False, type='str'), + robustness=dict(required=False, type='str'), + querier_timeout=dict(required=False, type='str'), + query_mrt=dict(required=False, type='str'), + query_interval=dict(required=False, type='str'), + last_member_qrt=dict(required=False, type='str'), + last_member_query_count=dict(required=False, type='str'), + group_timeout=dict(required=False, type='str'), + report_llg=dict(type='bool'), + immediate_leave=dict(type='bool'), + oif_routemap=dict(required=False, type='str'), + oif_prefix=dict(required=False, type='str'), + oif_source=dict(required=False, type='str'), + restart=dict(type='bool', default=False), + state=dict(choices=['present', 'absent', 'default'], + default='present'), + include_defaults=dict(default=True) + ) + argument_spec.update(nxos_argument_spec) + module = get_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + interface = module.params['interface'] + oif_prefix = module.params['oif_prefix'] + oif_source = module.params['oif_source'] + oif_routemap = module.params['oif_routemap'] + + if oif_source: + if not oif_prefix: + module.fail_json(msg='oif_prefix required when setting oif_source') + + 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 oif_prefix and oif_routemap: + module.fail_json(msg='cannot use oif_prefix AND oif_routemap.' + ' select one.') + + existing = get_igmp_interface(module, interface) + existing_copy = existing.copy() + end_state = existing_copy + + if not existing.get('version'): + module.fail_json(msg='pim needs to be enabled on the interface') + + existing_oif_prefix_source = existing.get('oif_prefix_source') + # not json serializable + existing.pop('oif_prefix_source') + + if oif_routemap and existing_oif_prefix_source: + module.fail_json(msg='Delete static-oif configurations on this ' + 'interface if you want to use a routemap') + + if oif_prefix and existing.get('oif_routemap'): + module.fail_json(msg='Delete static-oif route-map configuration ' + 'on this interface if you want to config ' + 'static entries') + + args = [ + 'version', + 'startup_query_interval', + 'startup_query_count', + 'robustness', + 'querier_timeout', + 'query_mrt', + 'query_interval', + 'last_member_qrt', + 'last_member_query_count', + 'group_timeout', + 'report_llg', + 'immediate_leave', + 'oif_routemap', + 'oif_prefix', + 'oif_source' + ] + + changed = False + commands = [] + proposed = dict((k, v) for k, v in module.params.iteritems() + if v is not None and k in args) + + CANNOT_ABSENT = ['version', 'startup_query_interval', + 'startup_query_count', 'robustness', 'querier_timeout', + 'query_mrt', 'query_interval', 'last_member_qrt', + 'last_member_query_count', 'group_timeout', 'report_llg', + 'immediate_leave'] + + if state == 'absent': + for each in CANNOT_ABSENT: + if each in proposed.keys(): + module.fail_json(msg='only params: oif_prefix, oif_source, ' + 'oif_routemap can be used when ' + 'state=absent') + + # delta check for all params except oif_prefix and oif_source + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + # now check to see there is a delta for prefix and source command option + found_both = False + found_prefix = False + + if existing_oif_prefix_source: + if oif_prefix and oif_source: + for each in existing_oif_prefix_source: + if (oif_prefix == each.get('prefix') and + oif_source == each.get('source')): + found_both = True + if not found_both: + delta['prefix'] = oif_prefix + delta['source'] = oif_source + elif oif_prefix: + for each in existing_oif_prefix_source: + if oif_prefix == each.get('prefix') and not each.get('source'): + found_prefix = True + if not found_prefix: + delta['prefix'] = oif_prefix + + if state == 'present': + if delta: + command = config_igmp_interface(delta, found_both, found_prefix) + if command: + commands.append(command) + + elif state == 'default': + command = config_default_igmp_interface(existing, delta, + found_both, found_prefix) + if command: + commands.append(command) + elif state == 'absent': + command = None + if existing.get('oif_routemap') or existing_oif_prefix_source: + command = config_remove_oif(existing, existing_oif_prefix_source) + + if command: + commands.append(command) + + command = config_default_igmp_interface(existing, delta, + found_both, found_prefix) + if command: + commands.append(command) + + if module.params['restart']: + commands.append('restart igmp') + + cmds = [] + results = {} + if commands: + commands.insert(0, ['interface {0}'.format(interface)]) + cmds = flatten_list(commands) + + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + execute_config_command(cmds, module) + changed = True + end_state = get_igmp_interface(module, interface) + + results['proposed'] = proposed + results['existing'] = existing_copy + results['updates'] = cmds + results['changed'] = changed + results['end_state'] = end_state + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From bafdedd25b661f2667da95b2df9c962ab5e4befd Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sat, 3 Sep 2016 04:16:21 +0200 Subject: [PATCH 110/770] Fix docstring --- network/nxos/nxos_igmp_interface.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index 714fb9f649a..02da82ac421 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -29,7 +29,7 @@ - Gabriele Gerbino (@GGabriele) notes: - When state=default, supported params will be reset to a default state. - These include: version, startup_query_interval, startup_query_count, + These include version, startup_query_interval, startup_query_count, robustness, querier_timeout, query_mrt, query_interval, last_member_qrt, last_member_query_count, group_timeout, report_llg, and immediate_leave - When state=absent, all configs for oif_prefix, oif_source, and @@ -40,11 +40,6 @@ route-map with 'static-oif' - If restart is set to true with other params set, the restart will happen last, i.e. after the configuration takes place - - While username and password are not required params, they are - if you are not using the .netauth file. .netauth file is recommended - as it will clean up the each task in the playbook by not requiring - the username and password params for every tasks. - - Using the username and password params will override the .netauth file options: interface: description: From 0dfd3f177fda3d6fbfffcebc4939744807f6e49a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 2 Sep 2016 22:36:22 -0400 Subject: [PATCH 111/770] update junos_template module This updates the junos_template module implementing the changes for Ansible 2.2 --- network/junos/junos_template.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/network/junos/junos_template.py b/network/junos/junos_template.py index 8bbdbc19f81..2f8e64d5b5e 100644 --- a/network/junos/junos_template.py +++ b/network/junos/junos_template.py @@ -100,6 +100,7 @@ src: config.j2 action: overwrite """ +from ansible.module_utils.junos import NetworkModule DEFAULT_COMMENT = 'configured by junos_template' @@ -115,8 +116,8 @@ def main(): transport=dict(default='netconf', choices=['netconf']) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + supports_check_mode=True) comment = module.params['comment'] confirm = module.params['confirm'] @@ -132,9 +133,9 @@ def main(): "set per junos documentation") results = dict(changed=False) - results['_backup'] = str(module.get_config()).strip() + results['_backup'] = str(module.config.get_config()).strip() - diff = module.load_config(src, action=action, comment=comment, + diff = module.config.load_config(src, action=action, comment=comment, format=fmt, commit=commit, confirm=confirm) if diff: @@ -144,8 +145,5 @@ def main(): module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.junos import * - if __name__ == '__main__': main() From ba8613cae90c61809daa365210fefeef7a57719a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 2 Sep 2016 22:40:38 -0400 Subject: [PATCH 112/770] update junos_netconf module Updates the junos_netconf module with changes to load the NetworkModule instead of the get_module factory method. This update is part of the 2.2 refactor of network modules --- network/junos/junos_netconf.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/network/junos/junos_netconf.py b/network/junos/junos_netconf.py index 3db3fa7347c..fa9ca4115e6 100644 --- a/network/junos/junos_netconf.py +++ b/network/junos/junos_netconf.py @@ -35,7 +35,7 @@ - This argument specifies the port the netconf service should listen on for SSH connections. The default port as defined in RFC 6242 is 830. - required: true + required: false default: 830 state: description: @@ -44,26 +44,39 @@ I(present) the netconf service will be configured. If the I(state) argument is set to I(absent) the netconf service will be removed from the configuration. - required: true + required: false default: present choices: ['present', 'absent'] """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + netconf: + host: "{{ inventory_hostname }}" + username: ansible + password: Ansible + transport: netconf + - name: enable netconf service on port 830 junos_netconf: listens_on: 830 state: present + provider: "{{ netconf }}" - name: disable netconf service junos_netconf: state: absent + provider: "{{ netconf }}" """ RETURN = """ """ import re +from ansible.module_utils.junos import NetworkModule + def parse_port(config): match = re.search(r'port (\d+)', config) if match: @@ -88,8 +101,8 @@ def main(): transport=dict(default='cli', choices=['cli']) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] port = module.params['listens_on'] @@ -109,14 +122,11 @@ def main(): if commands: if not module.check_mode: comment = 'configuration updated by junos_netconf' - module.connection.configure(commands, comment=comment) + module.config(commands, comment=comment) result['changed'] = True module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.shell import * -from ansible.module_utils.junos import * if __name__ == '__main__': main() From 256730e997bb6ba351e0444cd5ccf7ebefaf0a1a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 1 Sep 2016 17:22:51 -0400 Subject: [PATCH 113/770] add new functionality to junos_command module * commands argument now accepts a dict arguments * rpcs argument now accepts a dict argument * waitfor has been renamed to wait_for with an alias to waitfor * only show commands are allowd when check mode is specified * config mode is no longer allowed in the command stack * add argument match with valid values any, all --- network/junos/junos_command.py | 231 ++++++++++++++++++--------------- 1 file changed, 127 insertions(+), 104 deletions(-) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index 6a7f47c6951..cdbb34ce47c 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -34,9 +34,11 @@ options: commands: description: - - An ordered set of CLI commands to be executed on the remote - device. The output from the commands is then returned to - the playbook in the task results. + - The C(commands) to send to the remote device over the Netconf + transport. The resulting output from the command + is returned. If the I(wait_for) argument is provided, the + module is not returned until the condition is satisfied or + the number of I(retries) has been exceeded. required: false default: null rpcs: @@ -46,17 +48,29 @@ is return to the playbook via the modules results dictionary. required: false default: null - waitfor: + wait_for: description: - Specifies what to evaluate from the output of the command and what conditionals to apply. This argument will cause - the task to wait for a particular conditional or set of - conditionals to be true before moving forward. If the - conditional is not true by the configured retries, the - :1 - task fails. See examples. + the task to wait for a particular conditional to be true + before moving forward. If the conditional is not true + by the configured retries, the task fails. See examples. required: false default: null + aliases: ['waitfor'] + version_added: "2.2" + match: + description: + - The I(match) argument is used in conjunction with the + I(wait_for) argument to specify the match policy. Valid + values are C(all) or C(any). If the value is set to C(all) + then all conditionals in the I(wait_for) must be satisfied. If + the value is set to C(any) then only one of the values must be + satisfied. + required: false + default: all + choices: ['any', 'all'] + version_added: "2.2" retries: description: - Specifies the number of retries a command should by tried @@ -89,12 +103,18 @@ """ EXAMPLES = """ -# the required set of connection arguments have been purposely left off -# the examples for brevity +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + netconf: + host: "{{ inventory_hostname }}" + username: ansible + password: Ansible - name: run a set of commands junos_command: commands: ['show version', 'show ip route'] + provider: "{{ netconf }}" - name: run a command with a conditional applied to the second command junos_command: @@ -103,12 +123,14 @@ - show interfaces fxp0 waitfor: - "result[1].interface-information.physical-interface.name eq fxp0" + provider: "{{ netconf }}" - name: collect interface information using rpc junos_command: rpcs: - "get_interface_information interface=em0 media=True" - "get_interface_information interface=fxp0 media=True" + provider: "{{ netconf }}" """ RETURN = """ @@ -124,64 +146,60 @@ type: list sample: [['...', '...'], ['...', '...']] -xml: - description: The raw XML reply from the device - returned: when format is xml - type: list - sample: [['...', '...'], ['...', '...']] - failed_conditionals: description: the conditionals that failed retured: failed type: list sample: ['...', '...'] """ -import shlex - -def split(value): - lex = shlex.shlex(value) - lex.quotes = '"' - lex.whitespace_split = True - lex.commenters = '' - return list(lex) - -def rpc_args(args): - kwargs = dict() - args = split(args) - name = args.pop(0) - for arg in args: - key, value = arg.split('=') - if str(value).upper() in ['TRUE', 'FALSE']: - kwargs[key] = bool(value) - elif re.match(r'^[0-9]+$', value): - kwargs[key] = int(value) - else: - kwargs[key] = str(value) - return (name, kwargs) +import re -def parse_rpcs(rpcs): - parsed = list() - for rpc in (rpcs or list()): - parsed.append(rpc_args(rpc)) - return parsed +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.netcli import AddCommandError, FailedConditionsError +from ansible.module_utils.junos import NetworkModule, NetworkError -def run_rpcs(module, items, format): - response = list() - for name, kwargs in items: - kwargs['format'] = format - result = module.connection.rpc(name, **kwargs) - if format == 'text': - response.append(result.text) - else: - response.append(result) - return response +VALID_KEYS = { + 'cli': frozenset(['command', 'output', 'prompt', 'response']), + 'rpc': frozenset(['command', 'output']) +} -def iterlines(stdout): + +def to_lines(stdout): for item in stdout: if isinstance(item, basestring): item = str(item).split('\n') yield item +def parse(module, command_type): + if command_type == 'cli': + items = module.params['commands'] + elif command_type == 'rpc': + items = module.params['rpcs'] + + parsed = list() + for item in (items or list()): + if isinstance(item, basestring): + item = dict(command=item, output=None) + elif 'command' not in item: + module.fail_json(msg='command keyword argument is required') + elif item.get('output') not in [None, 'text', 'xml']: + module.fail_json(msg='invalid output specified for command' + 'Supported values are `text` or `xml`') + elif not set(item.keys()).issubset(VALID_KEYS[command_type]): + module.fail_json(msg='unknown command keyword specified. Valid ' + 'values are %s' % ', '.join(VALID_KEYS[command_type])) + + if not item['output']: + item['output'] = module.params['display'] + + item['command_type'] = command_type + + parsed.append(item) + + return parsed + + def main(): """main entry point for Ansible module """ @@ -189,76 +207,81 @@ def main(): spec = dict( commands=dict(type='list'), rpcs=dict(type='list'), - format=dict(default='xml', choices=['text', 'xml']), - waitfor=dict(type='list'), + + display=dict(default='xml', choices=['text', 'xml'], + aliases=['format', 'output']), + + wait_for=dict(type='list', aliases=['waitfor']), + match=dict(default='all', choices=['all', 'any']), + retries=dict(default=10, type='int'), interval=dict(default=1, type='int'), + transport=dict(default='netconf', choices=['netconf']) ) mutually_exclusive = [('commands', 'rpcs')] - module = get_module(argument_spec=spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) + module = NetworkModule(argument_spec=spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + commands = list() + for key in VALID_KEYS.keys(): + commands.extend(list(parse(module, key))) - commands = module.params['commands'] - rpcs = parse_rpcs(module.params['rpcs']) + conditionals = module.params['wait_for'] or list() - encoding = module.params['format'] - retries = module.params['retries'] - interval = module.params['interval'] + warnings = list() + runner = CommandRunner(module) - try: - queue = set() - for entry in (module.params['waitfor'] or list()): - queue.add(Conditional(entry)) - except AttributeError: - exc = get_exception() - module.fail_json(msg=exc.message) - - result = dict(changed=False) - - while retries > 0: - if commands: - response = module.run_commands(commands, format=encoding) + for cmd in commands: + if module.check_mode and not cmd['command'].startswith('show'): + warnings.append('only show commands are supported when using ' + 'check mode, not executing `%s`' % cmd['command']) else: - response = run_rpcs(module, rpcs, format=encoding) - - result['stdout'] = response - xmlout = list() - - for index in range(0, len(response)): - if encoding == 'xml': - xmlout.append(xml_to_string(response[index])) - response[index] = xml_to_json(response[index]) + if cmd['command'].startswith('co'): + module.fail_json(msg='junos_command does not support running ' + 'config mode commands. Please use ' + 'junos_config instead') + try: + runner.add_command(**cmd) + except AddCommandError: + exc = get_exception() + warnings.append('duplicate command detected: %s' % cmd) + + for item in conditionals: + runner.add_conditional(item) + + runner.retries = module.params['retries'] + runner.interval = module.params['interval'] + runner.match = module.params['match'] - for item in list(queue): - if item(response): - queue.remove(item) + try: + runner.run() + except FailedConditionsError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) - if not queue: - break + result = dict(changed=False, stdout=list()) - time.sleep(interval) - retries -= 1 - else: - failed_conditions = [item.raw for item in queue] - module.fail_json(msg='timeout waiting for value', failed_conditions=failed_conditions) + for cmd in commands: + try: + output = runner.get_command(cmd['command'], cmd.get('output')) + except ValueError: + output = 'command not executed due to check_mode, see warnings' + result['stdout'].append(output) - if xmlout: - result['xml'] = xmlout + result['warnings'] = warnings + result['stdout_lines'] = list(to_lines(result['stdout'])) - result['stdout_lines'] = list(iterlines(result['stdout'])) module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.junos import * - if __name__ == '__main__': main() From a11a311b493999f8fd08769b303f28a3b7cd6480 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 2 Sep 2016 21:31:06 -0400 Subject: [PATCH 114/770] update junos_config module * adds src argument to load configuration from disk * adds src_format to set the source file format * adds update argument with choices merge or replace * deprecates the replace argument in favor of update=replace --- network/junos/junos_config.py | 267 +++++++++++++++++++++++----------- 1 file changed, 186 insertions(+), 81 deletions(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 5a365a16647..0e4a7efcd11 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -21,15 +21,12 @@ module: junos_config version_added: "2.1" author: "Peter Sprygada (@privateip)" -short_description: Manage configuration on remote devices running Junos +short_description: Manage configuration on devices running Juniper JUNOS description: - - Provides an abstraction for working - with the configuration running on remote Junos devices. It can perform - operations that influence the configuration state. - - This module provides an implementation for configuring Juniper - JUNOS devices. The configuration statements must start with either - `set` or `delete` and are compared against the current device - configuration and only changes are pushed to the device. + - This module provides an implementation for working with the active + configuration running on Juniper JUNOS devices. It provides a set + of arguments for loading configuration, performing rollback operations + and zeroing the active configuration on the device. extends_documentation_fragment: junos options: lines: @@ -40,6 +37,38 @@ file in role or playbook root folder in templates directory. required: false default: null + src: + description: + - The I(src) argument provides a path to the configuration file + to load into the remote system. The path can either be a full + system path to the configuration file if the value starts with / + or relative to the root of the implemented role or playbook. + This arugment is mutually exclusive with the I(lines) and + I(parents) arguments. + required: false + default: null + version_added: "2.2" + src_format: + description: + - The I(src_format) argument specifies the format of the configuration + found int I(src). If the I(src_format) argument is not provided, + the module will attempt to determine the format of the configuration + file specified in I(src). + required: false + default: null + choices: ['xml', 'set', 'text', 'json'] + version_added: "2.2" + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) and I(replace). When the argument is set to + I(merge), the configuration changes are merged with the current + device active configuration. + required: false + default: merge + choices: ['merge', 'replace'] + version_added: "2.2" rollback: description: - The C(rollback) argument instructs the module to rollback the @@ -79,11 +108,24 @@ replace the current configuration hierarchy with the one specified in the corresponding hierarchy of the source configuration loaded from this module. + - Note this argument should be considered deprecated. To achieve + the equivalent, set the I(update) argument to C(replace). This argument + will be removed in a future release. + required: false required: true - choices: - - yes - - no + choices: ['yes', 'no'] default: false + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" requirements: - junos-eznc notes: @@ -92,118 +134,181 @@ """ EXAMPLES = """ -- name: load configuration lines in device +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + netconf: + host: "{{ inventory_hostname }}" + username: ansible + password: Ansible + +- name: load configure file into device junos_config: - lines: - - set system host-name {{ inventory_hostname }} - - delete interfaces ge-0/0/0 description + src: srx.cfg comment: update config + provider: "{{ netconf }}" - name: rollback the configuration to id 10 junos_config: rollback: 10 + provider: "{{ netconf }}" - name: zero out the current configuration junos_config: zeroize: yes - -- name: confirm a candidate configuration - junos_config: + provider: "{{ netconf }}" """ +RETURN = """ +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: path + sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 +""" import re +import json -DEFAULT_COMMENT = 'configured by junos_config' +from lxml import etree -def diff_config(candidate, config): +from ansible.module_utils.junos import NetworkModule, NetworkError - updates = set() - for line in candidate: - parts = line.split() - action = parts[0] - cfgline = ' '.join(parts[1:]) +DEFAULT_COMMENT = 'configured by junos_config' - if action not in ['set', 'delete']: - module.fail_json(msg='line must start with either `set` or `delete`') - elif action == 'set' and cfgline not in config: - updates.add(line) +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) - elif action == 'delete' and not config: - updates.add(line) +def run(module, result): + if module.params['rollback']: + action = 'rollback_config' + elif module.params['zeroize']: + action = 'zeroize_config' + else: + action = 'load_config' - elif action == 'delete': - for cfg in config: - if cfg[4:].startswith(cfgline): - updates.add(line) + return invoke(action, module, result) - return list(updates) -def main(): +def check_args(module, warnings): + if module.params['replace'] is True: + warnings.append('The replace argument is deprecated, please use ' + 'update=replace instead. This argument will be ' + 'removed in the future') + if module.params['lines'] and module.params['update'] == 'replace': + module.fail_json(msg='config replace is only allowed when src is specified') - argument_spec = dict( - lines=dict(type='list'), - rollback=dict(type='int'), - zeroize=dict(default=False, type='bool'), - confirm=dict(default=0, type='int'), - comment=dict(default=DEFAULT_COMMENT), - replace=dict(default=False, type='bool'), - transport=dict(default='netconf', choices=['netconf']) - ) +def guess_format(config): + try: + json.loads(config) + return 'json' + except ValueError: + pass - mutually_exclusive = [('lines', 'rollback'), ('lines', 'zeroize'), - ('rollback', 'zeroize')] + try: + etree.fromstring(config) + return 'xml' + except etree.XMLSyntaxError: + pass - module = get_module(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) + if config.startswith('set') or config.startswith('delete'): + return 'set' - rollback = module.params['rollback'] - zeroize = module.params['zeroize'] + return 'text' + +def backup_config(module, result): + result['__backup__'] = module.config.get_config() +def load_config(module, result): comment = module.params['comment'] confirm = module.params['confirm'] - if module.params['replace']: - action = 'replace' - else: - action = 'merge' + update = module.params['update'] - lines = module.params['lines'] + candidate = module.params['lines'] or module.params['src'] commit = not module.check_mode - results = dict(changed=False) + config_format = module.params['src_format'] or guess_format(candidate) + + diff = module.config.load_config(candidate, update=update, comment=comment, + format=config_format, commit=commit, + confirm=confirm) + + if diff: + result['changed'] = True + result['diff'] = dict(prepared=diff) + +def rollback_config(module, result): + rollback = module.params['rollback'] + comment = module.params['comment'] + + commit = not module.check_mode + + diff = module.connection.rollback_config(rollback, commit=commit, + comment=comment) + + if diff: + result['changed'] = True + result['diff'] = dict(prepared=diff) + +def zeroize_config(module, result): + if not module.check_mode: + module.cli.run_commands('request system zeroize') + result['changed'] = True + + +def main(): + + argument_spec = dict( + lines=dict(type='list'), + + src=dict(type='path'), + src_format=dict(choices=['xml', 'text', 'set', 'json']), + + # update operations + update=dict(default='merge', choices=['merge', 'replace']), + confirm=dict(default=0, type='int'), + comment=dict(default=DEFAULT_COMMENT), - if lines: - config = str(module.get_config(config_format='set')).split('\n') - updates = diff_config(lines, config) + # config operations + backup=dict(type='bool', default=False), + rollback=dict(type='int'), + zeroize=dict(default=False, type='bool'), + + # this argument is deprecated in favor of setting update=replace + # and will be removed in a future version + replace=dict(default=False, type='bool'), + + transport=dict(default='netconf', choices=['netconf']) + ) + + mutually_exclusive = [('lines', 'rollback'), ('lines', 'zeroize'), + ('rollback', 'zeroize'), ('lines', 'src'), + ('src', 'zeroize'), ('src', 'rollback')] - if updates: - updates = '\n'.join(updates) - diff = module.load_config(updates, action=action, comment=comment, - format='set', commit=commit, confirm=confirm) + module = NetworkModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) - if diff: - results['changed'] = True - results['diff'] = dict(prepared=diff) + warnings = list() + check_args(module, warnings) - elif rollback is not None: - diff = module.rollback_config(rollback, commit=commit) - if diff: - results['changed'] = True - results['diff'] = dict(prepared=diff) + if module.params['replace'] is True: + module.params['update'] = 'replace' - elif zeroize: - if not module.check_mode: - module.run_commands('request system zeroize') - results['changed'] = True + result = dict(changed=False, warnings=warnings) - module.exit_json(**results) + try: + run(module, result) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) + module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.junos import * if __name__ == '__main__': main() From e725a077c53e1bf93fb7939ec205631cd1b02908 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 12:58:00 +0200 Subject: [PATCH 115/770] Fixed module structure --- network/nxos/nxos_bgp.py | 702 ++++++--------------------------------- 1 file changed, 93 insertions(+), 609 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index b183d94510a..d0209179e9d 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -29,10 +29,10 @@ extends_documentation_fragment: nxos notes: - I(state)=absent removes the whole BGP ASN configuration when VRF is - C(default) or the whole VRF instance within the BGP process when using + I(default) or the whole VRF instance within the BGP process when using a different VRF. - - C(default) when supported restores params default value. - - Configuring global parmas is only permitted if VRF is C(default). + - I(default) when supported restores params default value. + - Configuring global parmas is only permitted if VRF is I(default). options: asn: description: @@ -41,12 +41,14 @@ required: true vrf: description: - - Name of the VRF. The name 'default' is a valid VRF representing the global BGP. + - Name of the VRF. The name 'default' is a valid VRF representing + the global BGP. required: false default: null bestpath_always_compare_med: description: - - Enable/Disable MED comparison on paths from different autonomous systems. + - Enable/Disable MED comparison on paths from different + autonomous systems. required: false choices: ['true','false'] default: null @@ -79,15 +81,16 @@ default: null bestpath_med_missing_as_worst: description: - - Enable/Disable assigns the value of infinity to received routes that - do not carry the MED attribute, making these routes the least desirable. + - Enable/Disable assigns the value of infinity to received + routes that do not carry the MED attribute, making these routes + the least desirable. required: false choices: ['true','false'] default: null bestpath_med_non_deterministic: description: - - Enable/Disable deterministic selection of the best MED path from among - the paths from the same autonomous system. + - Enable/Disable deterministic selection of the best MED pat + from among the paths from the same autonomous system. required: false choices: ['true','false'] default: null @@ -108,25 +111,28 @@ default: null disable_policy_batching: description: - - Enable/Disable the batching evaluation of prefix advertisements to all peers. + - Enable/Disable the batching evaluation of prefix advertisement + to all peers. required: false choices: ['true','false'] default: null disable_policy_batching_ipv4_prefix_list: description: - - Enable/Disable the batching evaluation of prefix advertisements to all - peers with prefix list. + - Enable/Disable the batching evaluation of prefix advertisements + to all peers with prefix list. required: false default: null disable_policy_batching_ipv6_prefix_list: description: - - Enable/Disable the batching evaluation of prefix advertisements to all peers with prefix list. + - Enable/Disable the batching evaluation of prefix advertisements + to all peers with prefix list. required: false enforce_first_as: description: - - Enable/Disable enforces the neighbor autonomous system to be the first AS number - listed in the AS path attribute for eBGP. On NX-OS, this property is only supported - in the global BGP context. + - Enable/Disable enforces the neighbor autonomous system to be + the first AS number listed in the AS path attribute for eBGP. + On NX-OS, this property is only supported in the + global BGP context. required: false choices: ['true','false'] default: null @@ -156,14 +162,16 @@ fast_external_fallover: description: - Enable/Disable immediately reset the session if the link to a - directly connected BGP peer goes down. Only supported in the global BGP context. + directly connected BGP peer goes down. Only supported in the + global BGP context. required: false choices: ['true','false'] default: null flush_routes: description: - Enable/Disable flush routes in RIB upon controlled restart. - On NX-OS, this property is only supported in the global BGP context. + On NX-OS, this property is only supported in the global + BGP context. required: false choices: ['true','false'] default: null @@ -187,7 +195,8 @@ default: null graceful_restart_timers_stalepath_time: description: - - Set maximum time that BGP keeps the stale routes from the restarting BGP peer. + - Set maximum time that BGP keeps the stale routes from the + restarting BGP peer. choices: ['true','false'] default: null isolate: @@ -209,13 +218,14 @@ default: null maxas_limit: description: - - Specify Maximum number of AS numbers allowed in the AS-path attribute - Valid values are between 1 and 512. + - Specify Maximum number of AS numbers allowed in the AS-path + attribute. Valid values are between 1 and 512. required: false default: null neighbor_down_fib_accelerate: description: - - Enable/Disable handle BGP neighbor down event, due to various reasons. + - Enable/Disable handle BGP neighbor down event, due to + various reasons. required: false choices: ['true','false'] default: null @@ -237,13 +247,15 @@ default: null suppress_fib_pending: description: - - Enable/Disable advertise only routes programmed in hardware to peers. + - Enable/Disable advertise only routes programmed in hardware + to peers. required: false choices: ['true','false'] default: null timer_bestpath_limit: description: - - Specify timeout for the first best path after a restart, in seconds. + - Specify timeout for the first best path after a restart, + in seconds. required: false default: null timer_bestpath_limit_always: @@ -264,16 +276,11 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' @@ -292,11 +299,12 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"} existing: description: k/v pairs of existing BGP configuration + returned: verbose mode type: dict sample: {"asn": "65535", "bestpath_always_compare_med": false, "bestpath_aspath_multipath_relax": false, @@ -317,7 +325,7 @@ "timer_bgp_keepalive": "60", "vrf": "test"} end_state: description: k/v pairs of BGP configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"asn": "65535", "bestpath_always_compare_med": false, "bestpath_aspath_multipath_relax": false, @@ -349,203 +357,51 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -558,119 +414,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -721,303 +464,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -1206,7 +690,7 @@ def get_value(arg, config): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) try: asn_regex = '.*router\sbgp\s(?P\d+).*' @@ -1221,7 +705,7 @@ def get_existing(module, args): if module.params['vrf'] != 'default': parents = [bgp_parent, 'vrf {0}'.format(module.params['vrf'])] else: - parents = bgp_parent + parents = [bgp_parent] config = netcfg.get_section(parents) if config: @@ -1409,12 +893,12 @@ def main(): timer_bestpath_limit=dict(required=False, type='str'), timer_bgp_hold=dict(required=False, type='str'), timer_bgp_keepalive=dict(required=False, type='str'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) module = get_module(argument_spec=argument_spec, required_together=[['timer_bgp_hold', 'timer_bgp_keepalive']], @@ -1509,7 +993,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From 7c9b5b8841f634f5628952c778edc4668dc4c767 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 13:13:54 +0200 Subject: [PATCH 116/770] Fixed module structure --- network/nxos/nxos_static_route.py | 690 ++++-------------------------- 1 file changed, 83 insertions(+), 607 deletions(-) diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index b92a483489b..56967b34897 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -26,7 +26,8 @@ extends_documentation_fragment: nxos notes: - If no vrf is supplied, vrf is set to default - - If state=absent, the route will be removed, regardless of the non-required parameters. + - If I(state)=absent, the route will be removed, regardless of the + non-required parameters. options: prefix: description: @@ -62,12 +63,6 @@ - Manage the state of the resource required: true choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' @@ -84,18 +79,19 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"next_hop": "3.3.3.3", "pref": "100", "prefix": "192.168.20.64/24", "route_name": "testing", "vrf": "default"} existing: description: k/v pairs of existing configuration + returned: verbose mode type: dict sample: {} end_state: description: k/v pairs of configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"next_hop": "3.3.3.3", "pref": "100", "prefix": "192.168.20.0/24", "route_name": "testing", @@ -113,195 +109,45 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - def to_block(self, section): return '\n'.join([item.raw for item in section]) @@ -322,119 +168,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -485,303 +218,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -807,7 +281,7 @@ def state_present(module, candidate, prefix): def state_absent(module, candidate, prefix): - netcfg = custom_get_config(module) + netcfg = get_config(module) commands = list() parents = 'vrf context {0}'.format(module.params['vrf']) invoke('set_route', module, commands, prefix) @@ -826,39 +300,41 @@ def state_absent(module, candidate, prefix): def fix_prefix_to_regex(prefix): - prefix = prefix.split('.') - prefix = '\.'.join(prefix) - prefix = prefix.split('/') - prefix = '\/'.join(prefix) - + prefix = prefix.replace('.', '\.').replace('/', '\/') return prefix def get_existing(module, prefix, warnings): key_map = ['tag', 'pref', 'route_name', 'next_hop'] - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = 'vrf context {0}'.format(module.params['vrf']) prefix_to_regex = fix_prefix_to_regex(prefix) - route_regex = '.*ip\sroute\s{0}\s(?P\S+)(\sname\s(?P\S+))?(\stag\s(?P\d+))?(\s(?P\d+)).*'.format(prefix_to_regex) + route_regex = ('.*ip\sroute\s{0}\s(?P\S+)(\sname\s(?P\S+))?' + '(\stag\s(?P\d+))?(\s(?P\d+)).*'.format(prefix_to_regex)) if module.params['vrf'] == 'default': config = str(netcfg) else: config = netcfg.get_section(parents) - try: - match_route = re.match(route_regex, config, re.DOTALL) - group_route = match_route.groupdict() - - for key in key_map: - if key not in group_route.keys(): - group_route['key'] = None - group_route['prefix'] = prefix - except (AttributeError, TypeError): - group_route = {} - if module.params['state'] == 'present': - msg = ("VRF {0} doesn't exist.".format(module.params['vrf'])) - warnings.append(msg) + + if config: + try: + match_route = re.match(route_regex, config, re.DOTALL) + group_route = match_route.groupdict() + + for key in key_map: + if key not in group_route.keys(): + group_route[key] = '' + group_route['prefix'] = prefix + group_route['vrf'] = module.params['vrf'] + except (AttributeError, TypeError): + group_route = {} + else: + group_route = {} + msg = ("VRF {0} didn't exist.".format(module.params['vrf'])) + if msg not in warnings: + warnings.append(msg) return group_route @@ -944,16 +420,17 @@ def main(): tag=dict(type='str'), route_name=dict(type='str'), pref=dict(type='str'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['absent', 'present'], default='present'), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) - m_facts = module.params['m_facts'] + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + state = module.params['state'] result = dict(changed=False) @@ -973,16 +450,15 @@ def main(): try: response = load_config(module, candidate) result.update(response) - except ShellError: + except Exception: exc = get_exception() module.fail_json(msg=str(exc)) else: result['updates'] = [] result['warnings'] = warnings - result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, prefix, warnings) result['end_state'] = end_state result['existing'] = existing From 9b03b39eb641df62e7a6a41c738b611dd4709202 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 13:20:52 +0200 Subject: [PATCH 117/770] Removed tabs --- network/nxos/nxos_static_route.py | 32 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index 56967b34897..d62066b4ea0 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -311,7 +311,7 @@ def get_existing(module, prefix, warnings): prefix_to_regex = fix_prefix_to_regex(prefix) route_regex = ('.*ip\sroute\s{0}\s(?P\S+)(\sname\s(?P\S+))?' - '(\stag\s(?P\d+))?(\s(?P\d+)).*'.format(prefix_to_regex)) + '(\stag\s(?P\d+))?(\s(?P\d+)).*'.format(prefix_to_regex)) if module.params['vrf'] == 'default': config = str(netcfg) @@ -319,22 +319,22 @@ def get_existing(module, prefix, warnings): config = netcfg.get_section(parents) if config: - try: - match_route = re.match(route_regex, config, re.DOTALL) - group_route = match_route.groupdict() - - for key in key_map: - if key not in group_route.keys(): - group_route[key] = '' - group_route['prefix'] = prefix - group_route['vrf'] = module.params['vrf'] - except (AttributeError, TypeError): - group_route = {} + try: + match_route = re.match(route_regex, config, re.DOTALL) + group_route = match_route.groupdict() + + for key in key_map: + if key not in group_route.keys(): + group_route[key] = '' + group_route['prefix'] = prefix + group_route['vrf'] = module.params['vrf'] + except (AttributeError, TypeError): + group_route = {} else: - group_route = {} - msg = ("VRF {0} didn't exist.".format(module.params['vrf'])) - if msg not in warnings: - warnings.append(msg) + group_route = {} + msg = ("VRF {0} didn't exist.".format(module.params['vrf'])) + if msg not in warnings: + warnings.append(msg) return group_route From 330e3237053874040718d9dfe1591b2a167c302e Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 09:15:03 -0400 Subject: [PATCH 118/770] removes state argument from ops_config module this removes the state argument from ops_config. The state argument should not have been there --- network/openswitch/ops_config.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index 0370eb79686..864ff0a915c 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -141,15 +141,6 @@ default: no choices: ['yes', 'no'] version_added: "2.2" - state: - description: - - This argument specifies whether or not the running-config is - present on the remote device. When set to I(absent) the - running-config on the remote device is erased. - required: false - default: no - choices: ['yes', 'no'] - version_added: "2.2" """ EXAMPLES = """ @@ -207,11 +198,6 @@ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.netcli import Command -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - def check_args(module, warnings): if module.params['parents']: if not module.params['lines'] or module.params['src']: @@ -253,7 +239,7 @@ def load_config(module, commands, result): result['changed'] = module.params['update'] != 'check' result['updates'] = commands.split('\n') -def present(module, result): +def run(module, result): match = module.params['match'] replace = module.params['replace'] @@ -282,11 +268,6 @@ def present(module, result): if module.params['save'] and not module.check_mode: module.config.save_config() -def absent(module, result): - if not module.check_mode: - module.cli('erase startup-config') - result['changed'] = True - def main(): argument_spec = dict( @@ -313,8 +294,6 @@ def main(): save=dict(type='bool', default=False), - state=dict(choices=['present', 'absent'], default='present'), - # ops_config is only supported over Cli transport so force # the value of transport to be cli transport=dict(default='cli', choices=['cli']) @@ -327,8 +306,6 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - state = module.params['state'] - if module.params['force'] is True: module.params['match'] = 'none' @@ -341,7 +318,7 @@ def main(): result['__backup__'] = module.config.get_config() try: - invoke(state, module, result) + run(module, result) except NetworkError: exc = get_exception() module.fail_json(msg=str(exc)) From e0f7912e372e84aa7640f649baca4b611b72290d Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 09:09:38 -0400 Subject: [PATCH 119/770] removes state argument from ios_config The state argument should not be in ios_config. This change removes the state argument --- network/ios/ios_config.py | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 5e71d79ff31..a48754dcf6a 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -174,15 +174,6 @@ default: no choices: ['yes', 'no'] version_added: "2.2" - state: - description: - - This argument specifies whether or not the running-config is - present on the remote device. When set to I(absent) the - running-config on the remote device is erased. - required: false - default: no - choices: ['yes', 'no'] - version_added: "2.2" """ EXAMPLES = """ @@ -247,11 +238,6 @@ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.netcli import Command -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - def check_args(module, warnings): if module.params['parents']: if not module.params['lines'] or module.params['src']: @@ -304,7 +290,7 @@ def load_config(module, commands, result): result['changed'] = module.params['update'] != 'check' result['updates'] = commands.split('\n') -def present(module, result): +def run(module, result): match = module.params['match'] replace = module.params['replace'] @@ -341,11 +327,6 @@ def present(module, result): if module.params['save'] and not module.check_mode: module.config.save_config() -def absent(module, result): - if not module.check_mode: - module.cli('write erase') - result['changed'] = True - def main(): argument_spec = dict( @@ -371,8 +352,6 @@ def main(): default=dict(type='bool', default=False), save=dict(type='bool', default=False), - - state=dict(choices=['present', 'absent'], default='present') ) mutually_exclusive = [('lines', 'src')] @@ -382,8 +361,6 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - state = module.params['state'] - if module.params['force'] is True: module.params['match'] = 'none' @@ -396,7 +373,7 @@ def main(): result['__backup__'] = module.config.get_config() try: - invoke(state, module, result) + run(module, result) except NetworkError: load_backup(module) exc = get_exception() From 989a2c7d0f7b9594c50315a1fd11bce3b6f12317 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 09:13:14 -0400 Subject: [PATCH 120/770] removes argument from vyos_config module The state argument should not be in vyos_config. This commit removes the state argument --- network/vyos/vyos_config.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/network/vyos/vyos_config.py b/network/vyos/vyos_config.py index 39d9be7e4e5..dbe1b257bc2 100644 --- a/network/vyos/vyos_config.py +++ b/network/vyos/vyos_config.py @@ -100,15 +100,6 @@ required: false default: false choices: ['yes', 'no'] - state: - description: - - The C(state) argument controls the existing state of the config - file on disk. When set to C(present), the configuration should - exist on disk and when set to C(absent) the configuration file - is removed. This only applies to the startup configuration. - required: false - default: present - choices: ['present', 'absent'] """ RETURN = """ @@ -162,11 +153,6 @@ ] -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - def check_args(module, warnings): if module.params['save'] and module.params['update'] == 'check': warnings.append('The configuration will not be saved when update ' @@ -262,7 +248,7 @@ def load_config(module, commands, result): result['changed'] = True -def present(module, result): +def run(module, result): # get the current active config from the node or passed in via # the config param config = get_config(module, result) @@ -283,12 +269,6 @@ def present(module, result): 'removed, please see the removed key') -def absent(module, result): - if not module.check_mode: - module.cli('rm /config/config.boot') - result['changed'] = True - - def main(): argument_spec = dict( @@ -304,8 +284,6 @@ def main(): config=dict(), save=dict(default=False, type='bool'), - - state=dict(choices=['present', 'absent'], default='present') ) mutually_exclusive = [('lines', 'src')] @@ -315,8 +293,6 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - state = module.params['state'] - warnings = list() check_args(module, warnings) @@ -326,7 +302,7 @@ def main(): result['__backup__'] = module.config.get_config() try: - invoke(state, module, result) + run(module, result) except NetworkError: exc = get_exception() module.fail_json(msg=str(exc), **exc.kwargs) From cba30b637a7aebd0344a18c623b6110c94837f19 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 09:11:10 -0400 Subject: [PATCH 121/770] updates nxos_config to remove state argument The state argument should not be in the nxos_config module. This change removes the state argument for this release --- network/nxos/nxos_config.py | 28 ++-------------------------- 1 file changed, 2 insertions(+), 26 deletions(-) diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index b7e40dd1edc..0a49460255a 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -157,16 +157,6 @@ required: false default: false version_added: "2.2" - state: - description: - - The I(state) argument specifies the state of the config - file on the device. When set to present, the configuration - is updated based on the values of the module. When the value - is set to absent, the device startup config is erased. - required: true - default: present - choices: ['present', 'absent'] - version_added: "2.2" """ @@ -223,11 +213,6 @@ from ansible.module_utils.nxos import NetworkModule, NetworkError from ansible.module_utils.basic import get_exception -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - def check_args(module, warnings): if module.params['save'] and module.check_mode: warnings.append('will not save configuration due to checkmode') @@ -287,7 +272,7 @@ def load_checkpoint(module, result): msg = 'unable to rollback configuration' module.fail_json(msg=msg, checkpoint=checkpoint, **exc.kwargs) -def present(module, result): +def run(module, result): match = module.params['match'] replace = module.params['replace'] update = module.params['update'] @@ -328,11 +313,6 @@ def present(module, result): module.config.save_config() result['changed'] = True -def absent(module, result): - if not module.check_mode: - module.cli('write erase') - result['changed'] = True - def main(): """ main entry point for module execution """ @@ -360,8 +340,6 @@ def main(): defaults=dict(type='bool', default=False), save=dict(type='bool', default=False), - - state=dict(default='present', choices=['absent', 'present']) ) mutually_exclusive = [('lines', 'src')] @@ -371,8 +349,6 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - state = module.params['state'] - if module.params['force'] is True: module.params['match'] = 'none' @@ -382,7 +358,7 @@ def main(): result = dict(changed=False, warnings=warnings) try: - invoke(state, module, result) + run(module, result) except NetworkError: load_checkpoint(module, result) exc = get_exception() From c20d82d9ca36e4131a110b0158e1fadae5f15d9d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 16:22:40 +0200 Subject: [PATCH 122/770] Fixed module structure --- network/nxos/nxos_acl.py | 657 +++++---------------------------------- 1 file changed, 83 insertions(+), 574 deletions(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index 05a2bda8ee4..65c80345ee7 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -217,6 +217,7 @@ "proto": "tcp", "seq": "10", "src": "1.1.1.1/24"} existing: description: k/v pairs of existing ACL entries. + returned: always type: dict sample: {} end_state: @@ -237,206 +238,56 @@ sample: true ''' - -# COMMON CODE FOR MIGRATION - -import re -import time import collections -import itertools -import shlex import json -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -449,119 +300,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -612,303 +350,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -916,6 +395,7 @@ def load_config(module, candidate): return result # END OF COMMON CODE + 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 @@ -938,6 +418,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -947,6 +432,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -1126,6 +624,15 @@ def execute_config_command(commands, module): 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 main(): @@ -1170,10 +677,12 @@ def main(): host=dict(required=True), username=dict(type='str'), password=dict(no_log=True, type='str'), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] action = module.params['action'] From 73cd257b66f5fc1a1bc5f8de6782daffe5c0237c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 16:38:24 +0200 Subject: [PATCH 123/770] Fixed module structure --- network/nxos/nxos_evpn_global.py | 645 +++---------------------------- 1 file changed, 59 insertions(+), 586 deletions(-) diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index ff1e0645be2..384b0be6634 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -31,32 +31,29 @@ - EVPN control plane required: true choices: ['true', 'false'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_evpn_global: nv_overlay_evpn=true + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" ''' RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"nv_overlay_evpn": true} existing: description: k/v pairs of existing configuration - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"nv_overlay_evpn": false} end_state: description: k/v pairs of configuration after module execution - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"nv_overlay_evpn": true} updates: @@ -73,203 +70,51 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -282,119 +127,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -445,303 +177,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - - -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -770,7 +243,7 @@ def get_value(arg, config, module): def get_existing(module): existing = {} - config = str(custom_get_config(module)) + config = str(get_config(module)) existing['nv_overlay_evpn'] = get_value('nv_overlay_evpn', config, module) return existing @@ -807,12 +280,12 @@ def get_commands(module, existing, proposed, candidate): def main(): argument_spec = dict( nv_overlay_evpn=dict(required=True, type='bool'), - m_facts=dict(required=False, default=False, type='bool'), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) existing = invoke('get_existing', module) end_state = existing @@ -833,7 +306,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module) result['end_state'] = end_state result['existing'] = existing From a1b666d0af58a1e180f9b2b3c991233fde43ffcf Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 16:46:59 +0200 Subject: [PATCH 124/770] Fixed module structure --- network/nxos/nxos_acl_interface.py | 662 ++++------------------------- 1 file changed, 83 insertions(+), 579 deletions(-) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 4a0cc4c3827..367472b06fd 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -84,11 +84,6 @@ type: list sample: [{"acl_type": "Router ACL", "direction": "egress", "interface": "Ethernet1/41", "name": "ANSIBLE"}] -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: commands sent to the device returned: always @@ -101,204 +96,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw +import json - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -311,119 +158,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -474,303 +208,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -779,6 +254,7 @@ def load_config(module, candidate): # END OF COMMON CODE + 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 @@ -801,6 +277,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -810,6 +291,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -955,6 +449,15 @@ def execute_config_command(commands, module): 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 main(): @@ -964,10 +467,12 @@ def main(): direction=dict(required=True, choices=['egress', 'ingress']), state=dict(choices=['absent', 'present'], default='present'), + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] name = module.params['name'] @@ -1020,7 +525,6 @@ def main(): results = {} results['proposed'] = proposed results['existing'] = existing - results['state'] = state results['updates'] = cmds results['changed'] = changed results['end_state'] = end_state From d894adba4a66ec31179658795657b48a60a2cb9a Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 16:53:51 +0200 Subject: [PATCH 125/770] Fixed module structure --- network/nxos/nxos_ospf.py | 640 ++++---------------------------------- 1 file changed, 55 insertions(+), 585 deletions(-) diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index f02f0584308..f5583f2789e 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -36,12 +36,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' @@ -56,17 +50,17 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"ospf": "1"} existing: description: k/v pairs of existing configuration - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"ospf": ["2"]} end_state: description: k/v pairs of configuration after module execution - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"ospf": ["1", "2"]} updates: @@ -83,203 +77,51 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __ne__(self, other): - return not self.__eq__(other) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -292,119 +134,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -455,303 +184,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - - -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -791,7 +261,7 @@ def get_value(config, module): def get_existing(module): existing = {} - config = str(custom_get_config(module)) + config = str(get_config(module)) value = get_value(config, module) if value: @@ -825,13 +295,13 @@ def state_absent(module, proposed, candidate): def main(): argument_spec = dict( ospf=dict(required=True, type='str'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, + module = get_network_module(argument_spec=argument_spec, supports_check_mode=True) state = module.params['state'] @@ -861,7 +331,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module) result['end_state'] = end_state result['existing'] = existing From 42add2f137428574cbe3153151d68365dafbc634 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 16:56:46 +0200 Subject: [PATCH 126/770] Fixed module structure --- network/nxos/nxos_bgp.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index d0209179e9d..025983848d2 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -899,10 +899,10 @@ def main(): config=dict(), save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - required_together=[['timer_bgp_hold', + module = get_network_module(argument_spec=argument_spec, + required_together=[['timer_bgp_hold', 'timer_bgp_keepalive']], - supports_check_mode=True) + supports_check_mode=True) state = module.params['state'] args = [ From 39d43e96e0219153a1d96772ccadfb920b4c9801 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:04:15 +0200 Subject: [PATCH 127/770] Fixed module structure --- network/nxos/nxos_ospf_vrf.py | 642 +++------------------------------- 1 file changed, 56 insertions(+), 586 deletions(-) diff --git a/network/nxos/nxos_ospf_vrf.py b/network/nxos/nxos_ospf_vrf.py index ef5e96ea8d4..32fd7cfcba8 100644 --- a/network/nxos/nxos_ospf_vrf.py +++ b/network/nxos/nxos_ospf_vrf.py @@ -107,12 +107,6 @@ Valid values are an integer, in Mbps, or the keyword 'default'. required: false default: null - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' @@ -125,7 +119,6 @@ timer_throttle_lsa_hold: 1100 timer_throttle_lsa_max: 3000 vrf: test - m_facts: true state: present username: "{{ un }}" password: "{{ pwd }}" @@ -135,7 +128,7 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"ospf": "1", "timer_throttle_lsa_hold": "1100", "timer_throttle_lsa_max": "3000", "timer_throttle_lsa_start": "60", @@ -144,7 +137,7 @@ "vrf": "test"} existing: description: k/v pairs of existing configuration - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "", "ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "5000", @@ -154,7 +147,7 @@ "timer_throttle_spf_start": "200", "vrf": "test"} end_state: description: k/v pairs of configuration after module execution - returned: when I(m_facts)=true + returned: verbose mode type: dict sample: {"auto_cost": "40000", "default_metric": "", "log_adjacency": "", "ospf": "1", "router_id": "", "timer_throttle_lsa_hold": "1100", @@ -176,203 +169,51 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __ne__(self, other): - return not self.__eq__(other) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -385,119 +226,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -548,303 +276,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - - -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -852,6 +321,7 @@ def load_config(module, candidate): return result # END OF COMMON CODE + PARAM_TO_COMMAND_KEYMAP = { 'router_id': 'router-id', 'default_metric': 'default-metric', @@ -909,7 +379,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = ['router ospf {0}'.format(module.params['ospf'])] if module.params['vrf'] != 'default': @@ -1049,13 +519,13 @@ def main(): timer_throttle_spf_hold=dict(required=False, type='str'), timer_throttle_spf_max=dict(required=False, type='str'), auto_cost=dict(required=False, type='str'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, + module = get_network_module(argument_spec=argument_spec, supports_check_mode=True) state = module.params['state'] @@ -1108,7 +578,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From 030141e76d4624ca8099a341afc1356a1b0d9b18 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:11:21 +0200 Subject: [PATCH 128/770] Fixed module structure --- network/nxos/nxos_bgp_af.py | 643 ++++-------------------------------- 1 file changed, 57 insertions(+), 586 deletions(-) diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index a86b4e96064..1ff9182b5d7 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -237,12 +237,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' # configure a simple address-family @@ -258,17 +252,18 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"advertise_l2vpn_evpn": true, "afi": "ipv4", "asn": "65535", "safi": "unicast", "vrf": "TESTING"} existing: description: k/v pairs of existing BGP AF configuration + returned: verbose mode type: dict sample: {} end_state: description: k/v pairs of BGP AF configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"additional_paths_install": false, "additional_paths_receive": false, @@ -301,203 +296,51 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -510,119 +353,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -673,303 +403,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -1209,7 +680,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) try: asn_regex = '.*router\sbgp\s(?P\d+).*' @@ -1511,18 +982,18 @@ def main(): suppress_inactive=dict(required=False, type='bool'), table_map=dict(required=False, type='str'), table_map_filter=dict(required=False, type='bool'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - required_together=[DAMPENING_PARAMS, + module = get_network_module(argument_spec=argument_spec, + required_together=[DAMPENING_PARAMS, ['distance_ibgp', 'distance_ebgp', 'distance_local']], - supports_check_mode=True) + supports_check_mode=True) state = module.params['state'] if module.params['dampening_routemap']: @@ -1620,7 +1091,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From 18562835e3db185e7b775281f989f43131790050 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:22:41 +0200 Subject: [PATCH 129/770] Fixed module structure --- network/nxos/nxos_bgp_neighbor.py | 644 +++--------------------------- 1 file changed, 58 insertions(+), 586 deletions(-) diff --git a/network/nxos/nxos_bgp_neighbor.py b/network/nxos/nxos_bgp_neighbor.py index db1c1e8821c..fdf492c8b5c 100644 --- a/network/nxos/nxos_bgp_neighbor.py +++ b/network/nxos/nxos_bgp_neighbor.py @@ -180,12 +180,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' # create a new neighbor @@ -206,7 +200,7 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"asn": "65535", "description": "just a description", "local_as": "20", "neighbor": "3.3.3.3", @@ -214,11 +208,12 @@ "update_source": "Ethernet1/3", "vrf": "default"} existing: description: k/v pairs of existing BGP neighbor configuration + returned: verbose mode type: dict sample: {} end_state: description: k/v pairs of BGP neighbor configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"asn": "65535", "capability_negotiation": false, "connected_check": false, "description": "just a description", @@ -246,203 +241,51 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -455,119 +298,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -618,303 +348,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -1042,7 +513,7 @@ def get_custom_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) custom = [ 'log_neighbor_changes', 'pwd', @@ -1204,13 +675,14 @@ def main(): m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - required_together=[['pwd', 'pwd_type'], - ['timers_holdtime', 'timers_keepalive']], - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + required_together=[['timer_bgp_hold', + 'timer_bgp_keepalive']], + supports_check_mode=True) state = module.params['state'] if module.params['pwd_type'] == 'default': @@ -1278,7 +750,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From 37d27c9b2cf33ecf09a24f5cb249da7843afcd97 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:30:19 +0200 Subject: [PATCH 130/770] Fixed module structure --- network/nxos/nxos_bgp_neighbor_af.py | 643 +++------------------------ 1 file changed, 57 insertions(+), 586 deletions(-) diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py index 11b0899dc64..5dfd6fc2ac8 100644 --- a/network/nxos/nxos_bgp_neighbor_af.py +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -253,12 +253,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' configure RR client @@ -277,18 +271,19 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"afi": "ipv4", "asn": "65535", "neighbor": "3.3.3.3", "route_reflector_client": true, "safi": "unicast", "vrf": "default"} existing: description: k/v pairs of existing configuration + returned: verbose mode type: dict sample: {} end_state: description: k/v pairs of configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"additional_paths_receive": "inherit", "additional_paths_send": "inherit", @@ -323,203 +318,51 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -532,119 +375,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -695,303 +425,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -1173,7 +644,7 @@ def get_custom_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) custom = [ 'allowas_in_max', @@ -1524,16 +995,16 @@ def main(): suppress_inactive=dict(required=False, type='bool'), unsuppress_map=dict(required=False, type='str'), weight=dict(required=False, type='str'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - mutually_exclusive=[['advertise_map_exist', + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['advertise_map_exist', 'advertise_map_non_exist']], - supports_check_mode=True) + supports_check_mode=True) state = module.params['state'] if ((module.params['max_prefix_interval'] or @@ -1632,7 +1103,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From d8e6773f8ca45b62b3bd3715a4ac1de7c7eb411a Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:36:48 +0200 Subject: [PATCH 131/770] Fixed module structure --- network/nxos/nxos_evpn_vni.py | 641 +++------------------------------- 1 file changed, 56 insertions(+), 585 deletions(-) diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py index 13cc756e043..1f759a69def 100644 --- a/network/nxos/nxos_evpn_vni.py +++ b/network/nxos/nxos_evpn_vni.py @@ -74,12 +74,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_evpn_vni: @@ -98,19 +92,20 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"route_target_import": ["5000:10", "4100:100", "5001:10"],"vni": "6000"} existing: description: k/v pairs of existing EVPN VNI configuration + returned: verbose mode type: dict sample: {"route_distinguisher": "70:10", "route_target_both": [], "route_target_export": [], "route_target_import": [ "4100:100", "5000:10"], "vni": "6000"} end_state: description: k/v pairs of EVPN VNI configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"route_distinguisher": "70:10", "route_target_both": [], "route_target_export": [], "route_target_import": [ @@ -128,203 +123,51 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -337,119 +180,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -500,303 +230,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -844,7 +315,7 @@ def get_route_target_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = ['evpn', 'vni {0} l2'.format(module.params['vni'])] config = netcfg.get_section(parents) @@ -940,13 +411,13 @@ def main(): route_target_both=dict(required=False, type='list'), route_target_import=dict(required=False, type='list'), route_target_export=dict(required=False, type='list'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, + module = get_network_module(argument_spec=argument_spec, supports_check_mode=True) state = module.params['state'] @@ -993,7 +464,7 @@ def main(): candidate.add(remove_commands, parents=parents) result = execute_config(module, candidate) - time.sleep(20) + time.sleep(30) candidate = CustomNetworkConfig(indent=3) candidate.add(commands, parents=parents) @@ -1002,7 +473,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From a30d508e0e6d22bbd17e608f2f1591aab84c0441 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:37:52 +0200 Subject: [PATCH 132/770] Fixed module structure --- network/nxos/nxos_evpn_vni.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py index 1f759a69def..261a839fef8 100644 --- a/network/nxos/nxos_evpn_vni.py +++ b/network/nxos/nxos_evpn_vni.py @@ -127,6 +127,7 @@ 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 From 67c24e5f04d8abd28d010fac71436c20f7de9e7c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:45:17 +0200 Subject: [PATCH 133/770] Fixed module structure --- network/nxos/nxos_file_copy.py | 642 ++++----------------------------- 1 file changed, 70 insertions(+), 572 deletions(-) diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index 8a06a96610f..0a27c5bae93 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -83,203 +83,52 @@ import time # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -292,119 +141,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -455,303 +191,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -760,6 +237,11 @@ def load_config(module, candidate): # END OF COMMON CODE 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) @@ -769,6 +251,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -866,9 +361,12 @@ def main(): local_file=dict(required=True), remote_file=dict(required=False), file_system=dict(required=False, default='bootflash:'), + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) local_file = module.params['local_file'] remote_file = module.params['remote_file'] From c9d293299ba3d291bbc997f85288f7d31acc05ce Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 17:54:53 +0200 Subject: [PATCH 134/770] Fixed module structure --- network/nxos/nxos_hsrp.py | 659 +++++--------------------------------- 1 file changed, 84 insertions(+), 575 deletions(-) diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index 174915cb515..1fa9c77873e 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -118,205 +118,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time import collections -import itertools -import shlex import json -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -329,119 +180,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -492,303 +230,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -798,12 +277,21 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: - body = module.configure(commands) + response = module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) - return body + except AttributeError: + try: + commands.insert(0, 'configure') + module.cli.add_commands(commands, output='config') + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return response def get_cli_body_ssh(command, response, module): @@ -829,6 +317,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -836,8 +329,21 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -1089,9 +595,12 @@ def main(): auth_string=dict(type='str', required=False), state=dict(choices=['absent', 'present'], required=False, default='present'), + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) interface = module.params['interface'].lower() group = module.params['group'] From b2aa859b330f11db402a08e1ac269199618c9845 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:01:33 +0200 Subject: [PATCH 135/770] Fixed module structure --- network/nxos/nxos_interface_ospf.py | 650 +++------------------------- 1 file changed, 61 insertions(+), 589 deletions(-) diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py index fc424e2805f..3966580e74c 100644 --- a/network/nxos/nxos_interface_ospf.py +++ b/network/nxos/nxos_interface_ospf.py @@ -111,12 +111,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_interface_ospf: @@ -132,11 +126,12 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"area": "1", "interface": "ethernet1/32", "ospf": "1"} existing: description: k/v pairs of existing OSPF configuration + returned: verbose mode type: dict sample: {"area": "", "cost": "", "dead_interval": "", "hello_interval": "", "interface": "ethernet1/32", @@ -146,7 +141,7 @@ "ospf": "", "passive_interface": false} end_state: description: k/v pairs of OSPF configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"area": "0.0.0.1", "cost": "", "dead_interval": "", "hello_interval": "", "interface": "ethernet1/32", @@ -168,203 +163,52 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -377,119 +221,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -540,303 +271,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -951,7 +423,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = ['interface {0}'.format(module.params['interface'].capitalize())] config = netcfg.get_section(parents) if 'ospf' in config: @@ -1120,18 +592,18 @@ def main(): message_digest_encryption_type=dict(required=False, type='str', choices=['cisco_type_7','3des']), message_digest_password=dict(required=False, type='str'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - required_together=[['message_digest_key_id', - 'message_digest_algorithm_type', - 'message_digest_encryption_type', - 'message_digest_password']], - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + required_together=[['message_digest_key_id', + 'message_digest_algorithm_type', + 'message_digest_encryption_type', + 'message_digest_password']], + supports_check_mode=True) for param in ['message_digest_encryption_type', 'message_digest_algorithm_type', @@ -1194,7 +666,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From fa72ffc3d1a6106b1a4eb25d9eb09a33cdceffe5 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:09:30 +0200 Subject: [PATCH 136/770] Fixed module structure --- network/nxos/nxos_overlay_global.py | 686 ++++------------------------ 1 file changed, 96 insertions(+), 590 deletions(-) diff --git a/network/nxos/nxos_overlay_global.py b/network/nxos/nxos_overlay_global.py index 6e4f1317547..8736f373243 100644 --- a/network/nxos/nxos_overlay_global.py +++ b/network/nxos/nxos_overlay_global.py @@ -35,13 +35,8 @@ - Anycast gateway mac of the switch. required: true default: null - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' + EXAMPLES = ''' - nxos_overlay_global: anycast_gateway_mac="b.b.b" @@ -53,23 +48,56 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict - sample: {"12:34:56:78:9a:bc"} + sample: {"asn": "65535", "router_id": "1.1.1.1", "vrf": "test"} existing: - description: k/v pairs of existing configuration + description: k/v pairs of existing BGP configuration + returned: verbose mode type: dict - sample: {"anycast_gateway_mac": "000E.000E.000E"} + sample: {"asn": "65535", "bestpath_always_compare_med": false, + "bestpath_aspath_multipath_relax": false, + "bestpath_compare_neighborid": false, + "bestpath_compare_routerid": false, + "bestpath_cost_community_ignore": false, + "bestpath_med_confed": false, + "bestpath_med_missing_as_worst": false, + "bestpath_med_non_deterministic": false, "cluster_id": "", + "confederation_id": "", "confederation_peers": "", + "graceful_restart": true, "graceful_restart_helper": false, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", "local_as": "", + "log_neighbor_changes": false, "maxas_limit": "", + "neighbor_down_fib_accelerate": false, "reconnect_interval": "60", + "router_id": "11.11.11.11", "suppress_fib_pending": false, + "timer_bestpath_limit": "", "timer_bgp_hold": "180", + "timer_bgp_keepalive": "60", "vrf": "test"} end_state: - description: k/v pairs of configuration after module execution - returned: always + description: k/v pairs of BGP configuration after module execution + returned: verbose mode type: dict - sample: {"anycast_gateway_mac": "1234.5678.9ABC"} + sample: {"asn": "65535", "bestpath_always_compare_med": false, + "bestpath_aspath_multipath_relax": false, + "bestpath_compare_neighborid": false, + "bestpath_compare_routerid": false, + "bestpath_cost_community_ignore": false, + "bestpath_med_confed": false, + "bestpath_med_missing_as_worst": false, + "bestpath_med_non_deterministic": false, "cluster_id": "", + "confederation_id": "", "confederation_peers": "", + "graceful_restart": true, "graceful_restart_helper": false, + "graceful_restart_timers_restart": "120", + "graceful_restart_timers_stalepath_time": "300", "local_as": "", + "log_neighbor_changes": false, "maxas_limit": "", + "neighbor_down_fib_accelerate": false, "reconnect_interval": "60", + "router_id": "1.1.1.1", "suppress_fib_pending": false, + "timer_bestpath_limit": "", "timer_bgp_hold": "180", + "timer_bgp_keepalive": "60", "vrf": "test"} updates: description: commands sent to the device returned: always type: list - sample: ["fabric forwarding anycast-gateway-mac 1234.5678.9ABC"] + sample: ["router bgp 65535", "vrf test", "router-id 1.1.1.1"] changed: description: check to see if a change was made on the device returned: always @@ -78,203 +106,52 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __str__(self): - return self.raw +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -287,119 +164,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -450,303 +214,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -775,7 +280,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - config = str(custom_get_config(module)) + config = str(get_config(module)) for arg in args: existing[arg] = get_value(arg, config, module) @@ -863,11 +368,12 @@ def main(): argument_spec = dict( anycast_gateway_mac=dict(required=True, type='str'), m_facts=dict(required=False, default=False, type='bool'), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) args = [ 'anycast_gateway_mac' @@ -890,7 +396,7 @@ def main(): module.fail_json(msg=str(exc)) result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From cb9bdee7bed4911551590602771b2cfd136db2f4 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:15:08 +0200 Subject: [PATCH 137/770] Fixed module structure --- network/nxos/nxos_pim.py | 643 ++++----------------------------------- 1 file changed, 58 insertions(+), 585 deletions(-) diff --git a/network/nxos/nxos_pim.py b/network/nxos/nxos_pim.py index 063d6b5bffe..6a417a8c2dc 100644 --- a/network/nxos/nxos_pim.py +++ b/network/nxos/nxos_pim.py @@ -20,7 +20,7 @@ --- module: nxos_pim version_added: "2.2" -short_description: Manages configuration of an Protocol Independent Multicast (PIM) instance. +short_description: Manages configuration of an Protocol Independent Multicast(PIM) instance. description: - Manages configuration of an Protocol Independent Multicast (PIM) instance. author: Gabriele Gerbino (@GGabriele) @@ -31,12 +31,6 @@ - Configure group ranges for Source Specific Multicast (SSM). Valid values are multicast addresses or the keyword 'none'. required: true - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_pim: @@ -49,16 +43,17 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"ssm_range": "232.0.0.0/8"} existing: description: k/v pairs of existing PIM configuration + returned: verbose mode type: dict sample: {"ssm_range": none} end_state: description: k/v pairs of BGP configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"ssm_range": "232.0.0.0/8"} updates: @@ -75,203 +70,52 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - def __str__(self): - return self.raw +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __ne__(self, other): - return not self.__eq__(other) -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -284,119 +128,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -447,303 +178,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -775,7 +247,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - config = str(custom_get_config(module)) + config = str(get_config(module)) for arg in args: existing[arg] = get_value(arg, config, module) return existing @@ -811,11 +283,12 @@ def main(): argument_spec = dict( ssm_range=dict(required=True, type='str'), m_facts=dict(required=False, default=False, type='bool'), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) splitted_ssm_range = module.params['ssm_range'].split('.') if len(splitted_ssm_range) != 4 and module.params['ssm_range'] != 'none': @@ -843,7 +316,7 @@ def main(): module.fail_json(msg=str(exc)) result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From e8b9a7cae7adf7f248255769defc87c8bc871427 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:21:47 +0200 Subject: [PATCH 138/770] Fixed module structure --- network/nxos/nxos_pim_rp_address.py | 648 +++------------------------- 1 file changed, 60 insertions(+), 588 deletions(-) diff --git a/network/nxos/nxos_pim_rp_address.py b/network/nxos/nxos_pim_rp_address.py index 984df580a42..e85bcc1eaf0 100644 --- a/network/nxos/nxos_pim_rp_address.py +++ b/network/nxos/nxos_pim_rp_address.py @@ -59,12 +59,6 @@ required: false choices: ['true','false'] default: null - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_pim_rp_address: @@ -78,16 +72,17 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"rp_address": "10.1.1.21"} existing: description: list of existing pim rp-address configuration entries + returned: verbose mode type: list sample: [] end_state: description: pim rp-address configuration entries after module execution - returned: always + returned: verbose mode type: list sample: [{"bidir": false, "group_list": "224.0.0.0/4", "rp_address": "10.1.1.21"}] @@ -106,203 +101,52 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -315,119 +159,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -478,303 +209,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -821,7 +293,7 @@ def get_value(config, module): def get_existing(module, args): existing = {} - config = str(custom_get_config(module)) + config = str(get_config(module)) existing = get_value(config, module) return existing @@ -876,17 +348,17 @@ def main(): prefix_list=dict(required=False, type='str'), route_map=dict(required=False, type='str'), bidir=dict(required=False, type='bool'), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - mutually_exclusive=[['group_list', 'route_map'], - ['group_list', 'prefix_list'], - ['route_map', 'prefix_list']], - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['group_list', 'route_map'], + ['group_list', 'prefix_list'], + ['route_map', 'prefix_list']], + supports_check_mode=True) state = module.params['state'] @@ -925,7 +397,7 @@ def main(): module.fail_json(msg=str(exc)) result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From c3592688e66ed28e33462ecaf093a140fc4586a4 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:29:08 +0200 Subject: [PATCH 139/770] Fixed module structure --- network/nxos/nxos_portchannel.py | 661 ++++--------------------------- 1 file changed, 83 insertions(+), 578 deletions(-) diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index 04e550859b6..856f79b1a3c 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -122,205 +122,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time import collections -import itertools -import shlex import json -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -333,119 +184,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -496,310 +234,50 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.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 - WARNINGS = [] PARAM_TO_COMMAND_KEYMAP = { 'min_links': 'lacp min-links' @@ -852,6 +330,15 @@ def execute_config_command(commands, module): 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') + output = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) return output @@ -865,6 +352,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -874,7 +366,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -905,7 +409,7 @@ def get_portchannel_mode(interface, protocol, module, netcfg): if protocol != 'LACP': mode = 'on' else: - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = ['interface {0}'.format(interface.capitalize())] body = netcfg.get_section(parents) @@ -979,7 +483,7 @@ def get_portchannel(module, netcfg=None): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) interface_exist = check_interface(module, netcfg) if interface_exist: @@ -1143,11 +647,12 @@ def main(): choices=['true', 'false']), state=dict(required=False, choices=['absent', 'present'], default='present'), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) group = str(module.params['group']) mode = module.params['mode'] From c50cfe345147d46c1996018df1427eed1dea6ed2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:42:01 +0200 Subject: [PATCH 140/770] Fixed module structure --- network/nxos/nxos_smu.py | 654 +++++---------------------------------- 1 file changed, 82 insertions(+), 572 deletions(-) diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index ddab9f0d2b0..213f525abb6 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -77,204 +77,56 @@ ''' import time -# COMMON CODE FOR MIGRATION - -import re -import time +import json import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -287,119 +139,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -450,303 +189,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -755,6 +235,11 @@ def load_config(module, candidate): # END OF COMMON CODE 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) @@ -764,6 +249,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -793,6 +291,15 @@ def execute_config_command(commands, module): 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') + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) return response @@ -835,9 +342,12 @@ def main(): argument_spec = dict( pkg=dict(required=True), file_system=dict(required=False, default='bootflash:'), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) pkg = module.params['pkg'] file_system = module.params['file_system'] From a4ee5c89dac1160cf9af2c1e1b7181c88ec2b59c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:48:08 +0200 Subject: [PATCH 141/770] Fixed module structure --- network/nxos/nxos_vpc.py | 653 +++++---------------------------------- 1 file changed, 79 insertions(+), 574 deletions(-) diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py index 1b1f9b8ad7a..410244a4eb1 100644 --- a/network/nxos/nxos_vpc.py +++ b/network/nxos/nxos_vpc.py @@ -142,204 +142,52 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex -import json - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -352,119 +200,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -515,303 +250,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -826,6 +302,15 @@ def execute_config_command(commands, module): 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): @@ -849,6 +334,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -858,6 +348,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -1063,10 +566,12 @@ def main(): auto_recovery=dict(required=True, type='bool'), delay_restore=dict(required=False, type='str'), state=dict(choices=['absent', 'present'], default='present'), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) domain = module.params['domain'] role_priority = module.params['role_priority'] From 9f06bb7e604b3ebbd3c5eb28aa25a45846b6e673 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:55:36 +0200 Subject: [PATCH 142/770] fix imports --- network/nxos/nxos_vpc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py index 410244a4eb1..bc0a871c212 100644 --- a/network/nxos/nxos_vpc.py +++ b/network/nxos/nxos_vpc.py @@ -141,6 +141,9 @@ sample: true ''' +import json +import collections + # COMMON CODE FOR MIGRATION import re From fdda32ec525d92a0ed14f763ec9bd305d078bc99 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:58:09 +0200 Subject: [PATCH 143/770] Fixed module structure --- network/nxos/nxos_vpc_interface.py | 659 ++++------------------------- 1 file changed, 84 insertions(+), 575 deletions(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index f5b5dd1d5c6..985516d88df 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -29,7 +29,7 @@ - Gabriele Gerbino (@GGabriele) notes: - Either vpc or peer_link param is required, but not both. - - C(state)=absent removes whatever VPC config is on a port-channel + - I(state)=absent removes whatever VPC config is on a port-channel if one exists. - Re-assigning a vpc or peerlink from one portchannel to another is not supported. The module will force the user to unconfigure an existing @@ -92,205 +92,57 @@ sample: true ''' -# COMMON CODE FOR MIGRATION -import re -import time import collections -import itertools -import shlex import json -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -303,119 +155,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -466,303 +205,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -772,12 +252,20 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: - output = module.configure(commands) + module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) - return output + 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): @@ -799,6 +287,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -808,6 +301,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -970,10 +476,13 @@ def main(): vpc=dict(required=False, type='str'), peer_link=dict(required=False, type='bool'), state=dict(choices=['absent', 'present'], default='present'), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - mutually_exclusive=[['vpc', 'peer_link']], - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['vpc', 'peer_link']], + supports_check_mode=True) portchannel = module.params['portchannel'] vpc = module.params['vpc'] From c177bc4f7259dc2566f35acf4f76f9b4069e26f2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 18:59:56 +0200 Subject: [PATCH 144/770] Fixed config function --- network/nxos/nxos_vpc_interface.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 985516d88df..9f0b2b8f07f 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -252,7 +252,7 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: - module.configure(commands) + response = module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', @@ -261,11 +261,12 @@ def execute_config_command(commands, module): try: commands.insert(0, 'configure') module.cli.add_commands(commands, output='config') - module.cli.run_commands() + response = module.cli.run_commands() except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) + return response def get_cli_body_ssh(command, response, module): From 7579ddee0ccb25d42fe964fb224d86fe94d2b50c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 19:08:29 +0200 Subject: [PATCH 145/770] Fixed config function --- network/nxos/nxos_vrf.py | 659 +++++---------------------------------- 1 file changed, 83 insertions(+), 576 deletions(-) diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index 3cbc7d5425f..08de47ff6f3 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -77,9 +77,7 @@ EXAMPLES = ''' # ensure ntc VRF exists on switch -- nxos_vrf: vrf=ntc host=68.170.147.165 -# ensure ntc VRF does not exist on switch -- nxos_vrf: vrf=ntc host=68.170.147.165 state=absent +- nxos_vrf: vrf=ntc username="{{ un }}" password="{{ pwd }}" host="{{ inventory_hostname }}" ''' RETURN = ''' @@ -112,205 +110,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time -import collections -import itertools -import shlex import json +import collections -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -323,119 +172,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -486,303 +222,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -797,6 +274,15 @@ def execute_config_command(commands, module): 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_vrf(module, command, response): @@ -818,6 +304,11 @@ def get_cli_body_ssh_vrf(module, command, response): 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) @@ -827,6 +318,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -946,9 +450,12 @@ def main(): required=False), state=dict(default='present', choices=['present', 'absent'], required=False), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) vrf = module.params['vrf'] admin_state = module.params['admin_state'].lower() From 336794d758c8ae203809a807b7b73e73fb0385a6 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 19:14:15 +0200 Subject: [PATCH 146/770] Fixed config function --- network/nxos/nxos_vrf_af.py | 642 ++++-------------------------------- 1 file changed, 57 insertions(+), 585 deletions(-) diff --git a/network/nxos/nxos_vrf_af.py b/network/nxos/nxos_vrf_af.py index b1c4d8d02f2..41f36cdc27f 100644 --- a/network/nxos/nxos_vrf_af.py +++ b/network/nxos/nxos_vrf_af.py @@ -57,12 +57,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_vrf_af: @@ -73,22 +67,22 @@ password: "{{ pwd }}" host: "{{ inventory_hostname }}" ''' - RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"afi": "ipv4", "route_target_both_auto_evpn": true, "safi": "unicast", "vrf": "test"} existing: description: k/v pairs of existing configuration + returned: verbose mode type: dict sample: {"afi": "ipv4", "route_target_both_auto_evpn": false, "safi": "unicast", "vrf": "test"} end_state: description: k/v pairs of configuration after module execution - returned: always + returned: verbose mode type: dict sample: {"afi": "ipv4", "route_target_both_auto_evpn": true, "safi": "unicast", "vrf": "test"} @@ -106,203 +100,52 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -315,119 +158,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -478,303 +208,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - - -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -814,7 +285,7 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) parents = ['vrf context {0}'.format(module.params['vrf'])] parents.append('address-family {0} {1}'.format(module.params['afi'], @@ -898,11 +369,12 @@ def main(): m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] @@ -943,7 +415,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From 78558fcf53f7e29b675f1346274fe5d09de1a39d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 19:47:02 +0200 Subject: [PATCH 147/770] Fixed config function --- network/nxos/nxos_vxlan_vtep.py | 655 ++++---------------------------- 1 file changed, 69 insertions(+), 586 deletions(-) diff --git a/network/nxos/nxos_vxlan_vtep.py b/network/nxos/nxos_vxlan_vtep.py index 0e30cdbfca4..6581370bf93 100644 --- a/network/nxos/nxos_vxlan_vtep.py +++ b/network/nxos/nxos_vxlan_vtep.py @@ -71,12 +71,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_vxlan_vtep: @@ -94,18 +88,19 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: always + returned: verbose mode type: dict sample: {"description": "simple description", "host_reachability": true, "interface": "nve1", "shutdown": true, "source_interface": "loopback0", "source_interface_hold_down_time": "30"} existing: description: k/v pairs of existing VXLAN VTEP configuration + returned: verbose mode type: dict sample: {} end_state: - description: k/v pairs of BGP configuration after module execution - returned: always + description: k/v pairs of VXLAN VTEP configuration after module execution + returned: verbose mode type: dict sample: {"description": "simple description", "host_reachability": true, "interface": "nve1", "shutdown": true, "source_interface": "loopback0", @@ -125,203 +120,52 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __str__(self): - return self.raw - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -334,119 +178,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -497,303 +228,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - - -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -872,9 +344,10 @@ def get_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) - parents = ['interface {0}'.format(module.params['interface'].lower())] + interface_string = 'interface {0}'.format(module.params['interface'].lower()) + parents = [interface_string] config = netcfg.get_section(parents) if config: @@ -882,6 +355,11 @@ def get_existing(module, args): existing[arg] = get_value(arg, config, module) existing['interface'] = module.params['interface'].lower() + else: + if interface_string in str(netcfg): + existing['interface'] = module.params['interface'].lower() + for arg in args: + existing[arg] = '' return existing @@ -949,6 +427,10 @@ def state_present(module, existing, proposed, candidate): commands = fix_commands(commands, module) parents = ['interface {0}'.format(module.params['interface'].lower())] candidate.add(commands, parents=parents) + else: + if not existing and module.params['interface']: + commands = ['interface {0}'.format(module.params['interface'].lower())] + candidate.add(commands, parents=[]) def state_absent(module, existing, proposed, candidate): @@ -967,11 +449,12 @@ def main(): m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] interface = module.params['interface'].lower() @@ -1026,7 +509,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From 0cb9a022029f0c13e3799f7ffb42982cae1bb26a Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 19:55:19 +0200 Subject: [PATCH 148/770] Fixed module structure --- network/nxos/nxos_vxlan_vtep_vni.py | 642 +++------------------------- 1 file changed, 57 insertions(+), 585 deletions(-) diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py index ab8c8815abc..e3b552cacbe 100644 --- a/network/nxos/nxos_vxlan_vtep_vni.py +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -77,12 +77,6 @@ required: false default: present choices: ['present','absent'] - m_facts: - description: - - Used to print module facts - required: false - default: false - choices: ['true','false'] ''' EXAMPLES = ''' - nxos_vxlan_vtep_vni: @@ -97,17 +91,17 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: when C(m_facts)=true + returned: verbose mode type: dict sample: {"ingress_replication": "default", "interface": "nve1", "vni": "6000"} existing: description: k/v pairs of existing configuration - returned: when C(m_facts)=true + returned: verbose mode type: dict sample: {} end_state: description: k/v pairs of configuration after module execution - returned: when C(m_facts)=true + returned: verbose mode type: dict sample: {"assoc_vrf": false, "ingress_replication": "", "interface": "nve1", "multicast_group": "", "peer_list": [], @@ -125,203 +119,52 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - def __str__(self): - return self.raw +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __ne__(self, other): - return not self.__eq__(other) -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -334,119 +177,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -497,303 +227,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - - -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -868,7 +339,7 @@ def get_custom_value(arg, config, module): def get_existing(module, args): existing = {} - netcfg = custom_get_config(module) + netcfg = get_config(module) custom = [ 'assoc_vrf', @@ -997,11 +468,12 @@ def main(): m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) if module.params['assoc_vrf']: mutually_exclusive_params = ['multicast_group', @@ -1077,7 +549,7 @@ def main(): result['updates'] = [] result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state, interface_exist = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From c9c3d26e4212f14a4babf0567eeaae4b9e9acfd0 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:06:07 +0200 Subject: [PATCH 149/770] Fixed module structure --- network/nxos/nxos_rollback.py | 657 +++++----------------------------- 1 file changed, 82 insertions(+), 575 deletions(-) diff --git a/network/nxos/nxos_rollback.py b/network/nxos/nxos_rollback.py index 50e2b76cd77..360bbb047d8 100644 --- a/network/nxos/nxos_rollback.py +++ b/network/nxos/nxos_rollback.py @@ -74,203 +74,52 @@ # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue - - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -283,119 +132,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -446,303 +182,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST', timeout=20) - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -752,15 +229,34 @@ def load_config(module, candidate): def execute_commands(cmds, module, command_type=None): + command_type_map = { + 'cli_show': 'json', + 'cli_show_ascii': 'text' + } + try: if command_type: - module.execute(cmds, command_type=command_type) + response = module.execute(cmds, command_type=command_type) else: - module.execute(cmds) + 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, output=command_type) + 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 prepare_show_command(command, module): @@ -777,16 +273,27 @@ def checkpoint(filename, module): def rollback(filename, module): commands = ['rollback running-config file %s' % filename] - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + try: + 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 main(): argument_spec = dict( checkpoint_file=dict(required=False), rollback_to=dict(required=False), + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, + module = get_network_module(argument_spec=argument_spec, mutually_exclusive=[['checkpoint_file', 'rollback_to']], supports_check_mode=False) From c58d21a5c6d1b1e8ddce4cc650a0532f6b3b05e6 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:11:03 +0200 Subject: [PATCH 150/770] Fixed module structure --- network/nxos/nxos_feature.py | 660 +++++------------------------------ 1 file changed, 81 insertions(+), 579 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index 04b7a6ce64e..bbee766e913 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -65,11 +65,6 @@ returned: always type: dict sample: {"state": "disabled"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "disabled" updates: description: commands sent to the device returned: always @@ -87,206 +82,56 @@ sample: "vpc" ''' +import json +import collections # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex -import json - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __str__(self): - return self.raw - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -299,119 +144,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -462,303 +194,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -774,6 +247,15 @@ def execute_config_command(commands, module): 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): @@ -795,6 +277,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -804,6 +291,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -903,10 +403,12 @@ def main(): feature=dict(type='str', required=True), state=dict(choices=['enabled', 'disabled'], default='enabled', required=False), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) feature = validate_feature(module) state = module.params['state'].lower() From 6c0ce20ad7642f4a3c18faac29ccbab68b864470 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:27:02 +0200 Subject: [PATCH 151/770] Fixed module structure --- network/nxos/nxos_reboot.py | 650 +++++------------------------------- 1 file changed, 75 insertions(+), 575 deletions(-) diff --git a/network/nxos/nxos_reboot.py b/network/nxos/nxos_reboot.py index 5bf5eb3626e..1797163f3ec 100644 --- a/network/nxos/nxos_reboot.py +++ b/network/nxos/nxos_reboot.py @@ -54,204 +54,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time +import json import collections -import itertools -import shlex - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -264,119 +116,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -427,303 +166,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -739,6 +219,11 @@ def reboot(module): 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) @@ -748,12 +233,25 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + 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': - body = execute_show(command, module, reboot=reboot) + body = execute_show(command, module) elif module.params['transport'] == 'nxapi': body = execute_show(command, module, command_type=command_type) @@ -761,17 +259,19 @@ def execute_show_command(command, module, command_type='cli_show'): def disable_confirmation(module): - command = 'terminal dont-ask' + command = ['terminal dont-ask'] body = execute_show_command(command, module, command_type='cli_show_ascii')[0] def main(): argument_spec = dict( confirm=dict(required=True, type='bool'), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) confirm = module.params['confirm'] if not confirm: From 85555199456c1d436576970462b5a0d7930a4418 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:33:03 +0200 Subject: [PATCH 152/770] Fixed module structure --- network/nxos/nxos_ip_interface.py | 661 ++++-------------------------- 1 file changed, 82 insertions(+), 579 deletions(-) diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index 376a45e326e..09e95a06cd9 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -83,11 +83,6 @@ sample: {"addresses": [{"addr": "20.20.20.20", "mask": 24}], "interface": "ethernet1/32", "prefix": "20.20.20.0", "type": "ethernet", "vrf": "default"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: commands sent to the device returned: always @@ -100,205 +95,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time -import collections -import itertools -import shlex import json +import collections -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') +# COMMON CODE FOR MIGRATION +import re - ancestors = list() - config = list() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - cfg = ConfigLine(text) - cfg.raw = line - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -311,119 +157,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -474,303 +207,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -785,6 +259,15 @@ def execute_config_command(commands, module): 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): @@ -809,6 +292,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -818,6 +306,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -1119,10 +620,12 @@ def main(): mask=dict(type='str', required=False), state=dict(required=False, default='present', choices=['present', 'absent']), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) addr = module.params['addr'] version = module.params['version'] From fe120c14a3702758c4cb0c9fd53d33d4a24d9e83 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:35:57 +0200 Subject: [PATCH 153/770] Fixed module structure --- network/nxos/nxos_ping.py | 646 +++++--------------------------------- 1 file changed, 73 insertions(+), 573 deletions(-) diff --git a/network/nxos/nxos_ping.py b/network/nxos/nxos_ping.py index b8d9526639d..72e87018d97 100644 --- a/network/nxos/nxos_ping.py +++ b/network/nxos/nxos_ping.py @@ -103,205 +103,56 @@ sample: "0.00%" ''' -# COMMON CODE FOR MIGRATION - -import re -import time -import collections -import itertools -import shlex import json +import collections -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -314,119 +165,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -477,303 +215,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -822,6 +301,11 @@ def get_statistics_summary_line(response_as_list): 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) @@ -831,6 +315,19 @@ def execute_show(cmds, module, command_type=None): 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -874,9 +371,12 @@ def main(): source=dict(required=False), state=dict(required=False, choices=['present', 'absent'], default='present'), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) destination = module.params['dest'] count = module.params['count'] From 0e8eab40fb70f6da2b5d77549c1f33328cd91944 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:43:06 +0200 Subject: [PATCH 154/770] Fixed module structure --- network/nxos/nxos_vlan.py | 666 +++++--------------------------------- 1 file changed, 85 insertions(+), 581 deletions(-) diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index 7f6eac86b31..dae7baa796b 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -120,11 +120,6 @@ type: dict or null sample: {"admin_state": "down", "name": "app_vlan", "vlan_id": "20", "vlan_state": "suspend", "mapped_vni": "5000"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: command string sent to the device returned: always @@ -138,205 +133,56 @@ ''' -# COMMON CODE FOR MIGRATION - -import re -import time -import collections -import itertools -import shlex import json +import collections -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') +# COMMON CODE FOR MIGRATION +import re - ancestors = list() - config = list() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - cfg = ConfigLine(text) - cfg.raw = line - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -349,119 +195,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -512,303 +245,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -982,6 +456,15 @@ def execute_config_command(commands, module): 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): @@ -1005,6 +488,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -1012,8 +500,21 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -1041,11 +542,14 @@ def main(): state=dict(choices=['present', 'absent'], default='present', required=False), admin_state=dict(choices=['up', 'down'], required=False), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - mutually_exclusive=[['vlan_range', 'name'], - ['vlan_id', 'vlan_range']], - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['vlan_range', 'name'], + ['vlan_id', 'vlan_range']], + supports_check_mode=True) vlan_range = module.params['vlan_range'] vlan_id = module.params['vlan_id'] From 303da8642357742f0f410b3e97a6a83391bfceba Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:50:18 +0200 Subject: [PATCH 155/770] Fixed module structure --- network/nxos/nxos_vrf_interface.py | 657 ++++------------------------- 1 file changed, 83 insertions(+), 574 deletions(-) diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index df273bca566..1416dc1b6c4 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -83,205 +83,56 @@ sample: true ''' -# COMMON CODE FOR MIGRATION - -import re -import time -import collections -import itertools -import shlex import json +import collections -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) - -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() +# COMMON CODE FOR MIGRATION +import re - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - cfg = ConfigLine(text) - cfg.raw = line +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - if not text or ignore_line(text, comment_tokens): - continue - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 - cfg.parents = ancestors[:level] +class CustomNetworkConfig(NetworkConfig): - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -294,119 +145,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -457,303 +195,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -770,6 +249,15 @@ def execute_config_command(commands, module): 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_vrf_interface(command, response, module): @@ -788,6 +276,11 @@ def get_cli_body_ssh_vrf_interface(command, response, module): 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) @@ -795,8 +288,21 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -901,9 +407,12 @@ def main(): interface=dict(type='str', required=True), state=dict(default='present', choices=['present', 'absent'], required=False), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) vrf = module.params['vrf'] interface = module.params['interface'].lower() From 70ef9ae42c4d986d806878755c09ca2a13948fd4 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 20:56:55 +0200 Subject: [PATCH 156/770] Fixed module structure --- network/nxos/nxos_igmp.py | 638 ++++---------------------------------- 1 file changed, 57 insertions(+), 581 deletions(-) diff --git a/network/nxos/nxos_igmp.py b/network/nxos/nxos_igmp.py index 36d59ae75a4..54370dc10b6 100644 --- a/network/nxos/nxos_igmp.py +++ b/network/nxos/nxos_igmp.py @@ -83,17 +83,17 @@ RETURN = ''' proposed: description: k/v pairs of parameters passed into module - returned: when C(m_facts)=true + returned: verbose mode type: dict sample: {"enforce_rtr_alert": true, "flush_routes": true} existing: description: k/v pairs of existing IGMP configuration - returned: when C(m_facts)=true + returned: verbose mode type: dict sample: {"enforce_rtr_alert": true, "flush_routes": false} end_state: description: k/v pairs of IGMP configuration after module execution - returned: when C(m_facts)=true + returned: verbose mode type: dict sample: {"enforce_rtr_alert": true, "flush_routes": true} updates: @@ -109,204 +109,52 @@ ''' # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex -import json - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - - def __str__(self): - return self.raw - - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents - - def __ne__(self, other): - return not self.__eq__(other) -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -319,119 +167,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -482,303 +217,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -805,7 +281,7 @@ def get_value(arg, config): def get_existing(module, args): existing = {} - config = str(custom_get_config(module)) + config = str(get_config(module)) for arg in args: existing[arg] = get_value(arg, config) @@ -861,12 +337,12 @@ def main(): enforce_rtr_alert=dict(type='bool'), restart=dict(type='bool', default=False), state=dict(choices=['present', 'default'], default='present'), - m_facts=dict(required=False, default=False, type='bool'), - include_defaults=dict(default=False) + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] restart = module.params['restart'] @@ -908,7 +384,7 @@ def main(): if restart: proposed['restart'] = restart result['connected'] = module.connected - if module.params['m_facts']: + if module._verbosity > 0: end_state = invoke('get_existing', module, args) result['end_state'] = end_state result['existing'] = existing From bc9bf359b0f5d77b458468edc6d85e486e2fca0e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 21:07:43 +0200 Subject: [PATCH 157/770] Fixed module structure --- network/nxos/nxos_igmp_interface.py | 659 ++++------------------------ 1 file changed, 82 insertions(+), 577 deletions(-) diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index 02da82ac421..cbbe56ac2a6 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -227,206 +227,56 @@ sample: true ''' +import json +import collections # COMMON CODE FOR MIGRATION - import re -import time -import collections -import itertools -import shlex -import json - -from ansible.module_utils.basic import AnsibleModule, env_fallback, get_exception -from ansible.module_utils.basic import BOOLEANS_TRUE, BOOLEANS_FALSE -from ansible.module_utils.shell import Shell, ShellError, HAS_PARAMIKO -from ansible.module_utils.netcfg import parse -from ansible.module_utils.urls import fetch_url - - -DEFAULT_COMMENT_TOKENS = ['#', '!'] - -class ConfigLine(object): - - def __init__(self, text): - self.text = text - self.children = list() - self.parents = list() - self.raw = None - - @property - def line(self): - line = ['set'] - line.extend([p.text for p in self.parents]) - line.append(self.text) - return ' '.join(line) - def __str__(self): - return self.raw +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.shell import ShellError - def __eq__(self, other): - if self.text == other.text: - return self.parents == other.parents +try: + from ansible.module_utils.nxos import get_module +except ImportError: + from ansible.module_utils.nxos import NetworkModule - def __ne__(self, other): - return not self.__eq__(other) -def ignore_line(text, tokens=None): - for item in (tokens or DEFAULT_COMMENT_TOKENS): - if text.startswith(item): - return True - -def get_next(iterable): - item, next_item = itertools.tee(iterable, 2) - next_item = itertools.islice(next_item, 1, None) - return itertools.izip_longest(item, next_item) - -def parse(lines, indent, comment_tokens=None): - toplevel = re.compile(r'\S') - childline = re.compile(r'^\s*(.+)$') - - ancestors = list() - config = list() - - for line in str(lines).split('\n'): - text = str(re.sub(r'([{};])', '', line)).strip() - - cfg = ConfigLine(text) - cfg.raw = line - - if not text or ignore_line(text, comment_tokens): - continue +def to_list(val): + if isinstance(val, (list, tuple)): + return list(val) + elif val is not None: + return [val] + else: + return list() - # handle top level commands - if toplevel.match(line): - ancestors = [cfg] - # handle sub level commands - else: - match = childline.match(line) - line_indent = match.start(1) - level = int(line_indent / indent) - parent_level = level - 1 +class CustomNetworkConfig(NetworkConfig): - cfg.parents = ancestors[:level] - - if level > len(ancestors): - config.append(cfg) + 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 - for i in range(level, len(ancestors)): - ancestors.pop() - - ancestors.append(cfg) - ancestors[parent_level].children.append(cfg) - - config.append(cfg) - - return config - - -class CustomNetworkConfig(object): - - def __init__(self, indent=None, contents=None, device_os=None): - self.indent = indent or 1 - self._config = list() - self._device_os = device_os - - if contents: - self.load(contents) - - @property - def items(self): - return self._config - - @property - def lines(self): - lines = list() - for item, next_item in get_next(self.items): - if next_item is None: - lines.append(item.line) - elif not next_item.line.startswith(item.line): - lines.append(item.line) - return lines - - def __str__(self): - text = '' + def get_object(self, path): for item in self.items: - if not item.parents: - expand = self.get_section(item.text) - text += '%s\n' % self.get_section(item.text) - return str(text).strip() - - def load(self, contents): - self._config = parse(contents, indent=self.indent) - - def load_from_file(self, filename): - self.load(open(filename).read()) - - def get(self, path): - if isinstance(path, basestring): - path = [path] - for item in self._config: if item.text == path[-1]: parents = [p.text for p in item.parents] if parents == path[:-1]: return item - def search(self, regexp, path=None): - regex = re.compile(r'^%s' % regexp, re.M) - - if path: - parent = self.get(path) - if not parent or not parent.children: - return - children = [c.text for c in parent.children] - data = '\n'.join(children) - else: - data = str(self) - - match = regex.search(data) - if match: - if match.groups(): - values = match.groupdict().values() - groups = list(set(match.groups()).difference(values)) - return (groups, match.groupdict()) - else: - return match.group() - - def findall(self, regexp): - regexp = r'%s' % regexp - return re.findall(regexp, str(self)) - - def expand(self, obj, items): - block = [item.raw for item in obj.parents] - block.append(obj.raw) - - current_level = items - for b in block: - if b not in current_level: - current_level[b] = collections.OrderedDict() - current_level = current_level[b] - for c in obj.children: - if c.raw not in current_level: - current_level[c.raw] = collections.OrderedDict() - - def to_lines(self, section): - lines = list() - for entry in section[1:]: - line = ['set'] - line.extend([p.text for p in entry.parents]) - line.append(entry.text) - lines.append(' '.join(line)) - return lines - 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) - if self._device_os == 'junos': - return self.to_lines(section) return self.to_block(section) except ValueError: return list() @@ -439,119 +289,6 @@ def get_section_objects(self, path): raise ValueError('path does not exist in config') return self.expand_section(obj) - 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 flatten(self, data, obj=None): - if obj is None: - obj = list() - for k, v in data.items(): - obj.append(k) - self.flatten(v, obj) - return obj - - 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 get_children(self, path): - obj = self.get_object(path) - if obj: - return obj.children - - def difference(self, other, path=None, match='line', replace='line'): - updates = list() - - config = self.items - if path: - config = self.get_children(path) or list() - - if match == 'line': - for item in config: - if item not in other.items: - updates.append(item) - - elif match == 'strict': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - for index, item in enumerate(config): - try: - if item != current[index]: - updates.append(item) - except IndexError: - updates.append(item) - - elif match == 'exact': - if path: - current = other.get_children(path) or list() - else: - current = other.items - - if len(current) != len(config): - updates.extend(config) - else: - for ours, theirs in itertools.izip(config, current): - if ours != theirs: - updates.extend(config) - break - - if self._device_os == 'junos': - return updates - - diffs = collections.OrderedDict() - for update in updates: - if replace == 'block' and update.parents: - update = update.parents[-1] - self.expand(update, diffs) - - return self.flatten(diffs) - - def replace(self, replace, text=None, regex=None, parents=None, - add_if_missing=False, ignore_whitespace=False): - match = None - - parents = parents or list() - if text is None and regex is None: - raise ValueError('missing required arguments') - - if not regex: - regex = ['^%s$' % text] - - patterns = [re.compile(r, re.I) for r in to_list(regex)] - - for item in self.items: - for regexp in patterns: - if ignore_whitespace is True: - string = item.text - else: - string = item.raw - if regexp.search(item.text): - if item.text != replace: - if parents == [p.text for p in item.parents]: - match = item - break - - if match: - match.text = replace - indent = len(match.raw) - len(match.raw.lstrip()) - match.raw = replace.rjust(len(replace) + indent) - - elif add_if_missing: - self.add(replace, parents=parents) - def add(self, lines, parents=None): """Adds one or lines of configuration @@ -602,303 +339,44 @@ def add(self, lines, parents=None): self.items.append(item) -def argument_spec(): - return dict( - # config options - running_config=dict(aliases=['config']), - save_config=dict(type='bool', default=False, aliases=['save']) - ) -nxos_argument_spec = argument_spec() - - -NET_PASSWD_RE = re.compile(r"[\r\n]?password: $", re.I) - -NET_COMMON_ARGS = dict( - host=dict(required=True), - port=dict(type='int'), - username=dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])), - password=dict(no_log=True, fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD'])), - ssh_keyfile=dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), - transport=dict(default='cli', choices=['cli', 'nxapi']), - use_ssl=dict(default=False, type='bool'), - validate_certs=dict(default=True, type='bool'), - provider=dict(type='dict'), - timeout=dict(default=10, type='int') -) - -NXAPI_COMMAND_TYPES = ['cli_show', 'cli_show_ascii', 'cli_conf', 'bash'] - -NXAPI_ENCODINGS = ['json', 'xml'] - -CLI_PROMPTS_RE = [ - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*[>|#|%](?:\s*)$'), - re.compile(r'[\r\n]?[a-zA-Z]{1}[a-zA-Z0-9-]*\(.+\)#(?:\s*)$') -] - -CLI_ERRORS_RE = [ - re.compile(r"% ?Error"), - re.compile(r"^% \w+", re.M), - re.compile(r"% ?Bad secret"), - re.compile(r"invalid input", re.I), - re.compile(r"(?:incomplete|ambiguous) command", re.I), - re.compile(r"connection timed out", re.I), - re.compile(r"[^\r\n]+ not found", re.I), - re.compile(r"'[^']' +returned error code: ?\d+"), - re.compile(r"syntax error"), - re.compile(r"unknown command") -] - - -def to_list(val): - if isinstance(val, (list, tuple)): - return list(val) - elif val is not None: - return [val] - else: - return list() - - -class Nxapi(object): - - def __init__(self, module): - self.module = module - - # sets the module_utils/urls.py req parameters - self.module.params['url_username'] = module.params['username'] - self.module.params['url_password'] = module.params['password'] - - self.url = None - self._nxapi_auth = None - - def _get_body(self, commands, command_type, encoding, version='1.0', chunk='0', sid=None): - """Encodes a NXAPI JSON request message - """ - if isinstance(commands, (list, set, tuple)): - commands = ' ;'.join(commands) - - if encoding not in NXAPI_ENCODINGS: - msg = 'invalid encoding, received %s, exceped one of %s' % \ - (encoding, ','.join(NXAPI_ENCODINGS)) - self.module_fail_json(msg=msg) - - msg = { - 'version': version, - 'type': command_type, - 'chunk': chunk, - 'sid': sid, - 'input': commands, - 'output_format': encoding - } - return dict(ins_api=msg) - - def connect(self): - host = self.module.params['host'] - port = self.module.params['port'] - - if self.module.params['use_ssl']: - proto = 'https' - if not port: - port = 443 - else: - proto = 'http' - if not port: - port = 80 - - self.url = '%s://%s:%s/ins' % (proto, host, port) - - def send(self, commands, command_type='cli_show_ascii', encoding='json'): - """Send commands to the device. - """ - clist = to_list(commands) - - if command_type not in NXAPI_COMMAND_TYPES: - msg = 'invalid command_type, received %s, exceped one of %s' % \ - (command_type, ','.join(NXAPI_COMMAND_TYPES)) - self.module_fail_json(msg=msg) - - data = self._get_body(clist, command_type, encoding) - data = self.module.jsonify(data) - - headers = {'Content-Type': 'application/json'} - if self._nxapi_auth: - headers['Cookie'] = self._nxapi_auth - - response, headers = fetch_url(self.module, self.url, data=data, - headers=headers, method='POST') - - self._nxapi_auth = headers.get('set-cookie') - - if headers['status'] != 200: - self.module.fail_json(**headers) - - response = self.module.from_json(response.read()) - result = list() - - output = response['ins_api']['outputs']['output'] - for item in to_list(output): - if item['code'] != '200': - self.module.fail_json(**item) - else: - result.append(item['body']) - - return result - - -class Cli(object): - - def __init__(self, module): - self.module = module - self.shell = None - - def connect(self, **kwargs): - host = self.module.params['host'] - port = self.module.params['port'] or 22 - - username = self.module.params['username'] - password = self.module.params['password'] - timeout = self.module.params['timeout'] - key_filename = self.module.params['ssh_keyfile'] - - allow_agent = (key_filename is not None) or (key_filename is None and password is None) - - try: - self.shell = Shell(kickstart=False, prompts_re=CLI_PROMPTS_RE, - errors_re=CLI_ERRORS_RE) - self.shell.open(host, port=port, username=username, - password=password, key_filename=key_filename, - allow_agent=allow_agent, timeout=timeout) - except ShellError: - e = get_exception() - msg = 'failed to connect to %s:%s - %s' % (host, port, str(e)) - self.module.fail_json(msg=msg) - - def send(self, commands, encoding='text'): - try: - return self.shell.send(commands) - except ShellError: - e = get_exception() - self.module.fail_json(msg=e.message, commands=commands) - - -class NetworkModule(AnsibleModule): - - def __init__(self, *args, **kwargs): - super(NetworkModule, self).__init__(*args, **kwargs) - self.connection = None - self._config = None - self._connected = False - - @property - def connected(self): - return self._connected - - @property - def config(self): - if not self._config: - self._config = self.get_config() - return self._config - - def _load_params(self): - super(NetworkModule, self)._load_params() - provider = self.params.get('provider') or dict() - for key, value in provider.items(): - if key in NET_COMMON_ARGS: - if self.params.get(key) is None and value is not None: - self.params[key] = value - - def connect(self): - cls = globals().get(str(self.params['transport']).capitalize()) - try: - self.connection = cls(self) - except TypeError: - e = get_exception() - self.fail_json(msg=e.message) - - self.connection.connect() - - if self.params['transport'] == 'cli': - self.connection.send('terminal length 0') - - self._connected = True - - def configure(self, commands): - commands = to_list(commands) - if self.params['transport'] == 'cli': - return self.configure_cli(commands) - else: - return self.execute(commands, command_type='cli_conf') - - def configure_cli(self, commands): - commands = to_list(commands) - commands.insert(0, 'configure') - responses = self.execute(commands) - responses.pop(0) - return responses - - def execute(self, commands, **kwargs): - if not self.connected: - self.connect() - return self.connection.send(commands, **kwargs) - - def disconnect(self): - self.connection.close() - self._connected = False - - def parse_config(self, cfg): - return parse(cfg, indent=2) - - def get_config(self): - cmd = 'show running-config' - if self.params.get('include_defaults'): - cmd += ' all' - response = self.execute(cmd) - return response[0] - - -def get_module(**kwargs): - """Return instance of NetworkModule - """ - argument_spec = NET_COMMON_ARGS.copy() - if kwargs.get('argument_spec'): - argument_spec.update(kwargs['argument_spec']) - kwargs['argument_spec'] = argument_spec - - module = NetworkModule(**kwargs) - - if module.params['transport'] == 'cli' and not HAS_PARAMIKO: - module.fail_json(msg='paramiko is required but does not appear to be installed') - - return module - +def get_network_module(**kwargs): + try: + return get_module(**kwargs) + except NameError: + return NetworkModule(**kwargs) -def custom_get_config(module, include_defaults=False): - config = module.params['running_config'] +def get_config(module, include_defaults=False): + config = module.params['config'] if not config: - cmd = 'show running-config' - if module.params['include_defaults']: - cmd += ' all' - if module.params['transport'] == 'nxapi': - config = module.execute([cmd], command_type='cli_show_ascii')[0] - else: - config = module.execute([cmd])[0] - + 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 = custom_get_config(module) + config = get_config(module) commands = candidate.difference(config) commands = [str(c).strip() for c in commands] - save_config = module.params['save_config'] + save_config = module.params['save'] result = dict(changed=False) if commands: if not module.check_mode: - module.configure(commands) + try: + module.configure(commands) + except AttributeError: + module.config(commands) + if save_config: - module.config.save_config() + try: + module.config.save_config() + except AttributeError: + module.execute(['copy running-config startup-config']) result['changed'] = True result['updates'] = commands @@ -906,7 +384,6 @@ def load_config(module, candidate): return result # END OF COMMON CODE - 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 @@ -930,6 +407,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -937,8 +419,21 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -1218,6 +713,15 @@ def execute_config_command(commands, module): 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 main(): @@ -1241,11 +745,12 @@ def main(): restart=dict(type='bool', default=False), state=dict(choices=['present', 'absent', 'default'], default='present'), - include_defaults=dict(default=True) + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - argument_spec.update(nxos_argument_spec) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] interface = module.params['interface'] From f8c9df2ca099023a3586b7a6192ddb958617acf3 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Sun, 4 Sep 2016 21:17:52 +0200 Subject: [PATCH 158/770] Fixed module structure and added one new param --- network/nxos/nxos_vrrp.py | 306 ++++++++++++++++++++++++++++++++------ 1 file changed, 260 insertions(+), 46 deletions(-) diff --git a/network/nxos/nxos_vrrp.py b/network/nxos/nxos_vrrp.py index 6c65295336b..8476bda8d41 100644 --- a/network/nxos/nxos_vrrp.py +++ b/network/nxos/nxos_vrrp.py @@ -19,7 +19,6 @@ DOCUMENTATION = ''' --- - module: nxos_vrrp version_added: "2.1" short_description: Manages VRRP configuration on NX-OS switches @@ -28,56 +27,59 @@ extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - - Gabriele Gerbino (@GGabriele) + - Gabriele Gerbino (@GGabriele) notes: - - VRRP feature needs to be enabled first on the system. - - SVIs must exist before using this module. - - Interface must be a L3 port before using this module. - - C(state=absent) removes the vrrp group if it exists on the device. - - VRRP cannot be configured on loopback interfaces. + - VRRP feature needs to be enabled first on the system + - SVIs must exist before using this module + - Interface must be a L3 port before using this module + - I(state)=absent removes the vrrp group if it exists on the device + - VRRP cannot be configured on loopback interfaces options: group: description: - - The VRRP group number. + - VRRP group number required: true interface: description: - - Full name of interface that is being managed for VRRP. + - Full name of interface that is being managed for VRRP required: true priority: description: - - VRRP priority. + - VRRP priority required: false default: null vip: description: - - HSRP virtual IP address. + - VRRP virtual IP address required: false default: null authentication: description: - - Clear text authentication string. + - clear text authentication string required: false default: null + admin_state: + description: + - Used to enable or disable the VRRP process + required: false + choices: ['shutdown', 'no shutdown'] + default: no shutdown + version_added: "2.2" state: description: - - Specify desired state of the resource. + - Specify desired state of the resource required: false default: present choices: ['present','absent'] ''' EXAMPLES = ''' - # ensure vrrp group 100 and vip 10.1.100.1 is on vlan10 -- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 host={{ inventory_hostname }} - +- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 host=68.170.147.165 # ensure removal of the vrrp group config # vip is required to ensure the user knows what they are removing -- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 state=absent host={{ inventory_hostname }} - +- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 state=absent host=68.170.147.165 # re-config with more params -- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 preempt=false priority=130 authentication=AUTHKEY host={{ inventory_hostname }} - +- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 preempt=false priority=130 authentication=AUTHKEY host=68.170.147.165 ''' RETURN = ''' @@ -85,7 +87,8 @@ description: k/v pairs of parameters passed into module returned: always type: dict - sample: {"authentication": "testing", "group": "150", "vip": "10.1.15.1"} + sample: {"authentication": "testing", "group": "150", "vip": "10.1.15.1", + "admin_state": "no shutdown"} existing: description: k/v pairs of existing vrrp info on the interface type: dict @@ -95,18 +98,14 @@ returned: always type: dict sample: {"authentication": "testing", "group": "150", "interval": "1", - "preempt": true, "priority": "100", "vip": "10.1.15.1"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" + "preempt": true, "priority": "100", "vip": "10.1.15.1", + "admin_state": "no shutdown"} updates: description: commands sent to the device returned: always type: list sample: ["interface vlan10", "vrrp 150", "address 10.1.15.1", - "authentication text testing"] + "authentication text testing", "no shutdown"] changed: description: check to see if a change was made on the device returned: always @@ -114,6 +113,162 @@ sample: true ''' +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: @@ -122,6 +277,15 @@ def execute_config_command(commands, module): 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_vrrp(command, response, module): @@ -147,6 +311,11 @@ def get_cli_body_ssh_vrrp(command, response, module): 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) @@ -154,11 +323,25 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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, output=command_type) + 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' @@ -223,19 +406,43 @@ def get_interface_mode(interface, intf_type, module): command = 'show interface {0}'.format(interface) interface = {} mode = 'unknown' + body = execute_show_command(command, module)[0] + interface_table = body['TABLE_interface']['ROW_interface'] + name = interface_table.get('interface') if intf_type in ['ethernet', 'portchannel']: - body = execute_show_command(command, module)[0] - interface_table = body['TABLE_interface']['ROW_interface'] mode = str(interface_table.get('eth_mode', 'layer3')) + if mode == 'access' or mode == 'trunk': mode = 'layer2' elif intf_type == 'svi': mode = 'layer3' - return mode + + return mode, name -def get_existing_vrrp(interface, group, module): +def get_vrr_status(group, module, interface): + command = 'show run all | section interface.{0}$'.format(interface) + body = execute_show_command(command, module, command_type='cli_show_ascii')[0] + vrf_index = None + admin_state = 'shutdown' + + if body: + splitted_body = body.splitlines() + for index in range(0, len(splitted_body) - 1): + if splitted_body[index].strip() == 'vrrp {0}'.format(group): + vrf_index = index + vrf_section = splitted_body[vrf_index::] + + for line in vrf_section: + if line.strip() == 'no shutdown': + admin_state = 'no shutdown' + break + + return admin_state + + +def get_existing_vrrp(interface, group, module, name): command = 'show vrrp detail interface {0}'.format(interface) body = execute_show_command(command, module) vrrp = {} @@ -267,6 +474,8 @@ def get_existing_vrrp(interface, group, module): parsed_vrrp['preempt'] = True if parsed_vrrp['group'] == group: + parsed_vrrp['admin_state'] = get_vrr_status(group, module, name) + return parsed_vrrp return vrrp @@ -287,6 +496,7 @@ def get_commands_config_vrrp(delta, group): preempt = delta.get('preempt') interval = delta.get('interval') auth = delta.get('authentication') + admin_state = delta.get('admin_state') if vip: commands.append((CMDS.get('vip')).format(vip)) @@ -300,6 +510,8 @@ def get_commands_config_vrrp(delta, group): commands.append((CMDS.get('interval')).format(interval)) if auth: commands.append((CMDS.get('auth')).format(auth)) + if admin_state: + commands.append(admin_state) commands.insert(0, 'vrrp {0}'.format(group)) @@ -340,14 +552,20 @@ def main(): group=dict(required=True, type='str'), interface=dict(required=True), priority=dict(required=False, type='str'), - preempt=dict(required=False, choices=BOOLEANS, type='bool'), + preempt=dict(required=False, type='bool'), vip=dict(required=False, type='str'), + admin_state=dict(required=False, type='str', + choices=['shutdown', 'no shutdown'], + default='no shutdown'), authentication=dict(required=False, type='str'), state=dict(choices=['absent', 'present'], required=False, default='present'), + include_defaults=dict(default=False), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) state = module.params['state'] interface = module.params['interface'].lower() @@ -356,6 +574,7 @@ def main(): preempt = module.params['preempt'] vip = module.params['vip'] authentication = module.params['authentication'] + admin_state = module.params['admin_state'] transport = module.params['transport'] @@ -371,16 +590,17 @@ def main(): module.fail_json(msg="Loopback interfaces don't support VRRP.", interface=interface) - mode = get_interface_mode(interface, intf_type, module) + mode, name = get_interface_mode(interface, intf_type, module) if mode == 'layer2': module.fail_json(msg='That interface is a layer2 port.\nMake it ' 'a layer 3 port first.', interface=interface) args = dict(group=group, priority=priority, preempt=preempt, - vip=vip, authentication=authentication) + vip=vip, authentication=authentication, + admin_state=admin_state) proposed = dict((k, v) for k, v in args.iteritems() if v is not None) - existing = get_existing_vrrp(interface, group, module) + existing = get_existing_vrrp(interface, group, module, name) changed = False end_state = existing @@ -407,12 +627,11 @@ def main(): else: execute_config_command(cmds, module) changed = True - end_state = get_existing_vrrp(interface, group, module) + end_state = get_existing_vrrp(interface, group, module, name) results = {} results['proposed'] = proposed results['existing'] = existing - results['state'] = state results['updates'] = cmds results['changed'] = changed results['end_state'] = end_state @@ -420,10 +639,5 @@ def main(): module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 59c0e54df58f7c66577b0b51427fb1bd807f9eb3 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 15:49:42 +0200 Subject: [PATCH 159/770] Fix docstring --- network/nxos/nxos_vpc_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 9f0b2b8f07f..4616c27f45f 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -29,7 +29,7 @@ - Gabriele Gerbino (@GGabriele) notes: - Either vpc or peer_link param is required, but not both. - - I(state)=absent removes whatever VPC config is on a port-channel + - C(state)=absent removes whatever VPC config is on a port-channel if one exists. - Re-assigning a vpc or peerlink from one portchannel to another is not supported. The module will force the user to unconfigure an existing From 90a9e983b4d9e9a6d3875639f0309f6a8fd776df Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 16:46:24 +0200 Subject: [PATCH 160/770] Fixed param value format in docstring --- network/nxos/nxos_vpc_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 4616c27f45f..7752bd7a821 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -29,7 +29,7 @@ - Gabriele Gerbino (@GGabriele) notes: - Either vpc or peer_link param is required, but not both. - - C(state)=absent removes whatever VPC config is on a port-channel + - C(state=absent) removes whatever VPC config is on a port-channel if one exists. - Re-assigning a vpc or peerlink from one portchannel to another is not supported. The module will force the user to unconfigure an existing From dd4b6a8585a6e3e9281048560cd5059e13b825f1 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 16:55:57 +0200 Subject: [PATCH 161/770] Fixing docstring format --- network/nxos/nxos_vpc_interface.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 7752bd7a821..02130e93601 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -37,21 +37,21 @@ options: portchannel: description: - - group number of the portchannel that will be configured + - Group number of the portchannel that will be configured. required: true vpc: description: - - vpc group/id that will be configured on associated portchannel + - VPC group/id that will be configured on associated portchannel. required: false default: null peer_link: description: - - Set to true/false for peer link config on assoicated portchannel + - Set to true/false for peer link config on assoicated portchannel. required: false default: null state: description: - - Manages desired state of the resource + - Manages desired state of the resource. required: true choices: ['present','absent'] ''' From 9357ef31fb6b9389ac60826b83bb68907446beda Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:10:12 +0200 Subject: [PATCH 162/770] Fixed docstring --- network/nxos/nxos_static_route.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index d62066b4ea0..0fb5878dc58 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -25,13 +25,13 @@ author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - If no vrf is supplied, vrf is set to default - - If I(state)=absent, the route will be removed, regardless of the + - If no vrf is supplied, vrf is set to default. + - If C(state=absent), the route will be removed, regardless of the non-required parameters. options: prefix: description: - - Destination prefix of static route + - Destination prefix of static route. required: true next_hop: description: @@ -40,7 +40,7 @@ required: true vrf: description: - - VRF for static route + - VRF for static route. required: false default: default tag: @@ -55,12 +55,12 @@ default: null pref: description: - - Preference or administrative difference of route (range 1-255) + - Preference or administrative difference of route (range 1-255). required: false default: null state: description: - - Manage the state of the resource + - Manage the state of the resource. required: true choices: ['present','absent'] ''' From 9723c7f9fb290983bfe65bd0f70669b5eba7b8c5 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:17:28 +0200 Subject: [PATCH 163/770] Fixed docstring --- network/nxos/nxos_acl.py | 75 ++++++++++++++++++++-------------------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index 65c80345ee7..9e9db7317ee 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -28,159 +28,160 @@ - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - I(state)=absent removes the ACE if it exists - - I(state)=delete_acl deleted the ACL if it exists - - for idempotency, use port numbers for the src/dest port + - C(state=absent) removes the ACE if it exists. + - C(state=delete_acl) deleted the ACL if it exists. + - For idempotency, use port numbers for the src/dest port params like I(src_port1) and names for the well defined protocols for the I(proto) param. - - while this module is idempotent in that if the ace as presented in the - task is identical to the one on the switch, no changes will be made. If - there is any difference, what is in Ansible will be pushed (configured + - Although this module is idempotent in that if the ace as presented in + the task is identical to the one on the switch, no changes will be made. + If there is any difference, what is in Ansible will be pushed (configured options will be overridden). This is to improve security, but at the same time remember an ACE is removed, then re-added, so if there is a - change, the new ACE will be exacty what params you are sending to the - module. + change, the new ACE will be exactly what paramaters you are sending to + the module. options: seq: description: - - sequence number of the entry (ACE) + - Sequence number of the entry (ACE). required: false default: null name: description: - - Case sensitive name of the access list (ACL) + - Case sensitive name of the access list (ACL). required: true action: description: - - action of the ACE + - Action of the ACE. required: false default: null choices: ['permit', 'deny', 'remark'] remark: description: - - If action is set to remark, this is the description + - If action is set to remark, this is the description. required: false default: null proto: description: - - port number or protocol (as supported by the switch) + - Port number or protocol (as supported by the switch). required: false default: null src: description: - - src ip and mask using IP/MASK notation and supports keyword 'any' + - Source ip and mask using IP/MASK notation and + supports keyword 'any'. required: false default: null src_port_op: description: - - src port operands such as eq, neq, gt, lt, range + - Source port operands such as eq, neq, gt, lt, range. required: false default: null choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range'] src_port1: description: - - port/protocol and also first (lower) port when using range - operand + - Port/protocol and also first (lower) port when using range + operand. required: false default: null src_port2: description: - - second (end) port when using range operand + - Second (end) port when using range operand. required: false default: null dest: description: - - dest ip and mask using IP/MASK notation and supports the - keyword 'any' + - Destination ip and mask using IP/MASK notation and supports the + keyword 'any'. required: false default: null default: null dest_port_op: description: - - dest port operands such as eq, neq, gt, lt, range + - Destination port operands such as eq, neq, gt, lt, range. required: false default: null choices: ['any', 'eq', 'gt', 'lt', 'neq', 'range'] dest_port1: description: - - port/protocol and also first (lower) port when using range - operand + - Port/protocol and also first (lower) port when using range + operand. required: false default: null dest_port2: description: - - second (end) port when using range operand + - Second (end) port when using range operand. required: false default: null log: description: - - Log matches against this entry + - Log matches against this entry. required: false default: null choices: ['enable'] urg: description: - - Match on the URG bit + - Match on the URG bit. required: false default: null choices: ['enable'] ack: description: - - Match on the ACK bit + - Match on the ACK bit. required: false default: null choices: ['enable'] psh: description: - - Match on the PSH bit + - Match on the PSH bit. required: false default: null choices: ['enable'] rst: description: - - Match on the RST bit + - Match on the RST bit. required: false default: null choices: ['enable'] syn: description: - - Match on the SYN bit + - Match on the SYN bit. required: false default: null choices: ['enable'] fin: description: - - Match on the FIN bit + - Match on the FIN bit. required: false default: null choices: ['enable'] established: description: - - Match established connections + - Match established connections. required: false default: null choices: ['enable'] fragments: description: - - Check non-initial fragments + - Check non-initial fragments. required: false default: null choices: ['enable'] time-range: description: - - Name of time-range to apply + - Name of time-range to apply. required: false default: null precedence: description: - - Match packets with given precedence + - Match packets with given precedence. required: false default: null choices: ['critical', 'flash', 'flash-override', 'immediate', 'internet', 'network', 'priority', 'routine'] dscp: description: - - Match packets with given dscp value + - Match packets with given dscp value. required: false default: null choices: ['af11', 'af12', 'af13', 'af21', 'af22', 'af23','af31','af32', @@ -188,7 +189,7 @@ 'cs5', 'cs6', 'cs7', 'default', 'ef'] state: description: - - Specify desired state of the resource + - Specify desired state of the resource. required: false default: present choices: ['present','absent','delete_acl'] From 964bffd1d7127bc4b20818527e4af3898dbdb724 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:19:48 +0200 Subject: [PATCH 164/770] Adding missing fullstop --- network/nxos/nxos_evpn_global.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index 384b0be6634..16cec8f30e0 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -28,7 +28,7 @@ options: nv_overlay_evpn: description: - - EVPN control plane + - EVPN control plane. required: true choices: ['true', 'false'] ''' From 3fa0ec974817a10e9e7b5d699326ebbf2cfb655b Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:23:48 +0200 Subject: [PATCH 165/770] Fixed docstring --- network/nxos/nxos_acl_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 367472b06fd..1989a1bfd77 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -34,7 +34,7 @@ required: true interface: description: - - Full name of interface. MUST be the full name. + - Full name of interface, e.g. I(Ethernet1/1) required: true direction: description: From 401514ac7c042c2465c1d34dc451b21845950f1b Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:27:56 +0200 Subject: [PATCH 166/770] Fixed docstring --- network/nxos/nxos_vrrp.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/network/nxos/nxos_vrrp.py b/network/nxos/nxos_vrrp.py index 8476bda8d41..5db65334160 100644 --- a/network/nxos/nxos_vrrp.py +++ b/network/nxos/nxos_vrrp.py @@ -21,53 +21,53 @@ --- module: nxos_vrrp version_added: "2.1" -short_description: Manages VRRP configuration on NX-OS switches +short_description: Manages VRRP configuration on NX-OS switches. description: - - Manages VRRP configuration on NX-OS switches + - Manages VRRP configuration on NX-OS switches. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - VRRP feature needs to be enabled first on the system - - SVIs must exist before using this module - - Interface must be a L3 port before using this module - - I(state)=absent removes the vrrp group if it exists on the device - - VRRP cannot be configured on loopback interfaces + - VRRP feature needs to be enabled first on the system. + - SVIs must exist before using this module. + - Interface must be a L3 port before using this module. + - C(state=absent) removes the VRRP group if it exists on the device. + - VRRP cannot be configured on loopback interfaces. options: group: description: - - VRRP group number + - VRRP group number. required: true interface: description: - - Full name of interface that is being managed for VRRP + - Full name of interface that is being managed for VRRP. required: true priority: description: - - VRRP priority + - VRRP priority. required: false default: null vip: description: - - VRRP virtual IP address + - VRRP virtual IP address. required: false default: null authentication: description: - - clear text authentication string + - Clear text authentication string. required: false default: null admin_state: description: - - Used to enable or disable the VRRP process + - Used to enable or disable the VRRP process. required: false choices: ['shutdown', 'no shutdown'] default: no shutdown version_added: "2.2" state: description: - - Specify desired state of the resource + - Specify desired state of the resource. required: false default: present choices: ['present','absent'] From cabc937b30f0c774424c9b86d11d3997b499c0e4 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:30:34 +0200 Subject: [PATCH 167/770] Fixed docstring --- network/nxos/nxos_igmp.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/network/nxos/nxos_igmp.py b/network/nxos/nxos_igmp.py index 54370dc10b6..05827a34da0 100644 --- a/network/nxos/nxos_igmp.py +++ b/network/nxos/nxos_igmp.py @@ -20,17 +20,18 @@ --- module: nxos_igmp version_added: "2.2" -short_description: Manages IGMP global configuration +short_description: Manages IGMP global configuration. description: - - Manages IGMP global configuration configuration settings + - Manages IGMP global configuration configuration settings. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - When state=default, all supported params will be reset to a default state + - When C(state=default), all supported params will be reset to a + default state. - If restart is set to true with other params set, the restart will happen - last, i.e. after the configuration takes place + last, i.e. after the configuration takes place. options: flush_routes: description: @@ -42,19 +43,19 @@ enforce_rtr_alert: description: - Enables or disables the enforce router alert option check for - IGMPv2 and IGMPv3 packets + IGMPv2 and IGMPv3 packets. required: false default: null choices: ['true', 'false'] restart: description: - - restarts the igmp process (using an exec config command) + - Restarts the igmp process (using an exec config command). required: false default: null choices: ['true', 'false'] state: description: - - Manages desired state of the resource + - Manages desired state of the resource. required: false default: present choices: ['present', 'default'] From f12fb67ca4324c2195e46ae1cab8f77ab4e63550 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:31:58 +0200 Subject: [PATCH 168/770] Fixed docstring --- network/nxos/nxos_vrf_interface.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index 1416dc1b6c4..893763ccc29 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -20,30 +20,32 @@ --- module: nxos_vrf_interface version_added: "2.1" -short_description: Manages interface specific VRF configuration +short_description: Manages interface specific VRF configuration. description: - - Manages interface specific VRF configuration + - Manages interface specific VRF configuration. extends_documentation_fragment: nxos -author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) notes: - VRF needs to be added globally with M(nxos_vrf) before - adding a VRF to an interface + adding a VRF to an interface. - Remove a VRF from an interface will still remove - all L3 attributes just as it does from CLI + all L3 attributes just as it does from CLI. - VRF is not read from an interface until IP address is - configured on that interface + configured on that interface. options: vrf: description: - - Name of VRF to be managed + - Name of VRF to be managed. required: true interface: description: - - Full name of interface to be managed, i.e. Ethernet1/1 + - Full name of interface to be managed, i.e. Ethernet1/1. required: true state: description: - - Manages desired state of the resource + - Manages desired state of the resource. required: false default: present choices: ['present','absent'] From ab81283f34a7255afd2655564cd7353c2e748fea Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:33:38 +0200 Subject: [PATCH 169/770] Fixed docstring --- network/nxos/nxos_vrf.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index 08de47ff6f3..f46e31eb74c 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -20,9 +20,9 @@ --- module: nxos_vrf version_added: "2.1" -short_description: Manages global VRF configuration +short_description: Manages global VRF configuration. description: - - Manages global VRF configuration + - Manages global VRF configuration. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) @@ -30,20 +30,20 @@ notes: - Cisco NX-OS creates the default VRF by itself. Therefore, you're not allowed to use default as I(vrf) name in this module. - - I(vrf) name must be shorter than 32 chars. + - C(vrf) name must be shorter than 32 chars. - VRF names are not case sensible in NX-OS. Anyway, the name is stored just like it's inserted by the user and it'll not be changed again - unless the VRF is removed and re-created. i.e. I(vrf=NTC) will create - a VRF named NTC, but running it again with I(vrf=ntc) will not cause + unless the VRF is removed and re-created. i.e. C(vrf=NTC) will create + a VRF named NTC, but running it again with C(vrf=ntc) will not cause a configuration change. options: vrf: description: - - Name of VRF to be managed + - Name of VRF to be managed. required: true admin_state: description: - - Administrative state of the VRF + - Administrative state of the VRF. required: false default: up choices: ['up','down'] @@ -64,13 +64,13 @@ version_added: "2.2" state: description: - - Manages desired state of the resource + - Manages desired state of the resource. required: false default: present choices: ['present','absent'] description: description: - - Description of the VRF + - Description of the VRF. required: false default: null ''' From 22ce05a489b43e412129a99de4ee651a203e2da2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:35:46 +0200 Subject: [PATCH 170/770] Fixed docstring --- network/nxos/nxos_vlan.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index dae7baa796b..6d1167d79dc 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -20,51 +20,51 @@ --- module: nxos_vlan version_added: "2.1" -short_description: Manages VLAN resources and attributes +short_description: Manages VLAN resources and attributes. description: - - Manages VLAN configurations on NX-OS switches + - Manages VLAN configurations on NX-OS switches. author: Jason Edelman (@jedelman8) extends_documentation_fragment: nxos options: vlan_id: description: - - single vlan id + - Single VLAN ID. required: false default: null vlan_range: description: - - range of VLANs such as 2-10 or 2,5,10-15, etc. + - Range of VLANs such as 2-10 or 2,5,10-15, etc. required: false default: null name: description: - - name of VLAN + - Name of VLAN. required: false default: null vlan_state: description: - Manage the vlan operational state of the VLAN - (equivalent to state {active | suspend} command + (equivalent to state {active | suspend} command. required: false default: active choices: ['active','suspend'] admin_state: description: - - Manage the vlan admin state of the VLAN equivalent - to shut/no shut in vlan config mode + - Manage the VLAN administrative state of the VLAN equivalent + to shut/no shut in VLAN config mode. required: false default: up choices: ['up','down'] mapped_vni: description: - - The Virtual Network Identifier (VNI) id that is mapped to the + - The Virtual Network Identifier (VNI) ID that is mapped to the VLAN. Valid values are integer and keyword 'default'. required: false default: null version_added: "2.2" state: description: - - Manage the state of the resource + - Manage the state of the resource. required: false default: present choices: ['present','absent'] From 192daac855ce440f6110c05105407f4d39658e0d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:36:48 +0200 Subject: [PATCH 171/770] Fixed docstring --- network/nxos/nxos_ping.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/network/nxos/nxos_ping.py b/network/nxos/nxos_ping.py index 72e87018d97..ea4ecb6826c 100644 --- a/network/nxos/nxos_ping.py +++ b/network/nxos/nxos_ping.py @@ -20,29 +20,31 @@ --- module: nxos_ping version_added: "2.1" -short_description: Tests reachability using ping from Nexus switch +short_description: Tests reachability using ping from Nexus switch. description: - - Tests reachability using ping from switch to a remote destination + - Tests reachability using ping from switch to a remote destination. extends_documentation_fragment: nxos -author: Jason Edelman (@jedelman8), Gabriele Gerbino (@GGabriele) +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) options: dest: description: - - IP address or hostname (resolvable by switch) of remote node + - IP address or hostname (resolvable by switch) of remote node. required: true count: description: - - Number of packets to send + - Number of packets to send. required: false default: 2 source: description: - - Source IP Address + - Source IP Address. required: false default: null vrf: description: - - Outgoing VRF + - Outgoing VRF. required: false default: null ''' From 30313f89799a2488768032603bc7109354ddd2a2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:37:50 +0200 Subject: [PATCH 172/770] Fixed docstring --- network/nxos/nxos_ip_interface.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index 09e95a06cd9..1d989c3e8e4 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -20,38 +20,38 @@ --- module: nxos_ip_interface version_added: "2.1" -short_description: Manages L3 attributes for IPv4 and IPv6 interfaces +short_description: Manages L3 attributes for IPv4 and IPv6 interfaces. description: - - Manages Layer 3 attributes for IPv4 and IPv6 interfaces + - Manages Layer 3 attributes for IPv4 and IPv6 interfaces. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - Interface must already be a L3 port when using this module - - Logical interfaces (po, loop, svi) must be created first - - I(mask) must be inserted in decimal format (i.e. 24) for + - Interface must already be a L3 port when using this module. + - Logical interfaces (po, loop, svi) must be created first. + - C(mask) must be inserted in decimal format (i.e. 24) for both IPv6 and IPv4. - A single interface can have multiple IPv6 configured. options: interface: description: - - Full name of interface, i.e. Ethernet1/1, vlan10 + - Full name of interface, i.e. Ethernet1/1, vlan10. required: true addr: description: - - IPv4 or IPv6 Address + - IPv4 or IPv6 Address. required: false default: null mask: description: - - Subnet mask for IPv4 or IPv6 Address in decimal format + - Subnet mask for IPv4 or IPv6 Address in decimal format. required: false default: null state: description: - - Specify desired state of the resource + - Specify desired state of the resource. required: false default: present choices: ['present','absent'] From b77436823bf5aa2525f48680c2fdb53951d0a6b3 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:40:26 +0200 Subject: [PATCH 173/770] Fixed docstring --- network/nxos/nxos_reboot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_reboot.py b/network/nxos/nxos_reboot.py index 1797163f3ec..aefab439e91 100644 --- a/network/nxos/nxos_reboot.py +++ b/network/nxos/nxos_reboot.py @@ -22,7 +22,7 @@ version_added: 2.2 short_description: Reboot a network device. description: - - Reboot a network device + - Reboot a network device. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) From eb2de71b65a846d184a113f0cddcb81997272970 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:41:05 +0200 Subject: [PATCH 174/770] Fixed docstring --- network/nxos/nxos_feature.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index bbee766e913..c1a243c9901 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -20,9 +20,9 @@ --- module: nxos_feature version_added: "2.1" -short_description: Manage features in NX-OS switches +short_description: Manage features in NX-OS switches. description: - - Offers ability to enable and disable features in NX-OS + - Offers ability to enable and disable features in NX-OS. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) From f5d5de56011eba73a3d35099e4f19905382f2514 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:41:53 +0200 Subject: [PATCH 175/770] Fixed docstring --- network/nxos/nxos_rollback.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_rollback.py b/network/nxos/nxos_rollback.py index 360bbb047d8..a6744dc1a77 100644 --- a/network/nxos/nxos_rollback.py +++ b/network/nxos/nxos_rollback.py @@ -20,17 +20,17 @@ --- module: nxos_rollback version_added: "2.2" -short_description: Set a checkpoint or rollback to a checkpoint +short_description: Set a checkpoint or rollback to a checkpoint. description: - This module offers the ability to set a configuration checkpoint file or rollback to a configuration checkpoint file on Cisco NXOS - switches + switches. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - Sometimes C(transport)=nxapi may cause a timeout error. + - Sometimes C(transport=nxapi) may cause a timeout error. options: checkpoint_file: description: From 393f07da885aa7c709cf540ce02d70b5d5b86b21 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:43:28 +0200 Subject: [PATCH 176/770] Fixed docstring --- network/nxos/nxos_vxlan_vtep_vni.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py index e3b552cacbe..041f10f8b72 100644 --- a/network/nxos/nxos_vxlan_vtep_vni.py +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -27,11 +27,11 @@ author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - default, where supported, restores params default value + - default, where supported, restores params default value. options: interface: description: - - Interface name for the VXLAN Network Virtualization Endpoint + - Interface name for the VXLAN Network Virtualization Endpoint. required: true vni: description: @@ -73,7 +73,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From 0dbe20beae315609cac452162fbfdb86933c0b81 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:45:35 +0200 Subject: [PATCH 177/770] Fixed docstring --- network/nxos/nxos_vxlan_vtep.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/network/nxos/nxos_vxlan_vtep.py b/network/nxos/nxos_vxlan_vtep.py index 6581370bf93..be03739bf52 100644 --- a/network/nxos/nxos_vxlan_vtep.py +++ b/network/nxos/nxos_vxlan_vtep.py @@ -20,7 +20,7 @@ --- module: nxos_vxlan_vtep version_added: "2.2" -short_description: Manages VXLAN Network Virtualization Endpoint (NVE) +short_description: Manages VXLAN Network Virtualization Endpoint (NVE). description: - Manages VXLAN Network Virtualization Endpoint (NVE) overlay interface that terminates VXLAN tunnels. @@ -28,13 +28,13 @@ extends_documentation_fragment: nxos notes: - The module is used to manage NVE properties, not to create NVE - interfaces. Use nxos_interface if you wish to do so. - - State 'absent' removes the interface - - default, where supported, restores params default value + interfaces. Use M(nxos_interface) if you wish to do so. + - C(state=absent) removes the interface. + - Default, where supported, restores params default value. options: interface: description: - - Interface name for the VXLAN Network Virtualization Endpoint + - Interface name for the VXLAN Network Virtualization Endpoint. required: true description: description: @@ -67,7 +67,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From 50e7b378a680c674b8a8b6074f19db2c338f42b4 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:47:13 +0200 Subject: [PATCH 178/770] Fixed docstring --- network/nxos/nxos_vrf_af.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_vrf_af.py b/network/nxos/nxos_vrf_af.py index 41f36cdc27f..67b4ef04046 100644 --- a/network/nxos/nxos_vrf_af.py +++ b/network/nxos/nxos_vrf_af.py @@ -20,13 +20,13 @@ --- module: nxos_vrf_af version_added: "2.2" -short_description: Manages VRF AF +short_description: Manages VRF AF. description: - Manages VRF AF author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - default, where supported, restores params default value + - Default, where supported, restores params default value. options: vrf: description: @@ -53,7 +53,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or + not on the device. required: false default: present choices: ['present','absent'] From 64ad771f11ebc5828cec1959aa7f9207865cef02 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:48:46 +0200 Subject: [PATCH 179/770] Fixed docstring --- network/nxos/nxos_smu.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index 213f525abb6..9629b4040c5 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -28,11 +28,11 @@ notes: - The module can only activate and commit a package, not remove or deactivate it. - - Use I(transport)=nxapi to avoid connection timeout + - Use C(transport=nxapi) to avoid connection timeout options: pkg: description: - - Name of the remote package + - Name of the remote package. required: true file_system: description: From 4a84fd2ac6b5824dc292368fcd708f056caf5260 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:50:01 +0200 Subject: [PATCH 180/770] Fixed docstring --- network/nxos/nxos_portchannel.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index 856f79b1a3c..49328ea4c84 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -21,38 +21,38 @@ module: nxos_portchannel version_added: "2.2" -short_description: Manages port-channel interfaces +short_description: Manages port-channel interfaces. description: - - Manages port-channel specific configuration parameters + - Manages port-channel specific configuration parameters. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - Absent removes the portchannel config and interface if it + - C(state=absent) removes the portchannel config and interface if it already exists. If members to be removed are not explicitly passed, all existing members (if any), are removed. - - Members must be a list - - LACP needs to be enabled first if active/passive modes are used + - Members must be a list. + - LACP needs to be enabled first if active/passive modes are used. options: group: description: - - channel-group number for the port-channel + - Channel-group number for the port-channel. required: true mode: description: - - Mode for the port-channel, i.e. on, active, passive + - Mode for the port-channel, i.e. on, active, passive. required: false default: on choices: ['active','passive','on'] min_links: description: - - min links required to keep portchannel up + - Min links required to keep portchannel up. required: false default: null members: description: - - List of interfaces that will be managed in a given portchannel + - List of interfaces that will be managed in a given portchannel. required: false default: null force: @@ -65,7 +65,7 @@ default: false state: description: - - Manage the state of the resource + - Manage the state of the resource. required: false default: present choices: ['present','absent'] From 7e645e16a7b50fa5cd555a80963a0e1487903adb Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:51:21 +0200 Subject: [PATCH 181/770] Fixed docstring --- network/nxos/nxos_pim_rp_address.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_pim_rp_address.py b/network/nxos/nxos_pim_rp_address.py index e85bcc1eaf0..bdb3c46fdb7 100644 --- a/network/nxos/nxos_pim_rp_address.py +++ b/network/nxos/nxos_pim_rp_address.py @@ -20,15 +20,14 @@ --- module: nxos_pim_rp_address version_added: "2.2" -short_description: Manages configuration of an Protocol Independent Multicast - (PIM) static rendezvous point (RP) address instance. +short_description: Manages configuration of an PIM static RP address instance. description: - Manages configuration of an Protocol Independent Multicast (PIM) static rendezvous point (RP) address instance. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - state=absent remove the whole rp-address configuration, if existing. + - C(state=absent) remove the whole rp-address configuration, if existing. options: rp_address: description: From 02ffc6a6f1f72ed1153e55f0f34fa2731b4d6170 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:52:15 +0200 Subject: [PATCH 182/770] Fixed docstring --- network/nxos/nxos_pim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_pim.py b/network/nxos/nxos_pim.py index 6a417a8c2dc..b97785184b2 100644 --- a/network/nxos/nxos_pim.py +++ b/network/nxos/nxos_pim.py @@ -20,9 +20,9 @@ --- module: nxos_pim version_added: "2.2" -short_description: Manages configuration of an Protocol Independent Multicast(PIM) instance. +short_description: Manages configuration of a PIM instance. description: - - Manages configuration of an Protocol Independent Multicast (PIM) instance. + - Manages configuration of a Protocol Independent Multicast (PIM) instance. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos options: From 9684ec26af4dac681a8d5f63bfa4fff5dbe15090 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:53:11 +0200 Subject: [PATCH 183/770] Fixed docstring --- network/nxos/nxos_overlay_global.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_overlay_global.py b/network/nxos/nxos_overlay_global.py index 8736f373243..e84541fd634 100644 --- a/network/nxos/nxos_overlay_global.py +++ b/network/nxos/nxos_overlay_global.py @@ -26,7 +26,7 @@ author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - default, where supported, restores params default value + - Default restores params default value - Supported MAC address format are "E.E.E", "EE-EE-EE-EE-EE-EE", "EE:EE:EE:EE:EE:EE" and "EEEE.EEEE.EEEE" options: From 1749b87fc955ee4e3b603f549892350346c58b22 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:56:58 +0200 Subject: [PATCH 184/770] Fixed docstring --- network/nxos/nxos_hsrp.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index 1fa9c77873e..c18e9c7934f 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -21,60 +21,60 @@ --- module: nxos_hsrp version_added: "2.2" -short_description: Manages HSRP configuration on NX-OS switches +short_description: Manages HSRP configuration on NX-OS switches. description: - - Manages HSRP configuration on NX-OS switches + - Manages HSRP configuration on NX-OS switches. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - HSRP feature needs to be enabled first on the system - - SVIs must exist before using this module - - Interface must be a L3 port before using this module - - HSRP cannot be configured on loopback interfaces + - HSRP feature needs to be enabled first on the system. + - SVIs must exist before using this module. + - Interface must be a L3 port before using this module. + - HSRP cannot be configured on loopback interfaces. - MD5 authentication is only possible with HSRPv2 while it is ignored if HSRPv1 is used instead, while it will not raise any error. Here we allow MD5 authentication only with HSRPv2 in order to enforce better practice. options: group: description: - - HSRP group number + - HSRP group number. required: true interface: description: - - Full name of interface that is being managed for HSRP + - Full name of interface that is being managed for HSRP. required: true version: description: - - HSRP version + - HSRP version. required: false default: 2 choices: ['1','2'] priority: description: - - HSRP priority + - HSRP priority. required: false default: null vip: description: - - HSRP virtual IP address + - HSRP virtual IP address. required: false default: null auth_string: description: - - Authentication string + - Authentication string. required: false default: null auth_type: description: - - Authentication type + - Authentication type. required: false default: null choices: ['text','md5'] state: description: - - Specify desired state of the resource + - Specify desired state of the resource. required: false choices: ['present','absent'] default: 'present' From 9d1ffd3ccc835927c3388f994fa749b3320b96f1 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 17:58:27 +0200 Subject: [PATCH 185/770] Fixed docstring --- network/nxos/nxos_file_copy.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index 0a27c5bae93..a5111a23e6b 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -22,14 +22,16 @@ version_added: "2.2" short_description: Copy a file to a remote NXOS device over SCP. description: - - Copy a file to the flash (or bootflash) remote network device on NXOS devices + - Copy a file to the flash (or bootflash) remote network device + on NXOS devices. author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - The feature must be enabled with feature scp-server. - - If the file is already present (md5 sums match), no transfer will take place. + - If the file is already present (md5 sums match), no transfer will + take place. - Check mode will tell you if the file would be copied. options: local_file: @@ -45,7 +47,8 @@ file_system: description: - The remote file system of the device. If omitted, - devices that support a file_system parameter will use their default values. + devices that support a file_system parameter will use + their default values. required: false default: null ''' From 3d8ae4ab6a354092f440292cff9cc6f548803126 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:00:27 +0200 Subject: [PATCH 186/770] Fixed docstring --- network/nxos/nxos_evpn_vni.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py index 261a839fef8..4ffcfeefcdb 100644 --- a/network/nxos/nxos_evpn_vni.py +++ b/network/nxos/nxos_evpn_vni.py @@ -20,24 +20,24 @@ --- module: nxos_evpn_vni version_added: "2.2" -short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI) +short_description: Manages Cisco EVPN VXLAN Network Identifier (VNI). description: - Manages Cisco Ethernet Virtual Private Network (EVPN) VXLAN Network Identifier (VNI) configurations of a Nexus device. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - default, where supported, restores params default value + - default, where supported, restores params default value. - RD override is not permitted. You should set it to the defalt values first and then reconfigure it. - - route_target_both, route_target_import and route_target_export valid - values are a list of extended communities, (i.e. ['1.2.3.4:5', '33:55']) - or the keywords 'auto' or 'default'. - - The route_target_both property is discouraged due to the inconsistent + - C(route_target_both), C(route_target_import) and + C(route_target_export valid) values are a list of extended communities, + (i.e. ['1.2.3.4:5', '33:55']) or the keywords 'auto' or 'default'. + - The C(route_target_both) property is discouraged due to the inconsistent behavior of the property across Nexus platforms and image versions. - For this reason it is recommended to use explicit 'route_target_export' - and 'route_target_import' properties instead of route_target_both. - - RD valid values are a String in one of the route-distinguisher formats, + For this reason it is recommended to use explicit C(route_target_export) + and C(route_target_import) properties instead of C(route_target_both). + - RD valid values are a string in one of the route-distinguisher formats, the keyword 'auto', or the keyword 'default'. options: vni: @@ -70,7 +70,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From 398e825c1b6fa64457724b8e445de45f664eb8aa Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:02:05 +0200 Subject: [PATCH 187/770] Fixed docstring --- network/nxos/nxos_bgp_neighbor.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/network/nxos/nxos_bgp_neighbor.py b/network/nxos/nxos_bgp_neighbor.py index fdf492c8b5c..56a6c2923ff 100644 --- a/network/nxos/nxos_bgp_neighbor.py +++ b/network/nxos/nxos_bgp_neighbor.py @@ -20,18 +20,18 @@ --- module: nxos_bgp_neighbor version_added: "2.2" -short_description: Manages BGP neighbors configurations +short_description: Manages BGP neighbors configurations. description: - - Manages BGP neighbors configurations on NX-OS switches + - Manages BGP neighbors configurations on NX-OS switches. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - State 'absent' removes the whole BGP neighbor configuration - - default, where supported, restores params default value + - C(state=absent) removes the whole BGP neighbor configuration. + - Default, where supported, restores params default value. options: asn: description: - - BGP autonomous system number. Valid values are String, + - BGP autonomous system number. Valid values are string, Integer in ASPLAIN or ASDOT notation. required: true vrf: @@ -86,7 +86,7 @@ log_neighbor_changes: description: - Specify whether or not to enable log messages for neighbor - up/down event. + up/down event. required: false choices: ['enable', 'disable', 'inherit'] default: null @@ -176,7 +176,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From 23de38d9d6223de44b99c6922b254bcc7eda6333 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:17:24 +0200 Subject: [PATCH 188/770] Fixed docstring --- network/nxos/nxos_bgp_neighbor_af.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py index 5dfd6fc2ac8..b6406674f3c 100644 --- a/network/nxos/nxos_bgp_neighbor_af.py +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -20,17 +20,17 @@ --- module: nxos_bgp_neighbor_af version_added: "2.2" -short_description: Manages BGP address-family's neighbors configuration +short_description: Manages BGP address-family's neighbors configuration. description: - - Manages BGP address-family's neighbors configurations on NX-OS switches + - Manages BGP address-family's neighbors configurations on NX-OS switches. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - State 'absent' removes the whole BGP address-family's - neighbor configuration - - default, when supported, removes properties - - to defaults maximum-prefix configuration, only - max_prefix_limit=default is needed + - C(state=absent) removes the whole BGP address-family's + neighbor configuration. + - Default, when supported, removes properties + - In order to default maximum-prefix configuration, only + C(max_prefix_limit=default) is needed. options: asn: description: @@ -61,7 +61,7 @@ additional_paths_receive: description: - Valid values are enable for basic command enablement; disable - for disabling the command at the neighbor_af level + for disabling the command at the neighbor af level (it adds the disable keyword to the basic command); and inherit to remove the command at this level (the command value is inherited from a higher BGP layer). @@ -71,7 +71,7 @@ additional_paths_send: description: - Valid values are enable for basic command enablement; disable - for disabling the command at the neighbor_af level + for disabling the command at the neighbor af level (it adds the disable keyword to the basic command); and inherit to remove the command at this level (the command value is inherited from a higher BGP layer). @@ -125,7 +125,7 @@ default_originate_route_map: description: - Optional route-map for the default_originate property. Can be - used independently or in conjunction with default_originate. + used independently or in conjunction with C(default_originate). Valid values are a string defining a route-map name, or 'default'. required: false @@ -244,12 +244,13 @@ default: null weight: description: - - weight value. Valid values are an integer value or 'default'. + - Weight value. Valid values are an integer value or 'default'. required: false default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From fae1d3019fe5cd72b02f850c7b1210390562f09b Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:19:22 +0200 Subject: [PATCH 189/770] Fixed docstring --- network/nxos/nxos_bgp_af.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 1ff9182b5d7..cb11733dfd4 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -20,14 +20,14 @@ --- module: nxos_bgp_af version_added: "2.2" -short_description: Manages BGP Address-family configuration +short_description: Manages BGP Address-family configuration. description: - - Manages BGP Address-family configurations on NX-OS switches + - Manages BGP Address-family configurations on NX-OS switches. author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - State 'absent' removes the whole BGP ASN configuration - - default, where supported, restores params default value + - C(state=absent) removes the whole BGP ASN configuration + - Default, where supported, restores params default value. options: asn: description: @@ -41,7 +41,7 @@ required: true afi: description: - - Address Family Identifie. + - Address Family Identifier. required: true choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn'] safi: @@ -52,7 +52,7 @@ additional_paths_install: description: - Install a backup path into the forwarding table and provide - prefix 'independent convergence (PIC) in case of a PE-CE link + prefix independent convergence (PIC) in case of a PE-CE link failure. required: false choices: ['true','false'] @@ -171,7 +171,7 @@ keyword which indicates that attributes should be copied from the aggregate. For example [['lax_inject_map', 'lax_exist_map'], ['nyc_inject_map', 'nyc_exist_map', 'copy-attributes'], - ['fsd_inject_map', 'fsd_exist_map']] + ['fsd_inject_map', 'fsd_exist_map']]. required: false default: null maximum_paths: @@ -192,7 +192,7 @@ Each entry in the array must include a prefix address and an optional route-map. For example [['10.0.0.0/16', 'routemap_LA'], ['192.168.1.1', 'Chicago'], ['192.168.2.0/24], - ['192.168.3.0/24', 'routemap_NYC']] + ['192.168.3.0/24', 'routemap_NYC']]. required: false default: null next_hop_route_map: @@ -209,7 +209,7 @@ redistribute from; the second entry defines a route-map name. A route-map is highly advised but may be optional on some platforms, in which case it may be omitted from the array list. - For example [['direct', 'rm_direct'], ['lisp', 'rm_lisp']] + For example [['direct', 'rm_direct'], ['lisp', 'rm_lisp']]. required: false default: null suppress_inactive: @@ -233,7 +233,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From 3496a4873a5099080a29155f2b2848686835defc Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:23:01 +0200 Subject: [PATCH 190/770] Fixed docstring --- network/nxos/nxos_interface_ospf.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py index 3966580e74c..d0898d1600b 100644 --- a/network/nxos/nxos_interface_ospf.py +++ b/network/nxos/nxos_interface_ospf.py @@ -26,11 +26,11 @@ author: Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - default, where supported, restores params default value + - Default, where supported, restores params default value. - To remove an existing authentication configuration you should use - message_digest_key_id=default plus all other options matching their + C(message_digest_key_id=default) plus all other options matching their existing values. - - State absent remove the whole OSPF interface configuration + - C(state=absent) removes the whole OSPF interface configuration. options: interface: description: @@ -80,7 +80,7 @@ default: null message_digest_key_id: description: - - md5 authentication key-id associated with the ospf instance. + - Md5 authentication key-id associated with the ospf instance. If this is present, message_digest_encryption_type, message_digest_algorithm_type and message_digest_password are mandatory. Valid value is an integer and 'default'. @@ -107,7 +107,8 @@ default: null state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] From 6bf9e99a5a08c4575f3e7608b4f6f9d76808ffd2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:30:18 +0200 Subject: [PATCH 191/770] Fixed docstring --- network/nxos/nxos_ospf.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index f5583f2789e..2f2800a1d6e 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -32,7 +32,8 @@ required: true state: description: - - Determines whether the config should be present or not on the device. + - Determines whether the config should be present or not + on the device. required: false default: present choices: ['present','absent'] @@ -40,7 +41,7 @@ EXAMPLES = ''' - nxos_ospf: - ospf=ntc + ospf=1 state: present username: "{{ un }}" password: "{{ pwd }}" From 63efe23ca33573852972147f7a800e4ef0a1e157 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 5 Sep 2016 18:34:08 +0200 Subject: [PATCH 192/770] Fixed docstring --- network/nxos/nxos_bgp.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 025983848d2..35eaf04265d 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -20,19 +20,19 @@ --- module: nxos_bgp version_added: "2.2" -short_description: Manages BGP configuration +short_description: Manages BGP configuration. description: - - Manages BGP configurations on NX-OS switches + - Manages BGP configurations on NX-OS switches. author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) extends_documentation_fragment: nxos notes: - - I(state)=absent removes the whole BGP ASN configuration when VRF is - I(default) or the whole VRF instance within the BGP process when using - a different VRF. - - I(default) when supported restores params default value. - - Configuring global parmas is only permitted if VRF is I(default). + - C(state=absent) removes the whole BGP ASN configuration when + C(vrf=default) or the whole VRF instance within the BGP process when + using a different VRF. + - Default when supported restores params default value. + - Configuring global parmas is only permitted if C(vrf=default). options: asn: description: @@ -231,7 +231,8 @@ default: null reconnect_interval: description: - - The BGP reconnection interval for dropped sessions. 1 - 60. + - The BGP reconnection interval for dropped sessions. + Valid values are between 1 and 60. required: false default: null router_id: @@ -266,12 +267,12 @@ default: null timer_bgp_hold: description: - - Set bgp hold timer + - Set BGP hold timer. required: false default: null timer_bgp_keepalive: description: - - Set bgp keepalive timer. + - Set BGP keepalive timer. required: false default: null state: From e5419a273874d23b062f6ce6c229b7ac14c66a44 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 5 Sep 2016 15:38:21 -0400 Subject: [PATCH 193/770] fixes issue where the configobjs are not deserialized to a list When the configuration is compared and the results deserialized, the dumps() function returns a string. This cohereces the return to a list in case before and/or after needs to be applied fixes 4707 --- network/ios/ios_config.py | 4 ++-- network/iosxr/iosxr_config.py | 4 ++-- network/openswitch/ops_config.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index a48754dcf6a..3a3ff2ce419 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -288,7 +288,7 @@ def load_config(module, commands, result): if not module.check_mode and module.params['update'] != 'check': module.config(commands) result['changed'] = module.params['update'] != 'check' - result['updates'] = commands.split('\n') + result['updates'] = commands def run(module, result): match = module.params['match'] @@ -304,7 +304,7 @@ def run(module, result): configobjs = candidate.items if configobjs: - commands = dumps(configobjs, 'commands') + commands = dumps(configobjs, 'commands').split('\n') if module.params['before']: commands[:0] = module.params['before'] diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 63c2a70218c..206b3c6ab85 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -264,7 +264,7 @@ def run(module, result): configobjs = candidate.items if configobjs: - commands = dumps(configobjs, 'commands') + commands = dumps(configobjs, 'commands').split('\n') if module.params['before']: commands[:0] = module.params['before'] @@ -272,7 +272,7 @@ def run(module, result): if module.params['after']: commands.extend(module.params['after']) - result['updates'] = commands.split('\n') + result['updates'] = commands if update != 'check': load_config(module, commands, result) diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index 864ff0a915c..fffa22f94b8 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -237,7 +237,7 @@ def load_config(module, commands, result): if not module.check_mode and module.params['update'] != 'check': module.config(commands) result['changed'] = module.params['update'] != 'check' - result['updates'] = commands.split('\n') + result['updates'] = commands def run(module, result): match = module.params['match'] @@ -253,7 +253,7 @@ def run(module, result): configobjs = candidate.items if configobjs: - commands = dumps(configobjs, 'commands') + commands = dumps(configobjs, 'commands').split('\n') if module.params['before']: commands[:0] = module.params['before'] From e9e1635f9cc4b0f8d69ace330e19b93cc4f8c54b Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 09:03:36 +0200 Subject: [PATCH 194/770] Added missing fullstop --- network/nxos/nxos_acl_interface.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 1989a1bfd77..4fb4032e8f9 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -34,7 +34,7 @@ required: true interface: description: - - Full name of interface, e.g. I(Ethernet1/1) + - Full name of interface, e.g. I(Ethernet1/1). required: true direction: description: From ab00ee04331b2054dca783e41d6be004d359f608 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 09:13:37 +0200 Subject: [PATCH 195/770] Fixed docstring --- network/nxos/nxos_igmp_interface.py | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index cbbe56ac2a6..61d49cc505e 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -20,30 +20,32 @@ --- module: nxos_igmp_interface version_added: "2.2" -short_description: Manages IGMP interface configuration +short_description: Manages IGMP interface configuration. description: - - Manages IGMP interface configuration settings + - Manages IGMP interface configuration settings. extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) notes: - - When state=default, supported params will be reset to a default state. - These include version, startup_query_interval, startup_query_count, - robustness, querier_timeout, query_mrt, query_interval, last_member_qrt, - last_member_query_count, group_timeout, report_llg, and immediate_leave - - When state=absent, all configs for oif_prefix, oif_source, and - oif_routemap will be removed. - - PIM must be enabled to use this module - - This module is for Layer 3 interfaces + - When C(state=default), supported params will be reset to a default state. + These include C(version), C(startup_query_interval), + C(startup_query_count), C(robustness), C(querier_timeout), C(query_mrt), + C(query_interval), C(last_member_qrt), C(last_member_query_count), + C(group_timeout), C(report_llg), and C(immediate_leave). + - When C(state=absent), all configs for C(oif_prefix), C(oif_source), and + C(oif_routemap) will be removed. + - PIM must be enabled to use this module. + - This module is for Layer 3 interfaces. - Route-map check not performed (same as CLI) check when configuring route-map with 'static-oif' - If restart is set to true with other params set, the restart will happen - last, i.e. after the configuration takes place + last, i.e. after the configuration takes place. options: interface: description: - - The FULL interface name for IGMP configuration. + - The full interface name for IGMP configuration. + e.g. I(Ethernet1/2). required: true version: description: @@ -93,7 +95,7 @@ description: - Sets the query interval waited after sending membership reports before the software deletes the group state. Values can range - from 1 to 25 seconds. The default is 1 second + from 1 to 25 seconds. The default is 1 second. required: false default: null last_member_query_count: @@ -147,13 +149,13 @@ default: null restart: description: - - Restart IGMP + - Restart IGMP. required: false choices: ['true', 'false'] default: null state: description: - - Manages desired state of the resource + - Manages desired state of the resource. required: false default: present choices: ['present', 'default'] From 1a5c9d51345d1695795efbcf3b9a26f0c122c431 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 09:18:56 +0200 Subject: [PATCH 196/770] Removed newline --- network/nxos/nxos_ip_interface.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index 1d989c3e8e4..bafc444e3b5 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -33,7 +33,6 @@ - C(mask) must be inserted in decimal format (i.e. 24) for both IPv6 and IPv4. - A single interface can have multiple IPv6 configured. - options: interface: description: From 33d11500338cd463e64f14c117fac6eb2565a135 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:22:56 +0200 Subject: [PATCH 197/770] Improved config function --- network/nxos/nxos_vrrp.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_vrrp.py b/network/nxos/nxos_vrrp.py index 5db65334160..073ee8d2e58 100644 --- a/network/nxos/nxos_vrrp.py +++ b/network/nxos/nxos_vrrp.py @@ -277,15 +277,6 @@ def execute_config_command(commands, module): 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_vrrp(command, response, module): From d183f22147b5f4fe6a215b06a85299df17fcb1e1 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:29:30 +0200 Subject: [PATCH 198/770] Improved config function --- network/nxos/nxos_igmp_interface.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index 61d49cc505e..5f74d34eab1 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -715,15 +715,6 @@ def execute_config_command(commands, module): 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 main(): From 01f3783e7b4aa747381b2e21275a4bbf9c0cf695 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:31:15 +0200 Subject: [PATCH 199/770] Improved config function --- network/nxos/nxos_vrf.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index f46e31eb74c..bcce2872eb0 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -274,15 +274,6 @@ def execute_config_command(commands, module): 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_vrf(module, command, response): From 9a5338e52f185b332d1c33aa9a1651366c710d46 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:32:11 +0200 Subject: [PATCH 200/770] Improved config function --- network/nxos/nxos_vrf_interface.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index 893763ccc29..c0742468397 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -251,15 +251,6 @@ def execute_config_command(commands, module): 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_vrf_interface(command, response, module): From 75a107161da4a1b155976b7577ad36077c9abe5e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:33:10 +0200 Subject: [PATCH 201/770] Improved config function --- network/nxos/nxos_vlan.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index 6d1167d79dc..c7071c10918 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -456,15 +456,6 @@ def execute_config_command(commands, module): 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): From bca384a3a6446ce772858c0a1768e2e1be6dfbe5 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:36:16 +0200 Subject: [PATCH 202/770] Improved config function --- network/nxos/nxos_ip_interface.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index bafc444e3b5..fa9abfd782f 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -258,15 +258,6 @@ def execute_config_command(commands, module): 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): From fb91c9ed724978a66446337b779c015ed8d29e12 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:40:30 +0200 Subject: [PATCH 203/770] Improved config function --- network/nxos/nxos_feature.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index c1a243c9901..c2cac62cf4c 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -247,15 +247,6 @@ def execute_config_command(commands, module): 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): From d1d69e8e38b275107bd3669c5c7a89c58d5b7c87 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:42:14 +0200 Subject: [PATCH 204/770] Improved config function --- network/nxos/nxos_vpc_interface.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 02130e93601..ed81ffe3343 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -257,15 +257,6 @@ def execute_config_command(commands, module): 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') - response = module.cli.run_commands() - except ShellError: - clie = get_exception() - module.fail_json(msg='Error sending CLI commands', - error=str(clie), commands=commands) return response From 055c273f5f343d4031115b221b9367649682c06e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:42:58 +0200 Subject: [PATCH 205/770] Improved config function --- network/nxos/nxos_vpc.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py index bc0a871c212..516c4ddcd92 100644 --- a/network/nxos/nxos_vpc.py +++ b/network/nxos/nxos_vpc.py @@ -305,15 +305,6 @@ def execute_config_command(commands, module): 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): From fae497122f821c9fd45505b47ee67dbc78e6cc86 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:44:02 +0200 Subject: [PATCH 206/770] Improved config function --- network/nxos/nxos_smu.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index 9629b4040c5..310fe7fcbc1 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -291,15 +291,6 @@ def execute_config_command(commands, module): 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') - response = module.cli.run_commands() - except ShellError: - clie = get_exception() - module.fail_json(msg='Error sending CLI commands', - error=str(clie), commands=commands) return response From b7031c810252126b3a0c83b89abe1a53d73b0d24 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:45:09 +0200 Subject: [PATCH 207/770] Improved config function --- network/nxos/nxos_portchannel.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index 49328ea4c84..ab07385effc 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -330,15 +330,6 @@ def execute_config_command(commands, module): 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') - output = module.cli.run_commands() - except ShellError: - clie = get_exception() - module.fail_json(msg='Error sending CLI commands', - error=str(clie), commands=commands) return output From ecc189434645c375e83ceed2e6a70671c3cbfdee Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:45:47 +0200 Subject: [PATCH 208/770] Improved config function --- network/nxos/nxos_hsrp.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index c18e9c7934f..be7cbd5c2b0 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -282,15 +282,6 @@ def execute_config_command(commands, module): 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') - response = module.cli.run_commands() - except ShellError: - clie = get_exception() - module.fail_json(msg='Error sending CLI commands', - error=str(clie), commands=commands) return response From 3051d22d4f17d7a3965bfed647616317d4328c06 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:46:57 +0200 Subject: [PATCH 209/770] Improved config function --- network/nxos/nxos_acl_interface.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 4fb4032e8f9..07060df43b1 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -449,15 +449,6 @@ def execute_config_command(commands, module): 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 main(): From a1520d5e485705da39396219ea173b26b20c1a69 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 6 Sep 2016 12:47:48 +0200 Subject: [PATCH 210/770] Improved config function --- network/nxos/nxos_acl.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index 9e9db7317ee..11117c45d08 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -625,15 +625,6 @@ def execute_config_command(commands, module): 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 main(): From dfb170cab95d7ac49769d8c0933b9c6258243b99 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Tue, 6 Sep 2016 19:30:47 +0200 Subject: [PATCH 211/770] Support DOS file attributes (e.g. archive-bit or hidden-bit) (#4705) This fixes #4554 --- files/unarchive.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index 41f72bdd26b..c14864d4666 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -338,8 +338,13 @@ def is_unarchived(self): # Check first and seventh field in order to skip header/footer if len(pcs[0]) != 7 and len(pcs[0]) != 10: continue if len(pcs[6]) != 15: continue - - if pcs[0][0] not in 'dl-?' or not frozenset(pcs[0][1:]).issubset('rwxst-'): + + # Possible entries: + # -rw-rws--- 1.9 unx 2802 t- defX 11-Aug-91 13:48 perms.2660 + # -rw-a-- 1.0 hpf 5358 Tl i4:3 4-Dec-91 11:33 longfilename.hpfs + # -r--ahs 1.1 fat 4096 b- i4:2 14-Jul-91 12:58 EA DATA. SF + # --w------- 1.0 mac 17357 bx i8:2 4-May-92 04:02 unzip.macr + if pcs[0][0] not in 'dl-?' or not frozenset(pcs[0][1:]).issubset('rwxstah-'): continue ztype = pcs[0][0] From 8e0cc9d370134b1b6989cde478d3710dc2c4cfba Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 6 Sep 2016 14:05:15 -0400 Subject: [PATCH 212/770] updated include_role docs --- utilities/logic/include_role.py | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 784d3a5af4f..6b9ccf265cc 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -37,14 +37,19 @@ - "File to load from a Role's defaults/ directory." required: False default: 'main' + static: + description: + - Gives Ansible a hint if this is a 'static' include or not. If static it implies that it won't need templating nor loops nor conditionals and will show included tasks in the --list options. + required: False + default: None + private: + description: + - If True the variables from defaults/ and vars/ in a role will not be made available to the rest of the play. + default: None notes: - THIS IS EARLY PREVIEW, THINGS MAY CHANGE - - Only basic roles have been tested for now, some things might not work as expected. - Handlers are made available to the whole play. - - Currently role variables are not pushed up to the play. - simple dependencies seem to work fine. - - Role search paths work (implicit vars/ templates/ files/ etc) - - loops don't work. - "Things not tested (yet): plugin overrides, nesting includes, used as handler, other stuff I did not think of when I wrote this." ''' @@ -62,6 +67,19 @@ vars: rolevar1: 'value from task' +- name: Use role in loop + include_role: + name: myrole + with_items: + - '{{roleinput1}}" + - '{{roleinput2}}" + loop_control: + loop_var: roleinputvar + +- name: conditional role + include_role: + name: myrole + when: not idontwanttorun """ RETURN = """ From a746eff954e0fb2985784ce4f12a9017301480a2 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 6 Sep 2016 13:26:40 -0700 Subject: [PATCH 213/770] Windows async module support (#4710) Powershell impls of async_wrapper, async_status- associated tests and async action changes are in https://github.com/ansible/ansible/pull/17400. --- windows/async_status.ps1 | 69 ++++++++ windows/async_wrapper.ps1 | 358 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 427 insertions(+) create mode 100644 windows/async_status.ps1 create mode 100644 windows/async_wrapper.ps1 diff --git a/windows/async_status.ps1 b/windows/async_status.ps1 new file mode 100644 index 00000000000..efde748fb97 --- /dev/null +++ b/windows/async_status.ps1 @@ -0,0 +1,69 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + +$results = @{changed=$false} + +$parsed_args = Parse-Args $args +$jid = Get-AnsibleParam $parsed_args "jid" -failifempty $true -resultobj $results +$mode = Get-AnsibleParam $parsed_args "mode" -Default "status" -ValidateSet "status","cleanup" + +# setup logging directory +$log_path = [System.IO.Path]::Combine($env:LOCALAPPDATA, ".ansible_async", $jid) + +If(-not $(Test-Path $log_path)) +{ + Fail-Json @{ansible_job_id=$jid; started=1; finished=1} "could not find job" +} + +If($mode -eq "cleanup") { + Remove-Item $log_path -Recurse + Exit-Json @{ansible_job_id=$jid; erased=$log_path} +} + +# NOT in cleanup mode, assume regular status mode +# no remote kill mode currently exists, but probably should +# consider log_path + ".pid" file and also unlink that above + +$data = $null +Try { + $data_raw = Get-Content $log_path + + # TODO: move this into module_utils/powershell.ps1? + $jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer + $data = $jss.DeserializeObject($data_raw) +} +Catch { + If(-not $data_raw) { + # file not written yet? That means it is running + Exit-Json @{results_file=$log_path; ansible_job_id=$jid; started=1; finished=0} + } + Else { + Fail-Json @{ansible_job_id=$jid; results_file=$log_path; started=1; finished=1} "Could not parse job output: $data" + } +} + +If (-not $data.ContainsKey("started")) { + $data['finished'] = 1 + $data['ansible_job_id'] = $jid +} +ElseIf (-not $data.ContainsKey("finished")) { + $data['finished'] = 0 +} + +Exit-Json $data diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1 new file mode 100644 index 00000000000..03ba5d72e91 --- /dev/null +++ b/windows/async_wrapper.ps1 @@ -0,0 +1,358 @@ +#!powershell +# This file is part of Ansible +# +# Copyright (c)2016, Matt Davis +# +# 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 . + +Param( + [string]$jid, + [int]$max_exec_time_sec, + [string]$module_path, + [string]$argfile_path, + [switch]$preserve_tmp +) + +# WANT_JSON +# POWERSHELL_COMMON + +Set-StrictMode -Version 2 +$ErrorActionPreference = "Stop" + +Function Start-Watchdog { + Param( + [string]$module_tempdir, + [string]$module_path, + [int]$max_exec_time_sec, + [string]$resultfile_path, + [string]$argfile_path, + [switch]$preserve_tmp, + [switch]$start_suspended + ) + + $native_process_util = @" + using Microsoft.Win32.SafeHandles; + using System; + using System.ComponentModel; + using System.Diagnostics; + using System.Linq; + using System.Runtime.InteropServices; + + namespace Ansible.Async { + + public static class NativeProcessUtil + { + [DllImport("kernel32.dll", SetLastError=true)] + static extern SafeFileHandle OpenThread( + ThreadAccessRights dwDesiredAccess, + bool bInheritHandle, + int dwThreadId); + + [DllImport("kernel32.dll", SetLastError=true)] + static extern int ResumeThread(SafeHandle hThread); + + [Flags] + enum ThreadAccessRights : uint + { + SUSPEND_RESUME = 0x0002 + } + + public static void ResumeThreadById(int threadId) + { + var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId); + if(threadHandle.IsInvalid) + throw new Exception(String.Format("Thread ID {0} is invalid ({1})", threadId, new Win32Exception(Marshal.GetLastWin32Error()).Message)); + + try + { + if(ResumeThread(threadHandle) == -1) + throw new Exception(String.Format("Thread ID {0} cannot be resumed ({1})", threadId, new Win32Exception(Marshal.GetLastWin32Error()).Message)); + } + finally + { + threadHandle.Dispose(); + } + } + + public static void ResumeProcessById(int pid) + { + var proc = Process.GetProcessById(pid); + + foreach(var thread in proc.Threads.OfType().Where(t => t.WaitReason == ThreadWaitReason.Suspended)) + ResumeThreadById(thread.Id); + } + } + } +"@ + + Add-Type -TypeDefinition $native_process_util + + $watchdog_script = { + Set-StrictMode -Version 2 + $ErrorActionPreference = "Stop" + + Function Log { + Param( + [string]$msg + ) + + If(Get-Variable -Name log_path -ErrorAction SilentlyContinue) { + Add-Content $log_path $msg + } + } + + Add-Type -AssemblyName System.Web.Extensions + + # -EncodedCommand won't allow us to pass args, so they have to be templated into the script + $jsonargs = @" + <> +"@ + Function Deserialize-Json { + Param( + [Parameter(ValueFromPipeline=$true)] + [string]$json + ) + + # FUTURE: move this into module_utils/powershell.ps1 and use for everything (sidestep PSCustomObject issues) + # FUTURE: won't work w/ Nano Server/.NET Core- fallback to DataContractJsonSerializer (which can't handle dicts on .NET 4.0) + + Log "Deserializing:`n$json" + + $jss = New-Object System.Web.Script.Serialization.JavaScriptSerializer + return $jss.DeserializeObject($json) + } + + Function Write-Result { + [hashtable]$result, + [string]$resultfile_path + + $result | ConvertTo-Json | Set-Content -Path $resultfile_path + } + + Function Exec-Module { + Param( + [string]$module_tempdir, + [string]$module_path, + [int]$max_exec_time_sec, + [string]$resultfile_path, + [string]$argfile_path, + [switch]$preserve_tmp + ) + + Log "in watchdog exec" + + Try + { + Log "deserializing existing resultfile args" + # read in existing resultsfile to merge w/ module output (it should be written by the time we're unsuspended and running) + $result = Get-Content $resultfile_path -Raw | Deserialize-Json + + Log "deserialized result is $($result | Out-String)" + + Log "creating runspace" + + $rs = [runspacefactory]::CreateRunspace() + $rs.Open() + $rs.SessionStateProxy.Path.SetLocation($module_tempdir) | Out-Null + + Log "creating Powershell object" + + $job = [powershell]::Create() + $job.Runspace = $rs + + Log "adding scripts" + + if($module_path.EndsWith(".ps1")) { + $job.AddScript($module_path) | Out-Null + } + else { + $job.AddCommand($module_path) | Out-Null + $job.AddArgument($argfile_path) | Out-Null + } + + Log "job BeginInvoke()" + + $job_asyncresult = $job.BeginInvoke() + + Log "waiting $max_exec_time_sec seconds for job to complete" + + $signaled = $job_asyncresult.AsyncWaitHandle.WaitOne($max_exec_time_sec * 1000) + + $result["finished"] = 1 + + If($job_asyncresult.IsCompleted) { + Log "job completed, calling EndInvoke()" + + $job_output = $job.EndInvoke($job_asyncresult) + $job_error = $job.Streams.Error + + Log "raw module stdout: \r\n$job_output" + If($job_error) { + Log "raw module stderr: \r\n$job_error" + } + + # write success/output/error to result object + + # TODO: cleanse leading/trailing junk + Try { + $module_result = Deserialize-Json $job_output + # TODO: check for conflicting keys + $result = $result + $module_result + } + Catch { + $excep = $_ + + $result.failed = $true + $result.msg = "failed to parse module output: $excep" + } + + # TODO: determine success/fail, or always include stderr if nonempty? + Write-Result $result $resultfile_path + + Log "wrote output to $resultfile_path" + } + Else { + $job.Stop() + # write timeout to result object + $result.failed = $true + $result.msg = "timed out waiting for module completion" + Write-Result $result $resultfile_path + + Log "wrote timeout to $resultfile_path" + } + + $rs.Close() | Out-Null + } + Catch { + $excep = $_ + + $result = @{failed=$true; msg="module execution failed: $($excep.ToString())`n$($excep.InvocationInfo.PositionMessage)"} + + Write-Result $result $resultfile_path + } + Finally + { + If(-not $preserve_tmp -and $module_tempdir -imatch "-tmp-") { + Try { + Log "deleting tempdir, cwd is $(Get-Location)" + Set-Location $env:USERPROFILE + $res = Remove-Item $module_tempdir -recurse 2>&1 + Log "delete output was $res" + } + Catch { + $excep = $_ + Log "error deleting tempdir: $excep" + } + } + Else { + Log "skipping tempdir deletion" + } + } + } + + Try { + Log "deserializing args" + + # deserialize the JSON args that should've been templated in before execution + $ext_args = Deserialize-Json $jsonargs + + Log "exec module" + + Exec-Module @ext_args + + Log "exec done" + } + Catch { + $excep = $_ + + Log $excep + } + } + + $bp = [hashtable] $MyInvocation.BoundParameters + # convert switch types to bool so they'll serialize as simple bools + $bp["preserve_tmp"] = [bool]$bp["preserve_tmp"] + $bp["start_suspended"] = [bool]$bp["start_suspended"] + + # serialize this function's args to JSON so we can template them verbatim into the script(block) + $jsonargs = $bp | ConvertTo-Json + + $raw_script = $watchdog_script.ToString() + $raw_script = $raw_script.Replace("<>", $jsonargs) + + $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_script)) + + $exec_path = "powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" + + # FUTURE: create under new job to ensure we kill all children on exit? + + # start process suspended + breakaway so we can record the watchdog pid without worrying about a completion race + Set-Variable CREATE_BREAKAWAY_FROM_JOB -Value ([uint32]0x01000000) -Option Constant + Set-Variable CREATE_SUSPENDED -Value ([uint32]0x00000004) -Option Constant + + $pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB + If($start_suspended) { + $pstartup_flags = $pstartup_flags -bor $CREATE_SUSPENDED + } + + $pstartup = ([wmiclass]"Win32_ProcessStartup") + $pstartup.Properties['CreateFlags'].Value = $pstartup_flags + + # execute the dynamic watchdog as a breakway process, which will in turn exec the module + # FUTURE: use CreateProcess + stream redirection to watch for/return quick watchdog failures? + $result = $([wmiclass]"Win32_Process").Create($exec_path, $null, $pstartup) + + $watchdog_pid = $result.ProcessId + + return $watchdog_pid +} + +$local_jid = $jid + "." + $pid + +$results_path = [System.IO.Path]::Combine($env:LOCALAPPDATA, ".ansible_async", $local_jid) + +[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($results_path)) | Out-Null + +$watchdog_args = @{ + module_tempdir=$([System.IO.Path]::GetDirectoryName($module_path)); + module_path=$module_path; + max_exec_time_sec=$max_exec_time_sec; + resultfile_path=$results_path; + argfile_path=$argfile_path; + start_suspended=$true; +} + +If($preserve_tmp) { + $watchdog_args["preserve_tmp"] = $true +} + +# start watchdog/module-exec +$watchdog_pid = Start-Watchdog @watchdog_args + +# populate initial results before we resume the process to avoid result race +$result = @{ + started=1; + finished=0; + results_file=$results_path; + ansible_job_id=$local_jid; + _suppress_tmpdir_delete=$true; + ansible_async_watchdog_pid=$watchdog_pid +} + +$result_json = ConvertTo-Json $result +Set-Content $results_path -Value $result_json + +[Ansible.Async.NativeProcessUtil]::ResumeProcessById($watchdog_pid) + +return $result_json From 5a5c1491aed50ce897806a44111b9da6e3b01618 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 6 Sep 2016 16:51:17 -0700 Subject: [PATCH 214/770] fix async_wrapper start suspended race (#4718) Main thread in started-suspended process may not be immediately resumable on slow targets- poll for proper state for awhile before attempting resume --- windows/async_wrapper.ps1 | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1 index 03ba5d72e91..a4f90094837 100644 --- a/windows/async_wrapper.ps1 +++ b/windows/async_wrapper.ps1 @@ -48,6 +48,7 @@ Function Start-Watchdog { using System.Diagnostics; using System.Linq; using System.Runtime.InteropServices; + using System.Threading; namespace Ansible.Async { @@ -89,7 +90,17 @@ Function Start-Watchdog { { var proc = Process.GetProcessById(pid); - foreach(var thread in proc.Threads.OfType().Where(t => t.WaitReason == ThreadWaitReason.Suspended)) + // wait for at least one suspended thread in the process (this handles possible slow startup race where primary thread of created-suspended process has not yet become runnable) + var retryCount = 0; + while(!proc.Threads.OfType().Any(t=>t.ThreadState == System.Diagnostics.ThreadState.Wait && t.WaitReason == ThreadWaitReason.Suspended)) + { + proc.Refresh(); + Thread.Sleep(50); + if (retryCount > 100) + throw new InvalidOperationException(String.Format("No threads were suspended in target PID {0} after 5s", pid)); + } + + foreach(var thread in proc.Threads.OfType().Where(t => t.ThreadState == System.Diagnostics.ThreadState.Wait && t.WaitReason == ThreadWaitReason.Suspended)) ResumeThreadById(thread.Id); } } From e464599632340f247c593f2db770be9782f7dec5 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 6 Sep 2016 22:51:40 -0400 Subject: [PATCH 215/770] minor bug fixes in eos_template * fixes issue where configuration was not being loaded (#4704) * fixes issue where defaults were not included when argument was set to True tested on EOS 4.15.4F --- network/eos/eos_template.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/network/eos/eos_template.py b/network/eos/eos_template.py index c5361876598..59733ab5931 100644 --- a/network/eos/eos_template.py +++ b/network/eos/eos_template.py @@ -122,8 +122,9 @@ def get_config(module): config = module.params.get('config') + defaults = module.params['include_defaults'] if not config and not module.params['force']: - config = module.config.get_config() + config = module.config.get_config(include_defaults=defaults) return config def filter_exit(commands): @@ -198,7 +199,12 @@ def main(): commands = filter_exit(commands) if commands: if not module.check_mode: - response = module.config(commands, replace=replace) + response = module.config.load_config(commands, + replace=replace, + session='eos_template', + commit=True) + + module.cli('no configure session eos_template') result['responses'] = response result['changed'] = True From 1d4c0abe2902d91b2895452feedcf72bf3dd9e20 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 7 Sep 2016 12:18:33 -0400 Subject: [PATCH 216/770] added backup_file to module returns (#4723) fixes #14502 also cleaned up some unused stuff and fixed imports --- files/ini_file.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index af250f42405..3b9cceecf60 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -101,9 +101,8 @@ backup=yes ''' -import ConfigParser -import sys import os +import re # ============================================================== # match_opt @@ -199,16 +198,17 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese changed = True + backup_file = None if changed and not module.check_mode: if backup: - module.backup_local(filename) + backup_file = module.backup_local(filename) ini_file = open(filename, 'w') try: ini_file.writelines(ini_lines) finally: ini_file.close() - return changed + return (changed, backup_file) # ============================================================== # main @@ -229,8 +229,6 @@ def main(): supports_check_mode = True ) - info = dict() - dest = os.path.expanduser(module.params['dest']) section = module.params['section'] option = module.params['option'] @@ -239,13 +237,17 @@ def main(): backup = module.params['backup'] no_extra_spaces = module.params['no_extra_spaces'] - changed = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces) + (changed,backup_file) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces) file_args = module.load_file_common_arguments(module.params) changed = module.set_fs_attributes_if_different(file_args, changed) + results = { 'changed': changed, 'msg': "OK", 'dest': dest } + if backup_file is not None: + results['backup_file'] = backup_file + # Mission complete - module.exit_json(dest=dest, changed=changed, msg="OK") + module.exit_json(**results) # import module snippets from ansible.module_utils.basic import * From 3bede243235762da2492c2944b40f10f0515955b Mon Sep 17 00:00:00 2001 From: David J Peacock Date: Wed, 7 Sep 2016 12:30:03 -0400 Subject: [PATCH 217/770] description expanded for 112, AKA VRRP protocol) (#4645) * description expanded for 112, AKA VRRP protocol) * corrected syntax for description for IP protocols --- cloud/openstack/os_security_group_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/openstack/os_security_group_rule.py b/cloud/openstack/os_security_group_rule.py index 0bbb2427784..9dcda820dd7 100644 --- a/cloud/openstack/os_security_group_rule.py +++ b/cloud/openstack/os_security_group_rule.py @@ -38,7 +38,7 @@ required: true protocol: description: - - IP protocol + - IP protocols TCP UDP ICMP 112 (VRRP) choices: ['tcp', 'udp', 'icmp', 112, None] default: None port_range_min: From 1d3a70a689f7ec9f487bdcb4d4884e4fed6b9cd5 Mon Sep 17 00:00:00 2001 From: "@skg_net" Date: Wed, 7 Sep 2016 09:34:06 -0700 Subject: [PATCH 218/770] dnos10_template module --- network/dnos10/dnos10_template.py | 176 ++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 network/dnos10/dnos10_template.py diff --git a/network/dnos10/dnos10_template.py b/network/dnos10/dnos10_template.py new file mode 100644 index 00000000000..10f5c806436 --- /dev/null +++ b/network/dnos10/dnos10_template.py @@ -0,0 +1,176 @@ +#!/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: dnos10_template +version_added: "2.2" +author: "Senthil Kumar Ganesan (@skg_net)" +short_description: Manage Dell OS10 device configurations over SSH. +description: + - Manages Dell OS10 network device configurations over SSH. This module + allows implementors to work with the device running-config. It + provides a way to push a set of commands onto a network device + by evaluating the current running-config and only pushing configuration + commands that are not already configured. The config source can + be a set of commands or a template. +extends_documentation_fragment: dnos10 +options: + src: + description: + - The path to the config source. The source can be either a + file with config or a template that will be merged during + runtime. By default the task will first search for the source + file in role or playbook root folder in templates unless a full + path to the file is given. + required: true + force: + description: + - The force argument instructs the module not to consider the + current device running-config. When set to true, this will + cause the module to push the contents of I(src) into the device + without first checking if already configured. This argument is + mutually exclusive with O(config). + required: false + default: false + choices: [ "true", "false" ] + backup: + description: + - When this argument is configured true, the module will backup + the running-config from the node prior to making any changes. + The backup file will be written to backup_{{ hostname }} in + the root of the playbook directory. This argument is + mutually exclusive with O(config). + + required: false + default: false + choices: [ "true", "false" ] + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task. The I(config) argument allows the implementer to + pass in the configuration to use as the base config for + comparison. This argument is mutually exclusive with + O(force) and O(backup). + + required: false + default: null +""" + +EXAMPLES = """ +- name: push a configuration onto the device + dnos10_template: + host: hostname + username: foo + src: config.j2 + +- name: forceable push a configuration onto the device + dnos10_template: + host: hostname + username: foo + src: config.j2 + force: yes + +- name: provide the base configuration for comparison + dnos10_template: + host: hostname + username: foo + src: candidate_config.txt + config: current_config.txt +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +_backup: + description: The current running config of the remote device. + returned: when running config is present in the remote device. + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + returned: when not check_mode + type: list + sample: ['...', '...'] +""" +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.dnos10 + + +def get_config(module): + config = module.params['config'] or dict() + if not config and not module.params['force']: + config = module.config.get_config() + return config + + +def main(): + """ main entry point for module execution + """ + + argument_spec = dict( + src=dict(), + force=dict(default=False, type='bool'), + backup=dict(default=False, type='bool'), + config=dict(), + ) + + mutually_exclusive = [('config', 'backup'), ('config', 'force')] + + module = NetworkModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + result = dict(changed=False) + + candidate = NetworkConfig(contents=module.params['src'], indent=1) + + + contents = get_config(module) + + if contents: + config = NetworkConfig(contents=contents[0], indent=1) + result['_backup'] = contents[0] + + commands = list() + if not module.params['force']: + commands = dumps(candidate.difference(config), 'commands') + else: + commands = str(candidate) + + if commands: + commands = commands.split('\n') + if not module.check_mode: + response = module.config(commands) + result['responses'] = response + result['changed'] = True + + result['updates'] = commands + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 87dc24b19edcc160e39ea95e99ac68295cf91b49 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 4 Sep 2016 09:06:09 -0400 Subject: [PATCH 219/770] roll up of minor fixes in eos_config module * fixes save argument to be type bool * now properly sets the changed returned flag based on diff * updates docstring RETURNS to add backup_path * removes unneeded state argument tested on EOS 4.15.4F --- network/eos/eos_config.py | 39 +++++++++++---------------------------- 1 file changed, 11 insertions(+), 28 deletions(-) diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index ab6aaac1eeb..68458613fb9 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -153,16 +153,6 @@ required: false default: false version_added: "2.2" - state: - description: - - The I(state) argument specifies the state of the config - file on the device. When set to present, the configuration - is updated based on the values of the module. When the value - is set to absent, the device startup config is erased. - required: true - default: present - choices: ['present', 'absent'] - version_added: "2.2" """ EXAMPLES = """ @@ -209,6 +199,11 @@ returned: always type: list sample: ['...', '...'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: path + sample: /playbooks/ansible/backup/eos_config.2016-07-16@22:28:34 """ import time @@ -216,11 +211,6 @@ from ansible.module_utils.eos import NetworkModule, NetworkError from ansible.module_utils.basic import get_exception -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - def check_args(module, warnings): if module.params['save'] and module.check_mode: warnings.append('will not save configuration due to checkmode') @@ -278,9 +268,11 @@ def load_config(module, commands, result): del result['__session__'] result['diff'] = dict(prepared=diff) - result['changed'] = not diff -def present(module, result): + if diff: + result['changed'] = True + +def run(module, result): match = module.params['match'] replace = module.params['replace'] update = module.params['update'] @@ -311,11 +303,6 @@ def present(module, result): if module.params['save'] and not module.check_mode: module.config.save_config() -def absent(module, result): - if not module.check_mode: - module.cli('write erase') - result['changed'] = True - def main(): """ main entry point for module execution """ @@ -342,9 +329,7 @@ def main(): config=dict(), defaults=dict(type='bool', default=False), - save=dict(default=False), - - state=dict(default='present', choices=['absent', 'present']) + save=dict(default=False, type='bool'), ) mutually_exclusive = [('lines', 'src')] @@ -354,8 +339,6 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - state = module.params['state'] - if module.params['force'] is True: module.params['match'] = 'none' @@ -368,7 +351,7 @@ def main(): result['__backup__'] = backup_config(module, result) try: - invoke(state, module, result) + run(module, result) except NetworkError: exc = get_exception() module.fail_json(msg=str(exc)) From 624f813f60f220096954ccbc17b8fb5fcdb5bcfa Mon Sep 17 00:00:00 2001 From: Matt Ferrante Date: Wed, 7 Sep 2016 14:16:32 -0600 Subject: [PATCH 220/770] Properly support tag updates on CloudFormation stack-update actions (#3638) --- cloud/amazon/cloudformation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 615f11527c5..331bd394286 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -342,7 +342,8 @@ def main(): stack_policy_body=stack_policy_body, disable_rollback=disable_rollback, template_url=template_url, - capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM']) + capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], + **kwargs) operation = 'UPDATE' except Exception as err: error_msg = boto_exception(err) From 37d08500c5eed79b6a2783ed765659e0a80f7046 Mon Sep 17 00:00:00 2001 From: mlewe Date: Thu, 8 Sep 2016 00:22:16 +0200 Subject: [PATCH 221/770] Fixes ansible/ansible#15922 (#3793) --- commands/command.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/commands/command.py b/commands/command.py index f3229278b41..410efb02ca2 100644 --- a/commands/command.py +++ b/commands/command.py @@ -163,7 +163,6 @@ def main(): cmd=args, stdout="skipped, since %s exists" % creates, changed=False, - stderr=False, rc=0 ) @@ -176,7 +175,6 @@ def main(): cmd=args, stdout="skipped, since %s does not exist" % removes, changed=False, - stderr=False, rc=0 ) From 05887b8f866cc6f73bf981e60f0a7dc434abacdc Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 7 Sep 2016 19:42:59 -0400 Subject: [PATCH 222/770] adds path kwarg when performing config diff checks in ios_config --- network/ios/ios_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 3a3ff2ce419..c45238dc2f0 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -242,8 +242,6 @@ def check_args(module, warnings): if module.params['parents']: if not module.params['lines'] or module.params['src']: warnings.append('ignoring unnecessary argument parents') - if module.params['match'] == 'none' and module.params['replace']: - warnings.append('ignorning unnecessary argument replace') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -298,7 +296,9 @@ def run(module, result): if match != 'none': config = get_config(module, result) - configobjs = candidate.difference(config, match=match, replace=replace) + path = module.params['parents'] + configobjs = candidate.difference(config, path=path, match=match, + replace=replace) else: config = None configobjs = candidate.items From c3c964475c073f0ad45685b65dbc79721c30807d Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 7 Sep 2016 19:56:27 -0400 Subject: [PATCH 223/770] fix bug in ios_template when include_defaults is set to true Module was ignoring include_defaults argument. This fixes the issue such that the correct configuration is returned --- network/ios/ios_template.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/network/ios/ios_template.py b/network/ios/ios_template.py index 067946829a7..52e82f26e3f 100644 --- a/network/ios/ios_template.py +++ b/network/ios/ios_template.py @@ -119,8 +119,9 @@ def get_config(module): config = module.params['config'] or dict() + defaults = module.params['include_defaults'] if not config and not module.params['force']: - config = module.config.get_config() + config = module.config.get_config(include_defaults=defaults) return config def main(): From aac55fcc629354955e3ccaf9e21a2a5ac15d71ca Mon Sep 17 00:00:00 2001 From: Florian Dambrine Date: Thu, 8 Sep 2016 04:25:17 -0600 Subject: [PATCH 224/770] Fix ec2 module source_dest_check when running on non VPC instances (EC2 Classic) (#3243) --- cloud/amazon/ec2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 1d96e492461..b8892187c34 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1309,7 +1309,7 @@ def startstop_instances(module, ec2, instance_ids, state, instance_tags): for inst in res.instances: # Check "source_dest_check" attribute - if inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: + if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: inst.modify_attribute('sourceDestCheck', source_dest_check) changed = True From 55d51b3946fb3c6e535ac684ff0b875cab766282 Mon Sep 17 00:00:00 2001 From: Abhijit Menon-Sen Date: Thu, 8 Sep 2016 22:03:36 +0530 Subject: [PATCH 225/770] Fix spot instance creation by ignoring instance_initiated_shutdown_behavior (#4741) Before this, all spot instance requests would fail because the code _always_ called module.fail_json when the parameter was set (which it always was, because the module parameter's default was set to 'stop'). As the comment said, this parameter doesn't make sense for spot instances at all, so the error message was also misleading. --- cloud/amazon/ec2.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index b8892187c34..3ce044abe10 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1051,8 +1051,9 @@ def create_instances(module, ec2, vpc, override_count=None): private_ip_address = private_ip, )) - # Spot instances do not support start/stop thereby not having the option to change shutdown behavior - params['instance_initiated_shutdown_behavior'] = instance_initiated_shutdown_behavior + # For ordinary (not spot) instances, we can select 'stop' + # (the default) or 'terminate' here. + params['instance_initiated_shutdown_behavior'] = instance_initiated_shutdown_behavior or 'stop' res = ec2.run_instances(**params) instids = [ i.id for i in res.instances ] @@ -1088,11 +1089,11 @@ def create_instances(module, ec2, vpc, override_count=None): module.fail_json( msg="placement_group parameter requires Boto version 2.3.0 or higher.") - if boto_supports_param_in_spot_request(ec2, 'instance_initiated_shutdown_behavior'): - params['instance_initiated_shutdown_behavior'] = instance_initiated_shutdown_behavior - elif instance_initiated_shutdown_behavior: + # You can't tell spot instances to 'stop'; they will always be + # 'terminate'd. For convenience, we'll ignore the latter value. + if instance_initiated_shutdown_behavior and instance_initiated_shutdown_behavior != 'terminate': module.fail_json( - msg="instance_initiated_shutdown_behavior parameter is not supported by your Boto version.") + msg="instance_initiated_shutdown_behavior=stop is not supported for spot instances.") if spot_launch_group and isinstance(spot_launch_group, basestring): params['launch_group'] = spot_launch_group @@ -1455,7 +1456,7 @@ def main(): source_dest_check = dict(type='bool', default=True), termination_protection = dict(type='bool', default=False), state = dict(default='present', choices=['present', 'absent', 'running', 'restarted', 'stopped']), - instance_initiated_shutdown_behavior=dict(default='stop', choices=['stop', 'terminate']), + instance_initiated_shutdown_behavior=dict(default=None, choices=['stop', 'terminate']), exact_count = dict(type='int', default=None), count_tag = dict(), volumes = dict(type='list'), From ec9f6594cac0eabdfa916283e452fefc11211c4b Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 8 Sep 2016 15:18:00 -0400 Subject: [PATCH 226/770] fixes bug where nxos_config wasn't handling checkpoints correctly --- network/nxos/nxos_config.py | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index 0a49460255a..33ced743b75 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -245,7 +245,7 @@ def get_config(module, result): contents = module.config.get_config(include_defaults=defaults) result[key] = contents - return NetworkConfig(indent=1, contents=contents) + return NetworkConfig(indent=2, contents=contents) def backup_config(module, result): if '__config__' not in result: @@ -254,15 +254,13 @@ def backup_config(module, result): def load_config(module, commands, result): if not module.check_mode: - checkpoint = 'ansible_%s' % int(time.time()) - module.cli(['checkpoint %s' % checkpoint], output='text') - result['__checkpoint__'] = checkpoint module.config.load_config(commands) result['changed'] = True def load_checkpoint(module, result): try: checkpoint = result['__checkpoint__'] + module.log('load checkpoint %s' % checkpoint) module.cli(['rollback running-config checkpoint %s' % checkpoint, 'no checkpoint %s' % checkpoint], output='text') except KeyError: @@ -281,7 +279,9 @@ def run(module, result): if match != 'none': config = get_config(module, result) - configobjs = candidate.difference(config, match=match, replace=replace) + path = module.params['parents'] + configobjs = candidate.difference(config, path=path, match=match, + replace=replace) else: config = None configobjs = candidate.items @@ -291,7 +291,6 @@ def run(module, result): if configobjs: commands = dumps(configobjs, 'commands').split('\n') - result['updates'] = commands if module.params['before']: commands[:0] = module.params['before'] @@ -299,6 +298,15 @@ def run(module, result): if module.params['after']: commands.extend(module.params['after']) + result['updates'] = commands + + # create a checkpoint of the current running config in case + # there is a problem loading the candidate config + checkpoint = 'ansible_%s' % int(time.time()) + module.cli(['checkpoint %s' % checkpoint], output='text') + result['__checkpoint__'] = checkpoint + module.log('create checkpoint %s' % checkpoint) + # if the update mode is set to check just return # and do not try to load into the system if update != 'check': @@ -307,7 +315,8 @@ def run(module, result): # remove the checkpoint file used to restore the config # in case of an error if not module.check_mode: - module.cli('no checkpoint %s' % result['__checkpoint__']) + module.log('remove checkpoint %s' % checkpoint) + module.cli('no checkpoint %s' % checkpoint, output='text') if module.params['save'] and not module.check_mode: module.config.save_config() From ab7fe884d7771e5c4d2d9a5f0c055311a73316a9 Mon Sep 17 00:00:00 2001 From: afunix Date: Thu, 8 Sep 2016 22:23:54 +0300 Subject: [PATCH 227/770] Updated get_url module to process FTP results correctly [#3661] (#4601) --- network/basics/get_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/basics/get_url.py b/network/basics/get_url.py index f2cc7c615c1..3ec59c2b08c 100644 --- a/network/basics/get_url.py +++ b/network/basics/get_url.py @@ -228,7 +228,7 @@ def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10, head module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', '')) # create a temporary file and copy content to do checksum-based replacement - if info['status'] != 200 and not url.startswith('file:/'): + if info['status'] != 200 and not url.startswith('file:/') and not (url.startswith('ftp:/') and info.get('msg', '').startswith('OK')): module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url, dest=dest) if tmp_dest != '': From 41f57373345d8424845752401f8da7b52c378683 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Thu, 8 Sep 2016 14:49:19 -0700 Subject: [PATCH 228/770] Added Command module to support Dell Networking OS9 --- network/dnos9/__init__.py | 0 network/dnos9/dnos9_command.py | 209 +++++++++++++++++++++++++++++++++ 2 files changed, 209 insertions(+) create mode 100644 network/dnos9/__init__.py create mode 100755 network/dnos9/dnos9_command.py diff --git a/network/dnos9/__init__.py b/network/dnos9/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/dnos9/dnos9_command.py b/network/dnos9/dnos9_command.py new file mode 100755 index 00000000000..1fa2ba4276f --- /dev/null +++ b/network/dnos9/dnos9_command.py @@ -0,0 +1,209 @@ +#!/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: dnos9_command +version_added: "2.2" +short_description: Run commands on remote devices running Dell OS9 +description: + - Sends arbitrary commands to a Dell OS9 node and returns the results + read from the device. This module includes an + argument that will cause the module to wait for a specific condition + before returning or timing out if the condition is not met. + - This module does not support running commands in configuration mode. + Please use M(dnos9_config) to configure Dell OS9 devices. +extends_documentation_fragment: dnos9 +options: + commands: + description: + - List of commands to send to the remote dnos9 device over the + configured provider. The resulting output from the command + is returned. If the I(wait_for) argument is provided, the + module is not returned until the condition is satisfied or + the number of retries has expired. + required: true + wait_for: + description: + - List of conditions to evaluate against the output of the + command. The task will wait for each condition to be true + before moving forward. If the conditional is not true + within the configured number of I(retries), the task fails. + See examples. + required: false + default: null + retries: + description: + - Specifies the number of retries a command should be tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the + I(wait_for) conditions. + required: false + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + required: false + default: 1 +""" + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: admin + password: admin + transport: cli + +tasks: + - name: run show version on remote devices + dnos9_command: + commands: show version + provider "{{ cli }}" + + - name: run show version and check to see if output contains OS9 + dnos9_command: + commands: show version + wait_for: result[0] contains OS9 + provider "{{ cli }}" + + - name: run multiple commands on remote nodes + dnos9_command: + commands: + - show version + - show interfaces + provider "{{ cli }}" + + - name: run multiple commands and evalute the output + dnos9_command: + commands: + - show version + - show interfaces + wait_for: + - result[0] contains OS9 + - result[1] contains Loopback + provider "{{ cli }}" +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always + type: list + sample: ['...', '...'] + +stdout_lines: + description: The value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] + +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] + +warnings: + description: The list of warnings (if any) generated by module based on arguments + returned: always + type: list + sample: ['...', '...'] +""" + +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner, FailedConditionsError +from ansible.module_utils.network import NetworkModule, NetworkError +import ansible.module_utils.dnos9 + + +def to_lines(stdout): + for item in stdout: + if isinstance(item, basestring): + item = str(item).split('\n') + yield item + + +def main(): + spec = dict( + commands=dict(type='list', required=True), + wait_for=dict(type='list'), + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + module = NetworkModule(argument_spec=spec, + connect_on_load=False, + supports_check_mode=True) + + commands = module.params['commands'] + conditionals = module.params['wait_for'] or list() + + warnings = list() + + runner = CommandRunner(module) + + for cmd in commands: + if module.check_mode and not cmd.startswith('show'): + warnings.append('only show commands are supported when using ' + 'check mode, not executing `%s`' % cmd) + else: + if cmd.startswith('conf'): + module.fail_json(msg='dnos9_command does not support running ' + 'config mode commands. Please use ' + 'dnos9_config instead') + runner.add_command(cmd) + + for item in conditionals: + runner.add_conditional(item) + + runner.retries = module.params['retries'] + runner.interval = module.params['interval'] + + try: + runner.run() + except FailedConditionsError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) + + result = dict(changed=False) + + result['stdout'] = list() + for cmd in commands: + try: + output = runner.get_command(cmd) + except ValueError: + output = 'command not executed due to check_mode, see warnings' + result['stdout'].append(output) + + result['warnings'] = warnings + result['stdout_lines'] = list(to_lines(result['stdout'])) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From c716744f3cb36c56214da788104229fcbf2572af Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 8 Sep 2016 15:46:39 -0400 Subject: [PATCH 229/770] minor bug fix to pass path to difference() in ios_config The ios_config module needs to pass the path kwarg to difference when specifying match=exact or strict. --- network/ios/ios_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index c45238dc2f0..1050b608443 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -291,13 +291,14 @@ def load_config(module, commands, result): def run(module, result): match = module.params['match'] replace = module.params['replace'] + path = module.params['parents'] candidate = get_candidate(module) if match != 'none': config = get_config(module, result) path = module.params['parents'] - configobjs = candidate.difference(config, path=path, match=match, + configobjs = candidate.difference(config, path=path,match=match, replace=replace) else: config = None From bf5b3de83eae72e0602ce2418a1a547c023ed3fe Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 8 Sep 2016 17:56:09 -0700 Subject: [PATCH 230/770] Python 3 fixes for apt_* modules. (#4754) --- packaging/os/apt.py | 8 +++++--- packaging/os/apt_key.py | 2 +- packaging/os/apt_repository.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 596430600d0..9dd54adfaf9 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -198,6 +198,8 @@ import fnmatch import itertools +from ansible.module_utils._text import to_native + # APT related constants APT_ENV_VARS = dict( DEBIAN_FRONTEND = 'noninteractive', @@ -372,7 +374,7 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache): return new_pkgspec def parse_diff(output): - diff = output.splitlines() + diff = to_native(output).splitlines() try: # check for start marker from aptitude diff_start = diff.index('Resolving dependencies...') @@ -385,7 +387,7 @@ def parse_diff(output): diff_start = -1 try: # check for end marker line from both apt-get and aptitude - diff_end = (i for i, item in enumerate(diff) if re.match('[0-9]+ (packages )?upgraded', item)).next() + diff_end = next(i for i, item in enumerate(diff) if re.match('[0-9]+ (packages )?upgraded', item)) except StopIteration: diff_end = len(diff) diff_start += 1 @@ -475,7 +477,7 @@ def get_field_of_deb(m, deb_file, field="Version"): rc, stdout, stderr = m.run_command(cmd) if rc != 0: m.fail_json(msg="%s failed" % cmd, stdout=stdout, stderr=stderr) - return stdout.strip('\n') + return to_native(stdout).strip('\n') def install_deb(m, debs, cache, force, install_recommends, allow_unauthenticated, dpkg_options): changed=False diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index ebcdc3aa4b9..c2993be1af7 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -130,7 +130,7 @@ def all_keys(module, keyring, short_format): cmd = "apt-key adv --list-public-keys --keyid-format=long" (rc, out, err) = module.run_command(cmd) results = [] - lines = out.split('\n') + lines = to_native(out).split('\n') for line in lines: if line.startswith("pub") or line.startswith("sub"): tokens = line.split() diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index 1a19c12e47a..cecd94ac322 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -374,7 +374,7 @@ def _get_ppa_info(self, owner_name, ppa_name): response, info = fetch_url(self.module, lp_api, headers=headers) if info['status'] != 200: self.module.fail_json(msg="failed to fetch PPA information, error was: %s" % info['msg']) - return json.load(response) + return json.loads(to_native(response.read())) def _expand_ppa(self, path): ppa = path.split(':')[1] From 3d365c5cf8683e043437c5d22657c2057a65c94d Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 9 Sep 2016 08:46:33 -0400 Subject: [PATCH 231/770] adds path kwarg when difference() is called from iosxr_config Adds the path kwarg to handle use cases with exact and strict matching --- network/iosxr/iosxr_config.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 206b3c6ab85..51a188db6f8 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -219,6 +219,12 @@ def check_args(module, warnings): warnings.append('ignoring unnecessary argument replace') if module.params['update'] == 'replace' and not module.params['src']: module.fail_json(msg='Must specify src when update is `replace`') + if module.params['src'] and module.params['match'] not in ['line', 'none']: + module.fail_json(msg='match argument must be set to either `line` or ' + '`none` when src argument is defined') + if module.params['src'] and module.params['replace'] != 'line': + module.fail_json(msg='replace argument must be set to `line` when ' + 'src argument is specified') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -253,12 +259,14 @@ def run(module, result): match = module.params['match'] replace = module.params['replace'] update = module.params['update'] + path = module.params['parents'] candidate = get_candidate(module) if match != 'none' and update != 'replace': config = get_config(module, result) - configobjs = candidate.difference(config, match=match, replace=replace) + configobjs = candidate.difference(config, path=path, match=match, + replace=replace) else: config = None configobjs = candidate.items From 4277b88de519f7c7b183c746a8fb15ff35373c2f Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 9 Sep 2016 12:22:43 -0400 Subject: [PATCH 232/770] update junos_facts module to remove get_module() factory function (#4760) Replaces get_module() with NetworkModule instance --- network/junos/junos_facts.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/network/junos/junos_facts.py b/network/junos/junos_facts.py index c310e78f6ca..8ff5eb4572b 100644 --- a/network/junos/junos_facts.py +++ b/network/junos/junos_facts.py @@ -85,6 +85,8 @@ returned: always type: dict """ +from ansible.module_utils.junos import NetworkModule +from ansible.module_utils.junos import xml_to_string, xml_to_json def main(): """ Main entry point for AnsibleModule @@ -95,12 +97,12 @@ def main(): transport=dict(default='netconf', choices=['netconf']) ) - module = get_module(argument_spec=spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=spec, + supports_check_mode=True) result = dict(changed=False) - facts = module.get_facts() + facts = module.connection.get_facts() if '2RE' in facts: facts['has_2RE'] = facts['2RE'] @@ -110,19 +112,17 @@ def main(): if module.params['config'] is True: config_format = module.params['config_format'] - resp_config = module.get_config( config_format=config_format) + resp_config = module.config.get_config(config_format=config_format) if config_format in ['text', 'set']: - facts['config'] = resp_config + facts['config'] = resp_config elif config_format == "xml": - facts['config'] = xml_to_string(resp_config) - facts['config_json'] = xml_to_json(resp_config) + facts['config'] = xml_to_string(resp_config) + facts['config_json'] = xml_to_json(resp_config) result['ansible_facts'] = facts module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.junos import * if __name__ == '__main__': main() From 477c71d9853e35997473418ff883dee9a8bd1271 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 9 Sep 2016 12:25:21 -0400 Subject: [PATCH 233/770] minor updates to junos_package module for 2.2 (#4761) * replaces get_module() with NetworkModule() * removes old call to package_version() --- network/junos/junos_package.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/network/junos/junos_package.py b/network/junos/junos_package.py index f11e121b241..fbf575b4dfc 100644 --- a/network/junos/junos_package.py +++ b/network/junos/junos_package.py @@ -92,6 +92,7 @@ src: junos-vsrx-12.1X46-D10.2-domestic.tgz reboot: no """ +from ansible.module_utils.junos import NetworkModule try: from jnpr.junos.utils.sw import SW @@ -127,8 +128,8 @@ def main(): transport=dict(default='netconf', choices=['netconf']) ) - module = get_module(argument_spec=spec, - supports_check_mode=True) + module = NetworkModule(argument_spec=spec, + supports_check_mode=True) if not HAS_SW: module.fail_json(msg='Missing jnpr.junos.utils.sw module') @@ -137,8 +138,8 @@ def main(): do_upgrade = module.params['force'] or False if not module.params['force']: - has_ver = module.get_facts().get('version') - wants_ver = module.params['version'] or package_version(module) + has_ver = module.connection.get_facts().get('version') + wants_ver = module.params['version'] do_upgrade = has_ver != wants_ver if do_upgrade: @@ -148,8 +149,6 @@ def main(): module.exit_json(**result) -from ansible.module_utils.basic import * -from ansible.module_utils.junos import * if __name__ == '__main__': main() From 1cda0b1819ce9e56df72a74a227357cd72231a99 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 9 Sep 2016 18:26:19 +0200 Subject: [PATCH 234/770] Ensure unicode characters in zip-compressed filenames work correctly (#4702) * Ensure unicode characters in zip-compressed filenames work correctly Another corner-case we are fixing hoping it doesn't break anything else. This fixes: - The correct encoding of unicode paths internally (so the filenames we scrape from the output and is returned by zipfile match) - Disable LANG=C for the unzip command (because it breaks the unicode output, unlike on gtar) * Fix for python3 and other suggestions from @abadger --- files/unarchive.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index c14864d4666..7f5ab13b4c4 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -133,6 +133,7 @@ import binascii import codecs from zipfile import ZipFile, BadZipfile +from ansible.module_utils._text import to_text try: # python 3.3+ from shlex import quote @@ -352,7 +353,7 @@ def is_unarchived(self): version = pcs[1] ostype = pcs[2] size = int(pcs[3]) - path = pcs[7] + path = to_text(pcs[7], errors='surrogate_or_strict') # Skip excluded files if path in self.excludes: @@ -597,7 +598,7 @@ def files_in_archive(self, force_refresh=False): if self.excludes: cmd.extend([ '--exclude=' + quote(f) for f in self.excludes ]) cmd.extend([ '-f', self.src ]) - rc, out, err = self.module.run_command(cmd) + rc, out, err = self.module.run_command(cmd, cwd=self.dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')) if rc != 0: raise UnarchiveError('Unable to list files in the archive') @@ -626,7 +627,7 @@ def is_unarchived(self): if self.excludes: cmd.extend([ '--exclude=' + quote(f) for f in self.excludes ]) cmd.extend([ '-f', self.src ]) - rc, out, err = self.module.run_command(cmd) + rc, out, err = self.module.run_command(cmd, cwd=self.dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')) # Check whether the differences are in something that we're # setting anyway @@ -675,7 +676,7 @@ def unarchive(self): if self.excludes: cmd.extend([ '--exclude=' + quote(f) for f in self.excludes ]) cmd.extend([ '-f', self.src ]) - rc, out, err = self.module.run_command(cmd, cwd=self.dest) + rc, out, err = self.module.run_command(cmd, cwd=self.dest, environ_update=dict(LANG='C', LC_ALL='C', LC_MESSAGES='C')) return dict(cmd=cmd, rc=rc, out=out, err=err) def can_handle_archive(self): @@ -746,9 +747,6 @@ def main(): supports_check_mode = True, ) - # We screenscrape a huge amount of commands so use C locale anytime we do - module.run_command_environ_update = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C', LC_CTYPE='C') - src = os.path.expanduser(module.params['src']) dest = os.path.expanduser(module.params['dest']) copy = module.params['copy'] From b5940e1a3ed5eada03dbb6466028a9a17d8d8574 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Fri, 9 Sep 2016 13:50:28 -0700 Subject: [PATCH 235/770] Added Command module to support Dell Networking OS6 --- network/dnos6/__init__.py | 0 network/dnos6/dnos6_command.py | 208 +++++++++++++++++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 network/dnos6/__init__.py create mode 100644 network/dnos6/dnos6_command.py diff --git a/network/dnos6/__init__.py b/network/dnos6/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/network/dnos6/dnos6_command.py b/network/dnos6/dnos6_command.py new file mode 100644 index 00000000000..1d26620ad1c --- /dev/null +++ b/network/dnos6/dnos6_command.py @@ -0,0 +1,208 @@ +# !/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: dnos6_command +version_added: "2.2" +short_description: Run commands on remote devices running Dell OS6 +description: + - Sends arbitrary commands to a Dell OS6 node and returns the results + read from the device. The M(dnos6_command) module includes an + argument that will cause the module to wait for a specific condition + before returning or timing out if the condition is not met. + - This module does not support running commands in configuration mode. + Please use M(dnos6_config) to configure Dell OS6 devices. +extends_documentation_fragment: dnos6 +options: + commands: + description: + - List of commands to send to the remote dnos6 device over the + configured provider. The resulting output from the command + is returned. If the I(waitfor) argument is provided, the + module is not returned until the condition is satisfied or + the number of I(retries) as expired. + required: true + wait_for: + description: + - List of conditions to evaluate against the output of the + command. The task will wait for each condition to be true + before moving forward. If the conditional is not true + within the configured number of I(retries), the task fails. + See examples. + required: false + default: null + retries: + description: + - Specifies the number of retries a command should be tried + before it is considered failed. The command is run on the + target device every retry and evaluated against the + I(waitfor) conditions. + required: false + default: 10 + interval: + description: + - Configures the interval in seconds to wait between retries + of the command. If the command does not pass the specified + conditions, the interval indicates how long to wait before + trying the command again. + required: false + default: 1 +""" + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: admin + password: admin + transport: cli + +tasks: + - name: run show verion on remote devices + dnos6_command: + commands: show version + provider "{{ cli }}" + + - name: run show version and check to see if output contains Dell + dnos6_command: + commands: show version + wait_for: result[0] contains Dell + provider "{{ cli }}" + + - name: run multiple commands on remote nodes + dnos6_command: + commands: + - show version + - show interfaces + provider "{{ cli }}" + + - name: run multiple commands and evaluate the output + dnos6_command: + commands: + - show version + - show interfaces + wait_for: + - result[0] contains Dell + - result[1] contains Access + provider "{{ cli }}" +""" + +RETURN = """ +stdout: + description: The set of responses from the commands + returned: always + type: list + sample: ['...', '...'] + +stdout_lines: + description: The value of stdout split into a list + returned: always + type: list + sample: [['...', '...'], ['...'], ['...']] + +failed_conditions: + description: The list of conditionals that have failed + returned: failed + type: list + sample: ['...', '...'] + +warnings: + description: The list of warnings (if any) generated by module based on arguments + returned: always + type: list + sample: ['...', '...'] +""" + +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner, FailedConditionsError +from ansible.module_utils.network import NetworkModule, NetworkError +import ansible.module_utils.dnos6 + +def to_lines(stdout): + for item in stdout: + if isinstance(item, basestring): + item = str(item).split('\n') + yield item + + +def main(): + spec = dict( + commands=dict(type='list', required=True), + wait_for=dict(type='list'), + retries=dict(default=10, type='int'), + interval=dict(default=1, type='int') + ) + + module = NetworkModule(argument_spec=spec, + connect_on_load=False, + supports_check_mode=True) + + commands = module.params['commands'] + conditionals = module.params['wait_for'] or list() + + warnings = list() + + runner = CommandRunner(module) + + for cmd in commands: + if module.check_mode and not cmd.startswith('show'): + warnings.append('only show commands are supported when using ' + 'check mode, not executing `%s`' % cmd) + else: + if cmd.startswith('conf'): + module.fail_json(msg='dnos6_command does not support running ' + 'config mode commands. Please use ' + 'dnos6_config instead') + runner.add_command(cmd) + + for item in conditionals: + runner.add_conditional(item) + + runner.retries = module.params['retries'] + runner.interval = module.params['interval'] + + try: + runner.run() + except FailedConditionsError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc)) + + result = dict(changed=False) + + result['stdout'] = list() + for cmd in commands: + try: + output = runner.get_command(cmd) + except ValueError: + output = 'command not executed due to check_mode, see warnings' + result['stdout'].append(output) + + result['warnings'] = warnings + result['stdout_lines'] = list(to_lines(result['stdout'])) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 819fe45864bfa317d219beb6ca900498c436bbf4 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 9 Sep 2016 19:38:05 -0400 Subject: [PATCH 236/770] Fix failure when powering on/off EC2 instances by tag only. (#4767) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If you apply `wait=yes` and use `instance_tags` as your filter for stopping/starting EC2 instances, this stack trace happens: ``` An exception occurred during task execution. The full traceback is: │~ Traceback (most recent call last): │~ File "/tmp/ryansb/ansible_FwE8VR/ansible_module_ec2.py", line 1540, in │~ main() │~ File "/tmp/ryansb/ansible_FwE8VR/ansible_module_ec2.py", line 1514, in main │~ (changed, instance_dict_array, new_instance_ids) = startstop_instances(module, ec2, instance_ids, state, instance_tags) │~ File "/tmp/ryansb/ansible_FwE8VR/ansible_module_ec2.py", line 1343, in startstop_instances │~ if len(matched_instances) < len(instance_ids): │~ TypeError: object of type 'NoneType' has no len() │~ │~ fatal: [localhost -> localhost]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_name": "ec2"}, "module_stderr": "Traceb│~ ack (most recent call last):\n File \"/tmp/ryansb/ansible_FwE8VR/ansible_module_ec2.py\", line 1540, in \n main()\n File \"/tmp/│~ ryansb/ansible_FwE8VR/ansible_module_ec2.py\", line 1514, in main\n (changed, instance_dict_array, new_instance_ids) = startstop_instances│~ (module, ec2, instance_ids, state, instance_tags)\n File \"/tmp/ryansb/ansible_FwE8VR/ansible_module_ec2.py\", line 1343, in startstop_insta│~ nces\n if len(matched_instances) < len(instance_ids):\nTypeError: object of type 'NoneType' has no len()\n", "module_stdout": "", "msg": "│~ MODULE FAILURE", "parsed": false} ``` That's because the `instance_ids` variable is None if not supplied in the task. That means the instances that result from the instance_tags query aren't going to be included in the wait loop. To fix this, a list needs to be kept of instances with matching tags and that list needs to be added to `instance_ids` before the wait loop. --- cloud/amazon/ec2.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 3ce044abe10..d6d34fbe863 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1305,7 +1305,7 @@ def startstop_instances(module, ec2, instance_ids, state, instance_tags): # Check that our instances are not in the state we want to take # Check (and eventually change) instances attributes and instances state - running_instances_array = [] + existing_instances_array = [] for res in ec2.get_all_instances(instance_ids, filters=filters): for inst in res.instances: @@ -1330,7 +1330,9 @@ def startstop_instances(module, ec2, instance_ids, state, instance_tags): except EC2ResponseError as e: module.fail_json(msg='Unable to change state for instance {0}, error: {1}'.format(inst.id, e)) changed = True + existing_instances_array.append(inst.id) + instance_ids = list(set(existing_instances_array + (instance_ids or []))) ## Wait for all the instances to finish starting or stopping wait_timeout = time.time() + wait_timeout while wait and wait_timeout > time.time(): From cac4e680907f4adde0ad817970bc16fe1db87b5e Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Fri, 9 Sep 2016 22:20:56 -0400 Subject: [PATCH 237/770] Default restart_retries to None rather than 0. Fixes #4534. --- cloud/docker/docker_container.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index d1cdd952044..f571cd25c9c 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -1927,7 +1927,7 @@ def main(): recreate=dict(type='bool', default=False), restart=dict(type='bool', default=False), restart_policy=dict(type='str', choices=['no', 'on-failure', 'always', 'unless-stopped']), - restart_retries=dict(type='int', default=0), + restart_retries=dict(type='int', default=None), shm_size=dict(type='str'), security_opts=dict(type='list'), state=dict(type='str', choices=['absent', 'present', 'started', 'stopped'], default='started'), From 0d43a0146236591077605282430f24f744c22179 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Sat, 10 Sep 2016 00:45:51 -0400 Subject: [PATCH 238/770] Purge networks using network name rather than ID. Fixes 4596. --- cloud/docker/docker_container.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index f571cd25c9c..fa54dc63c03 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -1767,9 +1767,9 @@ def _add_networks(self, container, differences): def _purge_networks(self, container, networks): for network in networks: self.results['actions'].append(dict(removed_from_network=network['name'])) - if not self.check_mode and network.get('id'): + if not self.check_mode: try: - self.client.disconnect_container_from_network(container.Id, network['id']) + self.client.disconnect_container_from_network(container.Id, network['name']) except Exception as exc: self.fail("Error disconnecting container from network %s - %s" % (network['name'], str(exc))) From de0122fdaf5975cf2df87fc36b059c74032396e6 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Sat, 10 Sep 2016 01:31:36 -0400 Subject: [PATCH 239/770] Set default log_driver to None to prevent config comparison when a log_driver is not specified. Fixes #4600. --- cloud/docker/docker_container.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index fa54dc63c03..b5e8313af89 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -200,7 +200,7 @@ required: false log_driver: description: - - Specify the logging driver. + - Specify the logging driver. Docker uses json-file by default. choices: - json-file - syslog @@ -209,7 +209,7 @@ - fluentd - awslogs - splunk - default: json-file + default: null required: false log_options: description: @@ -1906,7 +1906,7 @@ def main(): kill_signal=dict(type='str'), labels=dict(type='dict'), links=dict(type='list'), - log_driver=dict(type='str', choices=['json-file', 'syslog', 'journald', 'gelf', 'fluentd', 'awslogs', 'splunk'], default='json-file'), + log_driver=dict(type='str', choices=['json-file', 'syslog', 'journald', 'gelf', 'fluentd', 'awslogs', 'splunk'], default=None), log_options=dict(type='dict', aliases=['log_opt']), mac_address=dict(type='str'), memory=dict(type='str', default='0'), From 8e1e8c2cca03af9bbd30733b37d9162dd1225c0b Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sat, 10 Sep 2016 02:35:14 -0400 Subject: [PATCH 240/770] add new module sros_rollback Provides a configuration resource for managing the rollback feature on remote devices running Nokia SROS --- network/sros/sros_rollback.py | 219 ++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 network/sros/sros_rollback.py diff --git a/network/sros/sros_rollback.py b/network/sros/sros_rollback.py new file mode 100644 index 00000000000..d04437fb32f --- /dev/null +++ b/network/sros/sros_rollback.py @@ -0,0 +1,219 @@ +#!/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: sros_rollback +version_added: "2.2" +author: "Peter Sprygada (@privateip)" +short_description: Configure Nokia SROS rollback +description: + - Configure the rollback feature on remote Nokia devices running + the SROS operating system. this module provides a stateful + implementation for managing the configuration of the rollback + feature +extends_documentation_fragment: sros +options: + rollback_location: + description: + - The I(rollback_location) specifies the location and filename + of the rollback checkpoint files. This argument supports any + valid local or remote URL as specified in SROS + required: false + default: null + remote_max_checkpoints: + description: + - The I(remote_max_checkpoints) argument configures the maximum + number of rollback files that can be transfered and saved to + a remote location. Valid values for this argument are in the + range of 1 to 50 + required: false + default: null + local_max_checkpoints: + description: + - The I(local_max_checkpoints) argument configures the maximum + number of rollback files that can be saved on the devices local + compact flash. Valid values for this argument are in the range + of 1 to 50 + required: false + default: null + rescue_location: + description: + - The I(rescue_location) specifies the location of the + rescue file. This argument supports any valid local + or remote URL as specified in SROS + required: false + default: null + state: + description: + - The I(state) argument specifies the state of the configuration + entries in the devices active configuration. When the state + value is set to C(true) the configuration is present in the + devices active configuration. When the state value is set to + C(false) the configuration values are removed from the devices + active configuration. + required: false + default: present + choices: ['present', 'absent'] +""" + +EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: admin + password: admin + transport: cli + +- name: configure rollback location + sros_rollback: + rollback_location: "cb3:/ansible" + provider: "{{ cli }}" + +- name: remove all rollback configuration + sros_rollback: + state: absent + provider: "{{ cli }}" +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] +""" +from ansible.module_utils.basic import get_exception +from ansible.module_utils.sros import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig, dumps + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + +def sanitize_config(lines): + commands = list() + for line in lines: + for index, entry in enumerate(commands): + if line.startswith(entry): + del commands[index] + break + commands.append(line) + return commands + +def present(module, commands): + setters = set() + for key, value in module.argument_spec.iteritems(): + if module.params[key] is not None: + setter = value.get('setter') or 'set_%s' % key + if setter not in setters: + setters.add(setter) + invoke(setter, module, commands) + +def absent(module, commands): + config = module.config.get_config() + if 'rollback-location' in config: + commands.append('configure system rollback no rollback-location') + if 'rescue-location' in config: + commands.append('configure system rollback no rescue-location') + if 'remote-max-checkpoints' in config: + commands.append('configure system rollback no remote-max-checkpoints') + if 'local-max-checkpoints' in config: + commands.append('configure system rollback no remote-max-checkpoints') + +def set_rollback_location(module, commands): + value = module.params['rollback_location'] + commands.append('configure system rollback rollback-location "%s"' % value) + +def set_local_max_checkpoints(module, commands): + value = module.params['local_max_checkpoints'] + if not 1 <= value <= 50: + module.fail_json(msg='local_max_checkpoints must be between 1 and 50') + commands.append('configure system rollback local-max-checkpoints %s' % value) + +def set_remote_max_checkpoints(module, commands): + value = module.params['remote_max_checkpoints'] + if not 1 <= value <= 50: + module.fail_json(msg='remote_max_checkpoints must be between 1 and 50') + commands.append('configure system rollback remote-max-checkpoints %s' % value) + +def set_rescue_location(module, commands): + value = module.params['rescue_location'] + commands.append('configure system rollback rescue-location "%s"' % value) + +def get_config(module): + contents = module.config.get_config() + return NetworkConfig(device_os='sros', contents=contents) + +def load_config(module, commands, result): + candidate = NetworkConfig(device_os='sros', contents='\n'.join(commands)) + config = get_config(module) + configobjs = candidate.difference(config) + + if configobjs: + commands = dumps(configobjs, 'lines') + commands = sanitize_config(commands.split('\n')) + + result['updates'] = commands + + # send the configuration commands to the device and merge + # them with the current running config + if not module.check_mode: + module.config(commands) + + result['changed'] = True + +def main(): + """ main entry point for module execution + """ + argument_spec = dict( + rollback_location=dict(), + + local_max_checkpoints=dict(type='int'), + remote_max_checkpionts=dict(type='int'), + + rescue_location=dict(), + + state=dict(default='present', choices=['present', 'absent']) + ) + + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + supports_check_mode=True) + + state = module.params['state'] + + result = dict(changed=False) + + commands = list() + invoke(state, module, commands) + + try: + load_config(module, commands, result) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 0c05f0dfa42169da4ac00643bd1e6d8ae417700d Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Sat, 10 Sep 2016 07:46:30 -0600 Subject: [PATCH 241/770] Fix missing colons in network module examples (#4778) --- network/dnos10/dnos10_command.py | 6 +++--- network/eos/eos_command.py | 2 +- network/ios/ios_command.py | 6 +++--- network/iosxr/iosxr_command.py | 6 +++--- network/nxos/nxos_command.py | 2 +- network/sros/sros_command.py | 6 +++--- 6 files changed, 14 insertions(+), 14 deletions(-) diff --git a/network/dnos10/dnos10_command.py b/network/dnos10/dnos10_command.py index 2d68e691c5a..3f880985c62 100644 --- a/network/dnos10/dnos10_command.py +++ b/network/dnos10/dnos10_command.py @@ -80,20 +80,20 @@ - name: run show version on remote devices dnos10_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains OS10 dnos10_command: commands: show version wait_for: result[0] contains OS10 - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands on remote nodes dnos10_command: commands: - show version - show interface - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands and evaluate the output dnos10_command: diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index 931ca237fb0..911f0919364 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -91,7 +91,7 @@ - name: run show verion on remote devices eos_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains Arista eos_command: diff --git a/network/ios/ios_command.py b/network/ios/ios_command.py index b169c5e14c4..5f2e12780b0 100644 --- a/network/ios/ios_command.py +++ b/network/ios/ios_command.py @@ -94,20 +94,20 @@ - name: run show version on remote devices ios_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains IOS ios_command: commands: show version wait_for: result[0] contains IOS - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands on remote nodes ios_command: commands: - show version - show interfaces - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands and evaluate the output ios_command: diff --git a/network/iosxr/iosxr_command.py b/network/iosxr/iosxr_command.py index 3e97ab4b3dd..1d6acc887b8 100644 --- a/network/iosxr/iosxr_command.py +++ b/network/iosxr/iosxr_command.py @@ -93,20 +93,20 @@ - name: run show version on remote devices iosxr_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains iosxr iosxr_command: commands: show version wait_for: result[0] contains IOS-XR - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands on remote nodes iosxr_command: commands: - show version - show interfaces - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands and evaluate the output iosxr_command: diff --git a/network/nxos/nxos_command.py b/network/nxos/nxos_command.py index 5ffcb908239..bcd7566dce6 100644 --- a/network/nxos/nxos_command.py +++ b/network/nxos/nxos_command.py @@ -96,7 +96,7 @@ - name: run show verion on remote devices nxos_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains Cisco nxos_command: diff --git a/network/sros/sros_command.py b/network/sros/sros_command.py index 97342cf54ee..e2e3acd6ccd 100644 --- a/network/sros/sros_command.py +++ b/network/sros/sros_command.py @@ -92,20 +92,20 @@ - name: run show version on remote devices sros_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains sros sros_command: commands: show version wait_for: result[0] contains sros - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands on remote nodes sros_command: commands: - show version - show port detail - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands and evaluate the output sros_command: From 9df9a1dbd460a2f17985a59a6a7f2090a68fe947 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 15:31:53 -0400 Subject: [PATCH 242/770] roll up of updates to eos_config module * removes update argument * adds `config` option to replace argument * moves session management into shared module * cleans up doc strings * `before` and `after` args now only apply to lines --- network/eos/eos_config.py | 108 +++++++++++++------------------------- 1 file changed, 37 insertions(+), 71 deletions(-) diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index 68458613fb9..a91d54653b2 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -95,7 +95,7 @@ line is not correct. required: false default: line - choices: ['line', 'block'] + choices: ['line', 'block', 'config'] force: description: - The force argument instructs the module to not consider the @@ -108,19 +108,6 @@ required: false default: false choices: ['yes', 'no'] - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) I(replace) and I(check). When the argument is - set to I(merge), the configuration changes are merged with the current - device running configuration. When the argument is set to I(check) - the configuration updates are determined but not actually configured - on the remote device. - required: false - default: merge - choices: ['merge', 'replace', 'check'] - version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -191,13 +178,18 @@ before: no ip access-list test replace: block provider: "{{ cli }}" + +- name: load configuration from file + eos_config: + src: eos.cfg + provider: "{{ cli }}" """ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: always - type: list + returned: when lines is specified + type: when lines is specified sample: ['...', '...'] backup_path: description: The full path to the backup file @@ -212,10 +204,6 @@ from ansible.module_utils.basic import get_exception def check_args(module, warnings): - if module.params['save'] and module.check_mode: - warnings.append('will not save configuration due to checkmode') - if module.params['parents'] and module.params['src']: - warnings.append('ignoring parents argument when src specified') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -230,83 +218,57 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def get_config(module, result, defaults=False): - defaults = module.params['defaults'] - if defaults is True: - key = '__configall__' - else: - key = '__config__' - - contents = module.params['config'] or result.get(key) - +def get_config(module, defaults=False): + contents = module.params['config'] if not contents: + defaults = module.params['defaults'] contents = module.config.get_config(include_defaults=defaults) - result[key] = contents - return NetworkConfig(indent=3, contents=contents) -def backup_config(module, result): - if '__config__' not in result: - result['__config__'] = module.config.get_config() - result['__backup__'] = result['__config__'] - def load_config(module, commands, result): - session = 'ansible_%s' % int(time.time()) - - # save the sesion name in case we need later - result['__session__'] = session - - replace = module.params['update'] == 'replace' + replace = module.params['replace'] == 'config' commit = not module.check_mode - diff = module.config.load_config(commands, session=session, - replace=replace, commit=commit) - - # once the configuration is done, remove the config session and - # remove the session name from the result - module.cli(['no configure session %s' % session]) - del result['__session__'] - - result['diff'] = dict(prepared=diff) + diff = module.config.load_config(commands, replace=replace, commit=commit) if diff: + result['diff'] = dict(prepared=diff) result['changed'] = True def run(module, result): match = module.params['match'] replace = module.params['replace'] - update = module.params['update'] candidate = get_candidate(module) - if match != 'none' and update != 'replace': - config = get_config(module, result) + if match != 'none' and replace != 'config': + config = get_config(module) configobjs = candidate.difference(config, match=match, replace=replace) else: - config = None configobjs = candidate.items if configobjs: commands = dumps(configobjs, 'commands').split('\n') - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] - if module.params['after']: - commands.extend(module.params['after']) + if module.params['after']: + commands.extend(module.params['after']) - result['updates'] = commands + result['updates'] = commands - if update != 'check': - load_config(module, commands, result) + load_config(module, commands, result) - if module.params['save'] and not module.check_mode: - module.config.save_config() + if module.params['save']: + if not module.check_mode: + module.config.save_config() + result['changed'] = True def main(): """ main entry point for module execution """ - argument_spec = dict( lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), @@ -317,26 +279,31 @@ def main(): after=dict(type='list'), match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), - replace=dict(default='line', choices=['line', 'block']), + replace=dict(default='line', choices=['line', 'block', 'config']), # this argument is deprecated in favor of setting match: none # it will be removed in a future version force=dict(default=False, type='bool'), - update=dict(choices=['merge', 'replace', 'check'], default='merge'), backup=dict(type='bool', default=False), config=dict(), defaults=dict(type='bool', default=False), - save=dict(default=False, type='bool'), + save=dict(default=False, type='bool'), ) mutually_exclusive = [('lines', 'src')] + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines']), + ('replace', 'config', ['src'])] + module = NetworkModule(argument_spec=argument_spec, connect_on_load=False, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) if module.params['force'] is True: @@ -348,17 +315,16 @@ def main(): result = dict(changed=False, warnings=warnings) if module.params['backup']: - result['__backup__'] = backup_config(module, result) + result['__backup__'] = module.config.get_config() try: run(module, result) except NetworkError: exc = get_exception() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), **exc.kwargs) module.exit_json(**result) if __name__ == '__main__': main() - From 9b5e6bbfa11469fcd9228d46d9695c0b35e20c3d Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 19:42:10 -0400 Subject: [PATCH 243/770] roll up of updates to ios_config module * 'before' and 'after' are now only applied to 'lines' * remove update argument * update doc strings * add path argument when performing config difference --- network/ios/ios_config.py | 82 ++++++++++++++------------------------- 1 file changed, 29 insertions(+), 53 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 1050b608443..426c3183db0 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -109,20 +109,7 @@ will be removed in a future release. required: false default: false - choices: [ "true", "false" ] - version_added: "2.2" - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) and I(check). When the argument is set to - I(merge), the configuration changes are merged with the current - device running configuration. When the argument is set to I(check) - the configuration updates are determined but not actually configured - on the remote device. - required: false - default: merge - choices: ['merge', 'check'] + choices: ["true", "false"] version_added: "2.2" commit: description: @@ -218,18 +205,13 @@ updates: description: The set of commands that will be pushed to the remote device returned: always - type: list + type: when lines is defined sample: ['...', '...'] backup_path: description: The full path to the backup file returned: when backup is yes type: path sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34 -responses: - description: The set of responses from issuing the commands on the device - returned: when not check_mode - type: list - sample: ['...', '...'] """ import re @@ -239,27 +221,16 @@ from ansible.module_utils.netcli import Command def check_args(module, warnings): - if module.params['parents']: - if not module.params['lines'] or module.params['src']: - warnings.append('ignoring unnecessary argument parents') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' 'removed in the future') def get_config(module, result): - defaults = module.params['default'] - if defaults is True: - key = '__configall__' - else: - key = '__config__' - - contents = module.params['config'] or result.get(key) - + contents = module.params['config'] if not contents: + defaults = module.params['default'] contents = module.config.get_config(include_defaults=defaults) - result[key] = contents - return NetworkConfig(indent=1, contents=contents) def get_candidate(module): @@ -275,19 +246,13 @@ def load_backup(module): try: module.cli(['exit', 'config replace flash:/ansible-rollback force']) except NetworkError: - module.fail_json(msg='unable to rollback configuration') + module.fail_json(msg='unable to load backup configuration') def backup_config(module): cmd = 'copy running-config flash:/ansible-rollback' cmd = Command(cmd, prompt=re.compile('\? $'), response='\n') module.cli(cmd) -def load_config(module, commands, result): - if not module.check_mode and module.params['update'] != 'check': - module.config(commands) - result['changed'] = module.params['update'] != 'check' - result['updates'] = commands - def run(module, result): match = module.params['match'] replace = module.params['replace'] @@ -301,17 +266,19 @@ def run(module, result): configobjs = candidate.difference(config, path=path,match=match, replace=replace) else: - config = None configobjs = candidate.items if configobjs: commands = dumps(configobjs, 'commands').split('\n') - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) - if module.params['after']: - commands.extend(module.params['after']) + result['updates'] = commands # create a backup copy of the current running-config on # device flash drive @@ -319,23 +286,29 @@ def run(module, result): # send the configuration commands to the device and merge # them with the current running config - load_config(module, commands, result) + if not module.check_mode: + module.config(commands) + result['changed'] = True # remove the backup copy of the running-config since its # no longer needed module.cli('delete /force flash:/ansible-rollback') - if module.params['save'] and not module.check_mode: - module.config.save_config() + if module.params['save']: + if not module.check_mode: + module.config.save_config() + result['changed'] = True def main(): + """ main entry point for module execution + """ argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - before=dict(type='list'), after=dict(type='list'), @@ -346,20 +319,23 @@ def main(): # it will be removed in a future version force=dict(default=False, type='bool'), - update=dict(choices=['merge', 'check'], default='merge'), - backup=dict(type='bool', default=False), - config=dict(), default=dict(type='bool', default=False), save=dict(type='bool', default=False), + backup=dict(type='bool', default=False), ) mutually_exclusive = [('lines', 'src')] + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines'])] + module = NetworkModule(argument_spec=argument_spec, connect_on_load=False, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) if module.params['force'] is True: From e6f1f295f7f929b16d75f2b70f147a7c0cd6114a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 23:14:28 -0400 Subject: [PATCH 244/770] roll up of updates to iosxr_config module * 'before' and 'after' arguments now only apply to 'lines' * update doc strings * remove update argument * clean up warnings --- network/iosxr/iosxr_config.py | 86 ++++++++++++----------------------- 1 file changed, 29 insertions(+), 57 deletions(-) diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 51a188db6f8..25ffad7c7e6 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -97,7 +97,7 @@ line is not correct required: false default: line - choices: ['line', 'block'] + choices: ['line', 'block', 'config'] force: description: - The force argument instructs the module to not consider the @@ -111,19 +111,6 @@ default: false choices: [ "yes", "no" ] version_added: "2.2" - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) and I(check). When the argument is set to - I(merge), the configuration changes are merged with the current - device running configuration. When the argument is set to I(check) - the configuration updates are determined but not actually configured - on the remote device. - required: false - default: merge - choices: ['merge', 'replace', 'check'] - version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -191,7 +178,7 @@ updates: description: The set of commands that will be pushed to the remote device returned: always - type: list + type: when lines is defined sample: ['...', '...'] backup_path: description: The full path to the backup file @@ -206,25 +193,7 @@ DEFAULT_COMMIT_COMMENT = 'configured by iosxr_config' -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - def check_args(module, warnings): - if module.params['parents']: - if not module.params['lines'] or module.params['src']: - warnings.append('ignoring unnecessary argument parents') - if module.params['match'] == 'none' and module.params['replace']: - warnings.append('ignoring unnecessary argument replace') - if module.params['update'] == 'replace' and not module.params['src']: - module.fail_json(msg='Must specify src when update is `replace`') - if module.params['src'] and module.params['match'] not in ['line', 'none']: - module.fail_json(msg='match argument must be set to either `line` or ' - '`none` when src argument is defined') - if module.params['src'] and module.params['replace'] != 'line': - module.fail_json(msg='replace argument must be set to `line` when ' - 'src argument is specified') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -232,7 +201,7 @@ def check_args(module, warnings): def get_config(module, result): - contents = module.params['config'] or result.get('__config__') + contents = module.params['config'] if not contents: contents = module.config.get_config() return NetworkConfig(indent=1, contents=contents) @@ -247,75 +216,80 @@ def get_candidate(module): return candidate def load_config(module, commands, result): - replace = module.params['update'] == 'replace' + replace = module.params['replace'] == 'config' comment = module.params['comment'] commit = not module.check_mode + diff = module.config.load_config(commands, replace=replace, commit=commit, comment=comment) - result['diff'] = dict(prepared=diff) - result['changed'] = True + + if diff: + result['diff'] = dict(prepared=diff) + result['changed'] = True def run(module, result): match = module.params['match'] replace = module.params['replace'] - update = module.params['update'] path = module.params['parents'] candidate = get_candidate(module) - if match != 'none' and update != 'replace': + if match != 'none' and replace != 'config': config = get_config(module, result) configobjs = candidate.difference(config, path=path, match=match, replace=replace) else: - config = None configobjs = candidate.items if configobjs: commands = dumps(configobjs, 'commands').split('\n') - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] - if module.params['after']: - commands.extend(module.params['after']) + if module.params['after']: + commands.extend(module.params['after']) - result['updates'] = commands + result['updates'] = commands - if update != 'check': - load_config(module, commands, result) + load_config(module, commands, result) def main(): """main entry point for module execution """ argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - before=dict(type='list'), after=dict(type='list'), match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), - replace=dict(default='line', choices=['line', 'block']), - - update=dict(choices=['merge', 'replace', 'check'], default='merge'), - backup=dict(type='bool', default=False), - comment=dict(default=DEFAULT_COMMIT_COMMENT), + replace=dict(default='line', choices=['line', 'block', 'config']), # this argument is deprecated in favor of setting match: none # it will be removed in a future version force=dict(default=False, type='bool'), config=dict(), + backup=dict(type='bool', default=False), + comment=dict(default=DEFAULT_COMMIT_COMMENT), ) mutually_exclusive = [('lines', 'src')] + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines']), + ('replace', 'config', ['src'])] + module = NetworkModule(argument_spec=argument_spec, connect_on_load=False, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) if module.params['force'] is True: @@ -327,9 +301,7 @@ def main(): result = dict(changed=False, warnings=warnings) if module.params['backup']: - config = module.config.get_config() - result['__config__'] = config - result['__backup__'] = config + result['__backup__'] = module.config.get_config() try: run(module, result) From 4217adb4696e8133079f75719b103a61d63f3d12 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 23:17:21 -0400 Subject: [PATCH 245/770] roll up of updates to vyos_config module * remove 'udpates' argument * add required_if dependencies * clean up functions * update doc strings --- network/vyos/vyos_config.py | 39 +++++++++---------------------------- 1 file changed, 9 insertions(+), 30 deletions(-) diff --git a/network/vyos/vyos_config.py b/network/vyos/vyos_config.py index dbe1b257bc2..706f64911fc 100644 --- a/network/vyos/vyos_config.py +++ b/network/vyos/vyos_config.py @@ -56,17 +56,6 @@ required: false default: line choices: ['line', 'none'] - update: - description: - - The C(update) argument controls the method used to update the - remote device configuration. This argument accepts two valid - options, C(merge) or C(check). When C(merge) is specified, the - configuration is merged into the current active config. When - C(check) is specified, the module returns the set of updates - that would be applied to the active configuration. - required: false - default: merge - choices: ['merge', 'check'] backup: description: - The C(backup) argument will backup the current devices active @@ -108,7 +97,7 @@ returned: always type: list sample: ['...', '...'] -removed: +filtered: description: The list of configuration commands removed to avoid a load failure returned: always type: list @@ -153,11 +142,6 @@ ] -def check_args(module, warnings): - if module.params['save'] and module.params['update'] == 'check': - warnings.append('The configuration will not be saved when update ' - 'is set to check') - def config_to_commands(config): set_format = config.startswith('set') or config.startswith('delete') candidate = NetworkConfig(indent=4, contents=config, device_os='junos') @@ -181,7 +165,6 @@ def get_config(module, result): contents = module.params['config'] if not contents: contents = module.config.get_config(output='set').split('\n') - else: contents = config_to_commands(contents) @@ -223,11 +206,11 @@ def diff_config(commands, config): return list(updates) def sanitize_config(config, result): - result['removed'] = list() + result['filtered'] = list() for regex in CONFIG_FILTERS: for index, line in enumerate(list(config)): if regex.search(line): - result['removed'].append(line) + result['filtered'].append(line) del config[index] def load_config(module, commands, result): @@ -264,25 +247,24 @@ def run(module, result): if module.params['update'] != 'check': load_config(module, updates, result) - if result.get('removed'): + if result.get('filtered'): result['warnings'].append('Some configuration commands where ' - 'removed, please see the removed key') + 'removed, please see the filtered key') def main(): argument_spec = dict( - lines=dict(type='list'), src=dict(type='path'), - + lines=dict(type='list'), match=dict(default='line', choices=['line', 'none']), - update=dict(default='merge', choices=['merge', 'check']), - backup=dict(default=False, type='bool'), comment=dict(default=DEFAULT_COMMENT), config=dict(), + + backup=dict(default=False, type='bool'), save=dict(default=False, type='bool'), ) @@ -293,10 +275,7 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - warnings = list() - check_args(module, warnings) - - result = dict(changed=False, warnings=warnings) + result = dict(changed=False) if module.params['backup']: result['__backup__'] = module.config.get_config() From 4f03036428ff94ae2dd9a5f239918ba37f9df604 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 23:19:23 -0400 Subject: [PATCH 246/770] roll up updates to sros_config module * update doc strings * update message if rollback isn't configured --- network/sros/sros_config.py | 129 ++++++++---------------------------- 1 file changed, 26 insertions(+), 103 deletions(-) diff --git a/network/sros/sros_config.py b/network/sros/sros_config.py index e5b95a7705d..2ac47b83074 100644 --- a/network/sros/sros_config.py +++ b/network/sros/sros_config.py @@ -111,30 +111,6 @@ default: false choices: [ "true", "false" ] version_added: "2.2" - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) and I(check). When the argument is set to - I(merge), the configuration changes are merged with the current - device running configuration. When the argument is set to I(check) - the configuration updates are determined but not actually configured - on the remote device. - required: false - default: merge - choices: ['merge', 'check'] - version_added: "2.2" - commit: - description: - - This argument specifies the update method to use when applying the - configuration changes to the remote node. If the value is set to - I(merge) the configuration updates are merged with the running- - config. If the value is set to I(check), no changes are made to - the remote host. - required: false - default: merge - choices: ['merge', 'check'] - version_added: "2.2" backup: description: - This argument will cause the module to create a full backup of @@ -174,15 +150,6 @@ default: no choices: ['yes', 'no'] version_added: "2.2" - state: - description: - - This argument specifies whether or not the running-config is - present on the remote device. When set to I(absent) the - running-config on the remote device is erased. - required: false - default: no - choices: ['yes', 'no'] - version_added: "2.2" """ EXAMPLES = """ @@ -235,22 +202,9 @@ type: path sample: /playbooks/ansible/backup/sros_config.2016-07-16@22:28:34 """ -import re - from ansible.module_utils.basic import get_exception from ansible.module_utils.sros import NetworkModule, NetworkError from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.netcli import Command - -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - -def check_args(module, warnings): - if module.params['parents']: - if not module.params['lines'] or module.params['src']: - warnings.append('ignoring unnecessary argument parents') def sanitize_config(lines): commands = list() @@ -263,10 +217,9 @@ def sanitize_config(lines): return commands def get_config(module, result): - contents = module.params['config'] or result.get('__config__') + contents = module.params['config'] if not contents: contents = module.config.get_config() - result['__config__'] = contents return NetworkConfig(device_os='sros', contents=contents) def get_candidate(module): @@ -278,12 +231,7 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def revert_config(module): - if result.get('__checkpoint__'): - module.cli(['admin rollback revert latest-rb', - 'admin rollback delete latest-rb']) - -def present(module, result): +def run(module, result): match = module.params['match'] candidate = get_candidate(module) @@ -292,7 +240,6 @@ def present(module, result): config = get_config(module, result) configobjs = candidate.difference(config) else: - config = None configobjs = candidate.items if configobjs: @@ -301,58 +248,40 @@ def present(module, result): result['updates'] = commands - if module.params['update'] != 'check': - # check if creating checkpoints is possible - config = module.config.get_config() - if 'rollback-location' not in config: - warn = 'Cannot create checkpoint. Please enable this feature ' \ - 'with "configure system rollback rollback-location" ' \ - 'command. Automatic rollback will be disabled' - result['warnings'].append(warn) - result['__checkpoint__'] = False - else: - result['__checkpoint__'] = True + # check if creating checkpoints is possible + if not module.connection.rollback_enabled: + warn = 'Cannot create checkpoint. Please enable this feature ' \ + 'using the sros_rollback module. Automatic rollback ' \ + 'will be disabled' + result['warnings'].append(warn) - # create a config checkpoint prior to trying to - # configure the device - if result.get('__checkpoint__'): - module.cli(['admin rollback save']) + # send the configuration commands to the device and merge + # them with the current running config + if not module.check_mode: + module.config.load_config(commands) + result['changed'] = True - # send the configuration commands to the device and merge - # them with the current running config - if not module.check_mode: - module.config(commands) - result['changed'] = True - - # remove checkpoint from system - if result.get('__checkpoint__'): - module.cli(['admin rollback delete latest-rb']) - - if module.params['save'] and not module.check_mode: - module.config.save_config() - -def absent(module, result): - if not module.check_mode: - module.cli('write erase') - result['changed'] = True + if module.params['save']: + if not module.check_mode: + module.config.save_config() + result['changed'] = True def main(): - + """ main entry point for module execution + """ argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - match=dict(default='line', choices=['line', 'none']), - update=dict(choices=['merge', 'check'], default='merge'), - backup=dict(type='bool', default=False), config=dict(), default=dict(type='bool', default=False), - save=dict(type='bool', default=False), - state=dict(choices=['present', 'absent'], default='present') + backup=dict(type='bool', default=False), + save=dict(type='bool', default=False), ) mutually_exclusive = [('lines', 'src')] @@ -362,22 +291,16 @@ def main(): mutually_exclusive=mutually_exclusive, supports_check_mode=True) - state = module.params['state'] - - warnings = list() - check_args(module, warnings) - - result = dict(changed=False, warnings=warnings) + result = dict(changed=False, warnings=list()) if module.params['backup']: result['__backup__'] = module.config.get_config() try: - invoke(state, module, result) + run(module, result) except NetworkError: - revert_config(module) exc = get_exception() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), **exc.kwargs) module.exit_json(**result) From f5e64a8953870fc7e4933e78e0917d15592b40ad Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 23:22:00 -0400 Subject: [PATCH 247/770] roll up updates to ops_config module * 'before' and 'after' now only apply to 'lines' argument * add required_if dependencies * update doc strings * remove 'update' argument * clean up functions --- network/openswitch/ops_config.py | 80 +++++++++++--------------------- 1 file changed, 28 insertions(+), 52 deletions(-) diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index fffa22f94b8..a83bda2f764 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -108,19 +108,6 @@ required: false default: false choices: ['yes', 'no'] - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) I(replace) and I(check). When the argument is - set to I(merge), the configuration changes are merged with the current - device running configuration. When the argument is set to I(check) - the configuration updates are determined but not actually configured - on the remote device. - required: false - default: merge - choices: ['merge', 'check'] - version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -184,24 +171,15 @@ description: The full path to the backup file returned: when backup is yes type: path - sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34 -responses: - description: The set of responses from issuing the commands on the device - returned: when not check_mode - type: list - sample: ['...', '...'] + sample: /playbooks/ansible/backup/ops_config.2016-07-16@22:28:34 """ import re from ansible.module_utils.basic import get_exception from ansible.module_utils.openswitch import NetworkModule, NetworkError from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.netcli import Command def check_args(module, warnings): - if module.params['parents']: - if not module.params['lines'] or module.params['src']: - warnings.append('ignoring unnecessary argument parents') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -222,60 +200,56 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def load_backup(module): - try: - module.cli(['exit', 'config replace flash:/ansible-rollback force']) - except NetworkError: - module.fail_json(msg='unable to rollback configuration') - -def backup_config(module): - cmd = 'copy running-config flash:/ansible-rollback' - cmd = Command(cmd, prompt=re.compile('\? $'), response='\n') - module.cli(cmd) - def load_config(module, commands, result): - if not module.check_mode and module.params['update'] != 'check': + if not module.check_mode: module.config(commands) - result['changed'] = module.params['update'] != 'check' - result['updates'] = commands + result['changed'] = True def run(module, result): match = module.params['match'] replace = module.params['replace'] + path = module.params['parents'] candidate = get_candidate(module) if match != 'none': config = get_config(module, result) - configobjs = candidate.difference(config, match=match, replace=replace) + configobjs = candidate.difference(config, path=path, match=match, + replace=replace) else: - config = None configobjs = candidate.items if configobjs: commands = dumps(configobjs, 'commands').split('\n') - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] - if module.params['after']: - commands.extend(module.params['after']) + if module.params['after']: + commands.extend(module.params['after']) + + result['updates'] = commands # send the configuration commands to the device and merge # them with the current running config - load_config(module, commands, result) + if not module.check_mode: + module.config.load_config(commands) + result['changed'] = True - if module.params['save'] and not module.check_mode: - module.config.save_config() + if module.params['save']: + if not module.check_mode: + module.config.save_config() + result['changed'] = True def main(): argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - before=dict(type='list'), after=dict(type='list'), @@ -286,13 +260,10 @@ def main(): # it will be removed in a future version force=dict(default=False, type='bool'), - update=dict(choices=['merge', 'check'], default='merge'), - backup=dict(type='bool', default=False), - config=dict(), - default=dict(type='bool', default=False), save=dict(type='bool', default=False), + backup=dict(type='bool', default=False), # ops_config is only supported over Cli transport so force # the value of transport to be cli @@ -301,9 +272,14 @@ def main(): mutually_exclusive = [('lines', 'src')] + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines'])] + module = NetworkModule(argument_spec=argument_spec, connect_on_load=False, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) if module.params['force'] is True: From 42856d99497dd0df91d0364f56afbf510063d219 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 23:23:57 -0400 Subject: [PATCH 248/770] roll up updates to junos_config module * remove 'update' argument * update doc strings * reorder functions --- network/junos/junos_config.py | 84 +++++++++++++---------------------- 1 file changed, 31 insertions(+), 53 deletions(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 0e4a7efcd11..1c14965c381 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -177,30 +177,6 @@ DEFAULT_COMMENT = 'configured by junos_config' -def invoke(name, *args, **kwargs): - func = globals().get(name) - if func: - return func(*args, **kwargs) - -def run(module, result): - if module.params['rollback']: - action = 'rollback_config' - elif module.params['zeroize']: - action = 'zeroize_config' - else: - action = 'load_config' - - return invoke(action, module, result) - - -def check_args(module, warnings): - if module.params['replace'] is True: - warnings.append('The replace argument is deprecated, please use ' - 'update=replace instead. This argument will be ' - 'removed in the future') - if module.params['lines'] and module.params['update'] == 'replace': - module.fail_json(msg='config replace is only allowed when src is specified') - def guess_format(config): try: json.loads(config) @@ -219,23 +195,21 @@ def guess_format(config): return 'text' -def backup_config(module, result): - result['__backup__'] = module.config.get_config() - def load_config(module, result): - comment = module.params['comment'] - confirm = module.params['confirm'] - - update = module.params['update'] - candidate = module.params['lines'] or module.params['src'] - commit = not module.check_mode - config_format = module.params['src_format'] or guess_format(candidate) + kwargs = dict() + kwargs['comment'] = module.params['comment'] + kwargs['confirm'] = module.params['confirm'] + kwargs['commit'] = not module.check_mode - diff = module.config.load_config(candidate, update=update, comment=comment, - format=config_format, commit=commit, - confirm=confirm) + if module.params['src']: + config_format = module.params['src_format'] or guess_format(candidate) + elif module.params['lines']: + config_format = 'set' + kwargs['config_format'] = config_format + + diff = module.config.load_config(candidate, **kwargs) if diff: result['changed'] = True @@ -243,12 +217,11 @@ def load_config(module, result): def rollback_config(module, result): rollback = module.params['rollback'] - comment = module.params['comment'] - commit = not module.check_mode + kwargs = dict(comment=module.param['comment'], + commit=not module.check_mode) - diff = module.connection.rollback_config(rollback, commit=commit, - comment=comment) + diff = module.connection.rollback_config(rollback, **kwargs) if diff: result['changed'] = True @@ -259,9 +232,18 @@ def zeroize_config(module, result): module.cli.run_commands('request system zeroize') result['changed'] = True +def run(module, result): + if module.params['rollback']: + return rollback_config(module, result) + elif module.params['zeroize']: + return zeroize_config(module, result) + else: + return load_config(module, result) -def main(): +def main(): + """ main entry point for module execution + """ argument_spec = dict( lines=dict(type='list'), @@ -269,7 +251,7 @@ def main(): src_format=dict(choices=['xml', 'text', 'set', 'json']), # update operations - update=dict(default='merge', choices=['merge', 'replace']), + replace=dict(default=False, type='bool'), confirm=dict(default=0, type='int'), comment=dict(default=DEFAULT_COMMENT), @@ -278,10 +260,6 @@ def main(): rollback=dict(type='int'), zeroize=dict(default=False, type='bool'), - # this argument is deprecated in favor of setting update=replace - # and will be removed in a future version - replace=dict(default=False, type='bool'), - transport=dict(default='netconf', choices=['netconf']) ) @@ -289,17 +267,17 @@ def main(): ('rollback', 'zeroize'), ('lines', 'src'), ('src', 'zeroize'), ('src', 'rollback')] + required_if = [('replace', True, ['src'])] + module = NetworkModule(argument_spec=argument_spec, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) - warnings = list() - check_args(module, warnings) - - if module.params['replace'] is True: - module.params['update'] = 'replace' + result = dict(changed=False) - result = dict(changed=False, warnings=warnings) + if module.params['backup']: + result['__backup__'] = module.config.get_config() try: run(module, result) From 19f1bc07cbeaeec6ba64aa77d35e731fd478e557 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 11 Sep 2016 23:25:33 -0400 Subject: [PATCH 249/770] roll up updates to nxos_config module * remote 'updates' argument * clean up functions * update doc strings * add required_if dependencies --- network/nxos/nxos_config.py | 118 ++++++++++-------------------------- 1 file changed, 32 insertions(+), 86 deletions(-) diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index 33ced743b75..a8c993ec792 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -112,19 +112,6 @@ required: false default: false choices: [ "true", "false" ] - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) and I(check). When the argument is set to - I(merge), the configuration changes are merged with the current - device running configuration. When the argument is set to I(check) - the configuration updates are determined but not actually configured - on the remote device. - required: false - default: merge - choices: ['merge', 'check'] - version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -203,9 +190,14 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: always + returned: when list is specified type: list sample: ['...', '...'] +backup_path: + description: The full path to the backup file + returned: when backup is yes + type: path + sample: /playbooks/ansible/backup/nxos_config.2016-07-16@22:28:34 """ import time @@ -214,10 +206,6 @@ from ansible.module_utils.basic import get_exception def check_args(module, warnings): - if module.params['save'] and module.check_mode: - warnings.append('will not save configuration due to checkmode') - if module.params['parents'] and module.params['src']: - warnings.append('ignoring parents argument when src specified') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -232,94 +220,47 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def get_config(module, result): - defaults = module.params['defaults'] - if defaults is True: - key = '__configall__' - else: - key = '__config__' - - contents = module.params['config'] or result.get(key) - +def get_config(module): + contents = module.params['config'] if not contents: + defaults = module.params['defaults'] contents = module.config.get_config(include_defaults=defaults) - result[key] = contents - return NetworkConfig(indent=2, contents=contents) -def backup_config(module, result): - if '__config__' not in result: - result['__config__'] = module.config.get_config() - result['__backup__'] = result['__config__'] - -def load_config(module, commands, result): - if not module.check_mode: - module.config.load_config(commands) - result['changed'] = True - -def load_checkpoint(module, result): - try: - checkpoint = result['__checkpoint__'] - module.log('load checkpoint %s' % checkpoint) - module.cli(['rollback running-config checkpoint %s' % checkpoint, - 'no checkpoint %s' % checkpoint], output='text') - except KeyError: - module.fail_json(msg='unable to rollback, checkpoint not found') - except NetworkError: - exc = get_exception() - msg = 'unable to rollback configuration' - module.fail_json(msg=msg, checkpoint=checkpoint, **exc.kwargs) - def run(module, result): match = module.params['match'] replace = module.params['replace'] - update = module.params['update'] candidate = get_candidate(module) if match != 'none': - config = get_config(module, result) + config = get_config(module) path = module.params['parents'] configobjs = candidate.difference(config, path=path, match=match, replace=replace) else: - config = None configobjs = candidate.items - if module.params['backup']: - backup_config(module, result) - if configobjs: commands = dumps(configobjs, 'commands').split('\n') - if module.params['before']: - commands[:0] = module.params['before'] + if module.params['lines']: + if module.params['before']: + commands[:0] = module.params['before'] - if module.params['after']: - commands.extend(module.params['after']) + if module.params['after']: + commands.extend(module.params['after']) - result['updates'] = commands + result['updates'] = commands - # create a checkpoint of the current running config in case - # there is a problem loading the candidate config - checkpoint = 'ansible_%s' % int(time.time()) - module.cli(['checkpoint %s' % checkpoint], output='text') - result['__checkpoint__'] = checkpoint - module.log('create checkpoint %s' % checkpoint) + if not module.check_mode: + module.config.load_config(commands) - # if the update mode is set to check just return - # and do not try to load into the system - if update != 'check': - load_config(module, commands, result) + result['changed'] = True - # remove the checkpoint file used to restore the config - # in case of an error + if module.params['save']: if not module.check_mode: - module.log('remove checkpoint %s' % checkpoint) - module.cli('no checkpoint %s' % checkpoint, output='text') - - if module.params['save'] and not module.check_mode: - module.config.save_config() + module.config.save_config() result['changed'] = True def main(): @@ -327,11 +268,11 @@ def main(): """ argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - before=dict(type='list'), after=dict(type='list'), @@ -342,20 +283,23 @@ def main(): # it will be removed in a future version force=dict(default=False, type='bool'), - update=dict(choices=['merge', 'check'], default='merge'), - backup=dict(type='bool', default=False), - config=dict(), defaults=dict(type='bool', default=False), + backup=dict(type='bool', default=False), save=dict(type='bool', default=False), ) mutually_exclusive = [('lines', 'src')] + required_if = [('match', 'strict', ['lines']), + ('match', 'exact', ['lines']), + ('replace', 'block', ['lines'])] + module = NetworkModule(argument_spec=argument_spec, connect_on_load=False, mutually_exclusive=mutually_exclusive, + required_if=required_if, supports_check_mode=True) if module.params['force'] is True: @@ -366,10 +310,12 @@ def main(): result = dict(changed=False, warnings=warnings) + if module.params['backup']: + result['__backup__'] = module.config.get_config() + try: run(module, result) except NetworkError: - load_checkpoint(module, result) exc = get_exception() module.fail_json(msg=str(exc), **exc.kwargs) From 1a0e15094fc940a98f8fdc5acca3506f2ae633c0 Mon Sep 17 00:00:00 2001 From: nitzmahone Date: Sun, 11 Sep 2016 20:41:54 -0700 Subject: [PATCH 250/770] Fix win_user issue with disabled accounts/expired passwords Disabled and password-expired accounts cannot call ValidatePassword successfully fixed #4369 --- windows/win_user.ps1 | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/windows/win_user.ps1 b/windows/win_user.ps1 index 0ca11c743af..5eba6ad2a69 100644 --- a/windows/win_user.ps1 +++ b/windows/win_user.ps1 @@ -137,8 +137,16 @@ If ($state -eq 'present') { [void][system.reflection.assembly]::LoadWithPartialName('System.DirectoryServices.AccountManagement') $host_name = [System.Net.Dns]::GetHostName() $pc = New-Object -TypeName System.DirectoryServices.AccountManagement.PrincipalContext 'Machine', $host_name - # ValidateCredentials fails if PasswordExpired == 1 - If (!$pc.ValidateCredentials($username, $password)) { + + # ValidateCredentials will fail if either of these are true- just force update... + If($user_obj.AccountDisabled -or $user_obj.PasswordExpired) { + $password_match = $false + } + Else { + $password_match = $pc.ValidateCredentials($username, $password) + } + + If (-not $password_match) { $user_obj.SetPassword($password) $result.changed = $true } From 2f3fdc4975d03e8c02848b4e3f9c585636fa721a Mon Sep 17 00:00:00 2001 From: Gregor Giesen Date: Mon, 12 Sep 2016 07:39:12 +0200 Subject: [PATCH 251/770] cron: replacement for os.getlogin() (#4777) os.getlogin() returns the user logged in on the controlling terminal. However 'crontab' only looks for the login name of the process' real user id which pwd.getpwuid(os.getuid())[0] does provide. While in most cases there is no difference, the former might fail under certain circumstances (e.g. a lxc container connected by attachment without login), throwing the error 'OSError: [Errno 25] Inappropriate ioctl for device'. --- system/cron.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/cron.py b/system/cron.py index 0c17777be4e..00ac3709b90 100644 --- a/system/cron.py +++ b/system/cron.py @@ -200,6 +200,7 @@ ''' import os +import pwd import re import tempfile import platform @@ -479,7 +480,7 @@ def _read_user_execute(self): return "%s -l %s" % (pipes.quote(CRONCMD), pipes.quote(self.user)) elif platform.system() == 'HP-UX': return "%s %s %s" % (CRONCMD , '-l', pipes.quote(self.user)) - elif os.getlogin() != self.user: + elif pwd.getpwuid(os.getuid())[0] != self.user: user = '-u %s' % pipes.quote(self.user) return "%s %s %s" % (CRONCMD , user, '-l') @@ -491,7 +492,7 @@ def _write_execute(self, path): if self.user: if platform.system() in ['SunOS', 'HP-UX', 'AIX']: return "chown %s %s ; su '%s' -c '%s %s'" % (pipes.quote(self.user), pipes.quote(path), pipes.quote(self.user), CRONCMD, pipes.quote(path)) - elif os.getlogin() != self.user: + elif pwd.getpwuid(os.getuid())[0] != self.user: user = '-u %s' % pipes.quote(self.user) return "%s %s %s" % (CRONCMD , user, pipes.quote(path)) From f07e3d297f1cde26ecb1b5a8399bff63d3d2b522 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Matu=C5=A1ka?= Date: Mon, 12 Sep 2016 07:45:46 +0200 Subject: [PATCH 252/770] Force download if checksums do not match (#4262) --- network/basics/get_url.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/network/basics/get_url.py b/network/basics/get_url.py index 3ec59c2b08c..54bcedff05f 100644 --- a/network/basics/get_url.py +++ b/network/basics/get_url.py @@ -365,6 +365,11 @@ def main(): mtime = os.path.getmtime(dest) last_mod_time = datetime.datetime.utcfromtimestamp(mtime) + # If the checksum does not match we have to force the download + # because last_mod_time may be newer than on remote + if checksum_mismatch: + force = True + # download to tmpsrc tmpsrc, info = url_get(module, url, dest, use_proxy, last_mod_time, force, timeout, headers, tmp_dest) From c776932ca360342a0a073ef7b8a78f7a56d774fa Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Mon, 12 Sep 2016 07:47:45 +0200 Subject: [PATCH 253/770] Don't add included files as arguments on the command line (#4626) This means we will have to unarchive the complete archive if a single change is found. Unfortunately we cannot fix this for `unzip`, the only hope is a pure-python reimplementation. This fixes problems reported in the comments of #3810 --- files/unarchive.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index 7f5ab13b4c4..95bfe36238c 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -440,6 +440,7 @@ def is_unarchived(self): elif stat.S_ISREG(st.st_mode) and timestamp < st.st_mtime: # Add to excluded files, ignore other changes out += 'File %s is newer, excluding file\n' % path + self.excludes.append(path) continue else: if timestamp != st.st_mtime: @@ -545,12 +546,12 @@ def unarchive(self): cmd = [ self.cmd_path, '-o', self.src ] if self.opts: cmd.extend(self.opts) - if self.includes: + # NOTE: Including (changed) files as arguments is problematic (limits on command line/arguments) +# if self.includes: # NOTE: Command unzip has this strange behaviour where it expects quoted filenames to also be escaped - cmd.extend(map(shell_escape, self.includes)) - # We don't need to handle excluded files, since we simply do not include them -# if self.excludes: -# cmd.extend([ '-x' ] + self.excludes ]) +# cmd.extend(map(shell_escape, self.includes)) + if self.excludes: + cmd.extend([ '-x' ] + self.excludes) cmd.extend([ '-d', self.dest ]) rc, out, err = self.module.run_command(cmd) return dict(cmd=cmd, rc=rc, out=out, err=err) From 9bb0c498df2d270f7f90bbe0770fb864530e1f57 Mon Sep 17 00:00:00 2001 From: glovenglaven Date: Mon, 12 Sep 2016 07:45:32 -0500 Subject: [PATCH 254/770] Added Solaris support to core mount module (#4771) * Added Solaris support to the mount module. * Added checking so that if a non-standard fstab file is specified it will still work in Solaris without breaking existing functionality. * Added a check to avoid writing duplicate vfstab entries on Solaris * Added "version_added" to new boot option --- system/mount.py | 119 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 85 insertions(+), 34 deletions(-) diff --git a/system/mount.py b/system/mount.py index bd62a148bb0..0178bd32d7d 100644 --- a/system/mount.py +++ b/system/mount.py @@ -44,17 +44,19 @@ default: null opts: description: - - mount options (see fstab(5)) + - mount options (see fstab(5), or vfstab(4) on Solaris) required: false default: null dump: description: - "dump (see fstab(5)), Note that if nulled, C(state=present) will cease to work and duplicate entries will be made with subsequent runs." + - Has no effect on Solaris systems. required: false default: 0 passno: description: - "passno (see fstab(5)), Note that if nulled, C(state=present) will cease to work and duplicate entries will be made with subsequent runs." + - Deprecated on Solaris systems. required: false default: 0 state: @@ -71,7 +73,14 @@ unless you really know what you are doing. This might be useful if you need to configure mountpoints in a chroot environment. required: false - default: /etc/fstab + default: /etc/fstab (/etc/vfstab on Solaris) + boot: + version_added: 2.2 + description: + - Determines if the filesystem should be mounted on boot. Only applies to Solaris systems. + required: false + default: yes + choices: [ "yes", "no" ] author: - Ansible Core Team @@ -109,17 +118,26 @@ def _escape_fstab(v): def set_mount(module, **kwargs): """ set/change a mount point location in fstab """ - # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - args = dict( - opts = 'defaults', - dump = '0', - passno = '0', - fstab = '/etc/fstab' - ) + # solaris kwargs: name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + if get_platform() == 'SunOS': + args = dict( + opts = '-', + passno = '-', + fstab = '/etc/vfstab', + boot = 'yes' + ) + new_line = '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n' + else: + args = dict( + opts = 'defaults', + dump = '0', + passno = '0', + fstab = '/etc/fstab' + ) + new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n' args.update(kwargs) - new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n' - to_write = [] exists = False changed = False @@ -131,14 +149,17 @@ def set_mount(module, **kwargs): if line.strip().startswith('#'): to_write.append(line) continue - if len(line.split()) != 6: + if len(line.split()) != 6 and get_platform() != 'SunOS': # not sure what this is or why it is here # but it is not our fault so leave it be to_write.append(line) continue ld = {} - ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() + if get_platform() == 'SunOS': + ld['src'], dash, ld['name'], ld['fstype'], ld['passno'], ld['boot'], ld['opts'] = line.split() + else: + ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() if ld['name'] != escaped_args['name']: to_write.append(line) @@ -146,10 +167,16 @@ def set_mount(module, **kwargs): # it exists - now see if what we have is different exists = True - for t in ('src', 'fstype','opts', 'dump', 'passno'): - if ld[t] != escaped_args[t]: - changed = True - ld[t] = escaped_args[t] + if get_platform() == 'SunOS': + for t in ('src', 'fstype','passno', 'boot', 'opts'): + if ld[t] != escaped_args[t]: + changed = True + ld[t] = escaped_args[t] + else: + for t in ('src', 'fstype','opts', 'dump', 'passno'): + if ld[t] != escaped_args[t]: + changed = True + ld[t] = escaped_args[t] if changed: to_write.append(new_line % ld) @@ -169,13 +196,22 @@ def set_mount(module, **kwargs): def unset_mount(module, **kwargs): """ remove a mount point from fstab """ - # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - args = dict( - opts = 'default', - dump = '0', - passno = '0', - fstab = '/etc/fstab' - ) + # solaris kwargs: name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + if get_platform() == 'SunOS': + args = dict( + opts = '-', + passno = '-', + fstab = '/etc/vfstab', + boot = 'yes' + ) + else: + args = dict( + opts = 'default', + dump = '0', + passno = '0', + fstab = '/etc/fstab' + ) args.update(kwargs) to_write = [] @@ -188,14 +224,17 @@ def unset_mount(module, **kwargs): if line.strip().startswith('#'): to_write.append(line) continue - if len(line.split()) != 6: + if len(line.split()) != 6 and get_platform() != 'SunOS': # not sure what this is or why it is here # but it is not our fault so leave it be to_write.append(line) continue ld = {} - ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() + if get_platform() == 'SunOS': + ld['src'], dash, ld['name'], ld['fstype'], ld['passno'], ld['boot'], ld['opts'] = line.split() + else: + ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() if ld['name'] != escaped_name: to_write.append(line) @@ -213,13 +252,22 @@ def unset_mount(module, **kwargs): def mount(module, **kwargs): """ mount up a path or remount if needed """ - # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - args = dict( - opts = 'default', - dump = '0', - passno = '0', - fstab = '/etc/fstab' - ) + # solaris kwargs: name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + if get_platform() == 'SunOS': + args = dict( + opts = '-', + passno = '-', + fstab = '/etc/vfstab', + boot = 'yes' + ) + else: + args = dict( + opts = 'default', + dump = '0', + passno = '0', + fstab = '/etc/fstab' + ) args.update(kwargs) mount_bin = module.get_bin_path('mount') @@ -269,6 +317,7 @@ def main(): dump = dict(default=None), src = dict(required=False), fstype = dict(required=False), + boot = dict(default='yes', choices=['yes', 'no']), fstab = dict(default='/etc/fstab') ), supports_check_mode=True, @@ -292,7 +341,9 @@ def main(): args['opts'] = module.params['opts'] if module.params['dump'] is not None: args['dump'] = module.params['dump'] - if module.params['fstab'] is not None: + if get_platform() == 'SunOS' and module.params['fstab'] == '/etc/fstab': + args['fstab'] = '/etc/vfstab' + elif module.params['fstab'] is not None: args['fstab'] = module.params['fstab'] # if fstab file does not exist, we first need to create it. This mainly From dc7ba8f3c967c26182d64199b7fb04c98d72ef40 Mon Sep 17 00:00:00 2001 From: Antonio Murdaca Date: Mon, 12 Sep 2016 17:08:27 +0200 Subject: [PATCH 255/770] system/systemd: enable systemctl --user (#4208) Signed-off-by: Antonio Murdaca --- system/systemd.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/system/systemd.py b/system/systemd.py index fb94ba445e5..874b99528a6 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -57,6 +57,13 @@ description: - run daemon-reload before doing any other operations, to make sure systemd has read any changes. aliases: ['daemon-reload'] + user: + required: false + default: no + choices: [ "yes", "no" ] + description: + - run systemctl talking to the service manager of the calling user, rather than the service manager + of the system. notes: - One option other than name is required. requirements: @@ -231,6 +238,7 @@ def main(): enabled = dict(type='bool'), masked = dict(type='bool'), daemon_reload= dict(type='bool', default=False, aliases=['daemon-reload']), + user= dict(type='bool', default=False), ), supports_check_mode=True, required_one_of=[['state', 'enabled', 'masked', 'daemon_reload']], @@ -238,6 +246,8 @@ def main(): # initialize systemctl = module.get_bin_path('systemctl') + if module.params['user']: + systemctl = systemctl + " --user" unit = module.params['name'] rc = 0 out = err = '' From b76b3de28ef810fc09ada4e4ac76ec8f4caa68d7 Mon Sep 17 00:00:00 2001 From: John Barker Date: Mon, 12 Sep 2016 16:16:07 +0100 Subject: [PATCH 256/770] Document `backup` options To make future diffing easier, use consistent ordering --- network/eos/eos_config.py | 19 +++++++++++++++---- network/ios/ios_config.py | 4 ++-- network/nxos/nxos_config.py | 12 +++++++++++- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index a91d54653b2..9137946ca42 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # + DOCUMENTATION = """ --- module: eos_config @@ -108,6 +109,17 @@ required: false default: false choices: ['yes', 'no'] + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -270,11 +282,11 @@ def main(): """ main entry point for module execution """ argument_spec = dict( + src=dict(type='path'), + lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), - src=dict(type='path'), - before=dict(type='list'), after=dict(type='list'), @@ -285,11 +297,10 @@ def main(): # it will be removed in a future version force=dict(default=False, type='bool'), - backup=dict(type='bool', default=False), - config=dict(), defaults=dict(type='bool', default=False), + backup=dict(type='bool', default=False), save=dict(default=False, type='bool'), ) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 426c3183db0..b4bce06758f 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -63,7 +63,7 @@ a change needs to be made. This allows the playbook designer the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched - against the system + against the system. required: false default: null after: @@ -322,8 +322,8 @@ def main(): config=dict(), default=dict(type='bool', default=False), - save=dict(type='bool', default=False), backup=dict(type='bool', default=False), + save=dict(default=False, type='bool'), ) mutually_exclusive = [('lines', 'src')] diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index a8c993ec792..ab628fa7926 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -112,6 +112,17 @@ required: false default: false choices: [ "true", "false" ] + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no + choices: ['yes', 'no'] + version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -146,7 +157,6 @@ version_added: "2.2" """ - EXAMPLES = """ # Note: examples below use the following provider dict to handle # transport and authentication to the node. From 6ba5dc3188620f946f3515bf57e600bb4a8d6059 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Mon, 12 Sep 2016 11:31:40 -0500 Subject: [PATCH 257/770] Don't change to root dir in async_wrapper (#4779) The daemonizing code here is taken from an ActiveState recipe, which includes changing to / as a general best practice. While that is normally true to allow for deleting the directory that the daemon process started in, in this case it is not relevant as this is not intended to be an actual long-running daemon. Issue ansible/ansible#17466 --- utilities/logic/async_wrapper.py | 1 - 1 file changed, 1 deletion(-) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index b1adb9c5bcd..5c658d68b81 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -52,7 +52,6 @@ def daemonize_self(): sys.exit("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) # decouple from parent environment - os.chdir("/") os.setsid() os.umask(int('022', 8)) From 067167a9b3fb84ac88399650336ebec8940fad7d Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 7 Sep 2016 12:10:35 -0400 Subject: [PATCH 258/770] switched to use built in function for validation --- system/service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/system/service.py b/system/service.py index e8f50172a6e..ef1aded72c6 100644 --- a/system/service.py +++ b/system/service.py @@ -1463,9 +1463,8 @@ def main(): arguments = dict(aliases=['args'], default=''), ), supports_check_mode=True + required_one_of=[['state', 'enabled']], ) - if module.params['state'] is None and module.params['enabled'] is None: - module.fail_json(msg="Neither 'state' nor 'enabled' set") service = Service(module) From da0dac38415a0c040e449a229e8e8ff751ed5f9c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 12 Sep 2016 12:29:14 -0400 Subject: [PATCH 259/770] removed chdir / as it breaks tasks fixes #17466 --- utilities/logic/async_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 5c658d68b81..822f9dc9d6c 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -51,7 +51,7 @@ def daemonize_self(): e = sys.exc_info()[1] sys.exit("fork #1 failed: %d (%s)\n" % (e.errno, e.strerror)) - # decouple from parent environment + # decouple from parent environment (does not chdir / to keep the directory context the same as for non async tasks) os.setsid() os.umask(int('022', 8)) From e4f40d1cdda9161849098bfeac9d360fe9dd77f3 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Mon, 12 Sep 2016 19:36:14 +0200 Subject: [PATCH 260/770] Speedup git module on clone and pull (#4562) * remove redundant if submodules_updated * speed up git by reducing remote commands * run fetch only once * run ls-remote less * don't run ls-remote if one would run fetch anyhow * remove unnecessary remote_branch check in clone * kept if depth and version given * fix fetch on old git versions --- source_control/git.py | 92 +++++++++++++++++++------------------------ 1 file changed, 40 insertions(+), 52 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index 570350fffe3..5c5618403a1 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -369,18 +369,17 @@ def clone(git_path, module, repo, dest, remote, depth, version, bare, pass cmd = [ git_path, 'clone' ] - branch_or_tag = is_remote_branch(git_path, module, dest, repo, version) \ - or is_remote_tag(git_path, module, dest, repo, version) - if bare: cmd.append('--bare') else: cmd.extend([ '--origin', remote ]) - if branch_or_tag: - cmd.extend([ '--branch', version ]) - if depth and (branch_or_tag or version == 'HEAD' or refspec): - # only use depth if the remote opject is branch or tag (i.e. fetchable) - cmd.extend([ '--depth', str(depth) ]) + if depth: + if version == 'HEAD' \ + or refspec \ + or is_remote_branch(git_path, module, dest, repo, version) \ + or is_remote_tag(git_path, module, dest, repo, version): + # only use depth if the remote opject is branch or tag (i.e. fetchable) + cmd.extend([ '--depth', str(depth) ]) if reference: cmd.extend([ '--reference', str(reference) ]) cmd.extend([ repo, dest ]) @@ -594,7 +593,7 @@ def set_remote_url(git_path, module, repo, dest, remote): # for Git versions prior to 1.7.5 that lack required functionality. return remote_url is not None -def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec): +def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, git_version_used): ''' updates repo from remote sources ''' set_remote_url(git_path, module, repo, dest, remote) commands = [] @@ -630,20 +629,22 @@ def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec): # version fetch_cmd.extend(['--depth', str(depth)]) - fetch_cmd.extend([remote]) if not depth or not refspecs: # don't try to be minimalistic but do a full clone # also do this if depth is given, but version is something that can't be fetched directly if bare: refspecs = ['+refs/heads/*:refs/heads/*', '+refs/tags/*:refs/tags/*'] else: - # unlike in bare mode, there's no way to combine the - # additional refspec with the default git fetch behavior, - # so use two commands - commands.append((fetch_str, fetch_cmd)) - refspecs = ['+refs/tags/*:refs/tags/*'] + # ensure all tags are fetched + if git_version_used >= LooseVersion('1.9'): + fetch_cmd.append('--tags') + else: + # old git versions have a bug in --tags that prevents updating existing tags + commands.append((fetch_str, fetch_cmd + [remote])) + refspecs = ['+refs/tags/*:refs/tags/*'] if refspec: refspecs.append(refspec) + fetch_cmd.extend([remote]) commands.append((fetch_str, fetch_cmd + refspecs)) @@ -744,7 +745,15 @@ def set_remote_branch(git_path, module, dest, remote, version, depth): def switch_version(git_path, module, dest, remote, version, verify_commit, depth): cmd = '' - if version != 'HEAD': + if version == 'HEAD': + branch = get_head_branch(git_path, module, dest, remote) + (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, branch), cwd=dest) + if rc != 0: + module.fail_json(msg="Failed to checkout branch %s" % branch, + stdout=out, stderr=err, rc=rc) + cmd = "%s reset --hard %s" % (git_path, remote) + else: + # FIXME check for local_branch first, should have been fetched already if is_remote_branch(git_path, module, dest, remote, version): if not is_local_branch(git_path, module, dest, version): if depth: @@ -760,13 +769,6 @@ def switch_version(git_path, module, dest, remote, version, verify_commit, depth cmd = "%s reset --hard %s/%s" % (git_path, remote, version) else: cmd = "%s checkout --force %s" % (git_path, version) - else: - branch = get_head_branch(git_path, module, dest, remote) - (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, branch), cwd=dest) - if rc != 0: - module.fail_json(msg="Failed to checkout branch %s" % branch, - stdout=out, stderr=err, rc=rc) - cmd = "%s reset --hard %s" % (git_path, remote) (rc, out1, err1) = module.run_command(cmd, cwd=dest) if rc != 0: if version != 'HEAD': @@ -906,7 +908,7 @@ def main(): result.update(before=None) local_mods = False - repo_updated = None + need_fetch = True if (dest and not os.path.exists(gitconfig)) or (not dest and not allow_clone): # if there is no git configuration, do a clone operation unless: # * the user requested no clone (they just want info) @@ -922,7 +924,7 @@ def main(): module.exit_json(**result) # there's no git config, so clone clone(git_path, module, repo, dest, remote, depth, version, bare, reference, refspec, verify_commit) - repo_updated = True + need_fetch = False elif not update: # Just return having found a repo already in the dest path # this does no checking that the repo is the actual repo @@ -951,38 +953,26 @@ def main(): if remote_url_changed: result.update(remote_url_changed=True) - remote_head = get_remote_head(git_path, module, dest, version, remote, bare) - if result['before'] == remote_head: - if local_mods: - result.update(changed=True, after=remote_head, msg='Local modifications exist') + if need_fetch: + if module.check_mode: + remote_head = get_remote_head(git_path, module, dest, version, remote, bare) + result.update(changed=(result['before'] != remote_head), after=remote_head) + # FIXME: This diff should fail since the new remote_head is not fetched yet?! if module._diff: diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after']) if diff: result['diff'] = diff module.exit_json(**result) - elif version == 'HEAD': - # If the remote and local match and we're using the default of - # HEAD (It's not a real tag) then exit early - repo_updated = False - elif is_remote_tag(git_path, module, dest, repo, version): - # if the remote is a tag and we have the tag locally, exit early - if version in get_tags(git_path, module, dest): - repo_updated = False else: - # if the remote is a branch and we have the branch locally, exit early - if version in get_branches(git_path, module, dest): - repo_updated = False + fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, git_version_used) - if repo_updated is None: - if module.check_mode: - result.update(changed=(result['before']!=remote_head), after=remote_head) - if module._diff: - diff = get_diff(module, git_path, dest, repo, remote, depth, bare, result['before'], result['after']) - if diff: - result['diff'] = diff + result['after'] = get_version(module, git_path, dest) + + if result['before'] == result['after']: + if local_mods: + result.update(changed=True, after=remote_head, msg='Local modifications exist') + # no diff, since the repo didn't change module.exit_json(**result) - fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec) - repo_updated = True # switch to version specified regardless of whether # we got new revisions from the repository @@ -996,12 +986,10 @@ def main(): if submodules_updated: result.update(submodules_changed=submodules_updated) - if module.check_mode: - if submodules_updated: + if module.check_mode: result.update(changed=True, after=remote_head) module.exit_json(**result) - if submodules_updated: # Switch to version specified submodule_update(git_path, module, dest, track_submodules, force=force) From 7136bbe74c1866ecd89b4b22d68376f257f063df Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Mon, 12 Sep 2016 11:31:46 -0700 Subject: [PATCH 261/770] Addressed review comments from @privateip --- network/dnos6/dnos6_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/dnos6/dnos6_command.py b/network/dnos6/dnos6_command.py index 1d26620ad1c..f1250086121 100644 --- a/network/dnos6/dnos6_command.py +++ b/network/dnos6/dnos6_command.py @@ -1,4 +1,4 @@ -# !/usr/bin/python +#!/usr/bin/python # # This file is part of Ansible # From 432ee70da1a1899478c2cc2af48fa051c5820075 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 12 Sep 2016 11:54:58 -0700 Subject: [PATCH 262/770] Fix syntax --- system/service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/service.py b/system/service.py index ef1aded72c6..baac4fed1a6 100644 --- a/system/service.py +++ b/system/service.py @@ -1462,7 +1462,7 @@ def main(): runlevel = dict(required=False, default='default'), arguments = dict(aliases=['args'], default=''), ), - supports_check_mode=True + supports_check_mode=True, required_one_of=[['state', 'enabled']], ) From cbcb90e112cbd09cde217629651ab58140c5c5bb Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 12 Sep 2016 16:29:51 -0400 Subject: [PATCH 263/770] fixes error when calling load_config() with session keyword The session keyword is no longer needed or supported in the load_config() method for eos. This fixes an issue in eos_template where the session keyword was still being sent. --- network/eos/eos_template.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/network/eos/eos_template.py b/network/eos/eos_template.py index 59733ab5931..a5d0f38c93c 100644 --- a/network/eos/eos_template.py +++ b/network/eos/eos_template.py @@ -117,8 +117,10 @@ """ import re +import ansible.module_utils.eos + +from ansible.module_utils.network import NetworkModule from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.eos import NetworkModule def get_config(module): config = module.params.get('config') @@ -199,12 +201,8 @@ def main(): commands = filter_exit(commands) if commands: if not module.check_mode: - response = module.config.load_config(commands, - replace=replace, - session='eos_template', + response = module.config.load_config(commands, replace=replace, commit=True) - - module.cli('no configure session eos_template') result['responses'] = response result['changed'] = True From ae6992bf8c346dc9053d0fc0fcdec26d6c1b1618 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Mon, 12 Sep 2016 18:26:13 -0400 Subject: [PATCH 264/770] Handle EC2 instances with multiple network interfaces (#4766) Currently instances with multiple ENI's can't be started or stopped because sourceDestCheck is a per-interface attribute, but we use the boto global access to it (which only works when there's a single ENI). This patch handles multiple ENI's and applies the sourcedestcheck across all interfaces the same way. Fixes #3234 --- cloud/amazon/ec2.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index d6d34fbe863..5f69ab978b7 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1310,9 +1310,22 @@ def startstop_instances(module, ec2, instance_ids, state, instance_tags): for inst in res.instances: # Check "source_dest_check" attribute - if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: - inst.modify_attribute('sourceDestCheck', source_dest_check) - changed = True + try: + if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: + inst.modify_attribute('sourceDestCheck', source_dest_check) + changed = True + except boto.exception.EC2ResponseError as exc: + # instances with more than one Elastic Network Interface will + # fail, because they have the sourceDestCheck attribute defined + # per-interface + if exc.code == 'InvalidInstanceID': + for interface in inst.interfaces: + if interface.source_dest_check != source_dest_check: + ec2.modify_network_interface_attribute(interface.id, "sourceDestCheck", source_dest_check) + changed = True + else: + module.fail_json(msg='Failed to handle source_dest_check state for instance {0}, error: {1}'.format(inst.id, exc), + exception=traceback.format_exc(exc)) # Check "termination_protection" attribute if inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection: From 2632aa630f122b01f95571524ab4b64a33c63b8a Mon Sep 17 00:00:00 2001 From: Christopher Kotfila Date: Tue, 13 Sep 2016 15:07:14 -0400 Subject: [PATCH 265/770] Unpack AWS reservations while waiting to terminate (#4012) Previously calculation of the number of instances that have been terminated assumed all instances were in the first reservation returned by AWS. If this is not the case the calculated number of instances terminated never reaches the number of instances and the module always times out. By unpacking the instances we get an accurate number and the module correctly exits. --- cloud/amazon/ec2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 5f69ab978b7..3bc6e4d37a2 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1236,7 +1236,7 @@ def terminate_instances(module, ec2, instance_ids): instance_ids=terminated_instance_ids, \ filters={'instance-state-name':'terminated'}) try: - num_terminated = len(response.pop().instances) + num_terminated = sum([len(res.instances) for res in response]) except Exception as e: # got a bad response of some sort, possibly due to # stale/cached data. Wait a second and then try again From c12e90f4f14e03c9f1129854ad25ed989dcd572d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Tue, 13 Sep 2016 22:59:44 +0200 Subject: [PATCH 266/770] Fixing nxos_feature --- network/nxos/nxos_feature.py | 37 ++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index c2cac62cf4c..b487f6cae37 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -326,34 +326,39 @@ def apply_key_map(key_map, table): 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) - try: - body = body[0]['TABLE_cfcFeatureCtrlTable']['ROW_cfcFeatureCtrlTable'] - except (TypeError, IndexError): - return available_features + body = execute_show_command(command, module, command_type='cli_show_ascii') + split_body = body[0].splitlines() - for each_feature in body: - feature = each_feature['cfcFeatureCtrlName2'] - state = each_feature['cfcFeatureCtrlOpStatus2'] + 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 'enabled' in state: - state = 'enabled' + if feature and state: + if 'enabled' in state: + state = 'enabled' - if feature not in available_features.keys(): - available_features[feature] = state - else: - if (available_features[feature] == 'disabled' and - state == 'enabled'): + if feature not in available_features.keys(): 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: From bee0aeaf860c306c26bdf600f064a48e88a7b48b Mon Sep 17 00:00:00 2001 From: Aditya Marella Date: Mon, 12 Sep 2016 13:15:31 -0700 Subject: [PATCH 267/770] Docker module: add support for OomScoreAdj * docker-py param name oom_score_adj * translates to OomScoreAdj in the docker remote API * setting version_added to "2.2" --- cloud/docker/docker_container.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index b5e8313af89..1768f9629ba 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -276,6 +276,12 @@ - Whether or not to disable OOM Killer for the container. default: false required: false + oom_score_adj: + description: + - An integer value containing the score given to the container in order to tune OOM killer preferences. + default: 0 + required: false + version_added: "2.2" paused: description: - Use with the started state to pause running processes inside the container. @@ -713,6 +719,7 @@ def __init__(self, client): self.network_mode = None self.networks = None self.oom_killer = None + self.oom_score_adj = None self.paused = None self.pid_mode = None self.privileged = None @@ -909,6 +916,7 @@ def _host_config(self): mem_limit='memory', memswap_limit='memory_swap', mem_swappiness='memory_swappiness', + oom_score_adj='oom_score_adj', shm_size='shm_size', group_add='groups', devices='devices', @@ -1218,6 +1226,7 @@ def has_different_configuration(self, image): memory_swappiness=host_config.get('MemorySwappiness'), network_mode=host_config.get('NetworkMode'), oom_killer=host_config.get('OomKillDisable'), + oom_score_adj=host_config.get('OomScoreAdj'), pid_mode=host_config.get('PidMode'), privileged=host_config.get('Privileged'), expected_ports=host_config.get('PortBindings'), @@ -1329,6 +1338,7 @@ def has_different_resource_limits(self): memory=host_config.get('Memory'), memory_reservation=host_config.get('MemoryReservation'), memory_swap=host_config.get('MemorySwap'), + oom_score_adj=host_config.get('OomScoreAdj'), ) differences = [] @@ -1917,6 +1927,7 @@ def main(): network_mode=dict(type='str'), networks=dict(type='list'), oom_killer=dict(type='bool'), + oom_score_adj=dict(type='int'), paused=dict(type='bool', default=False), pid_mode=dict(type='str'), privileged=dict(type='bool', default=False), From 12a63c0717f802bae3e77cb933f811d0b422ccd9 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 08:32:14 +0200 Subject: [PATCH 268/770] Adding nxos_snmp_community --- network/nxos/nxos_snmp_contact.py | 370 ++++++++++++++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 network/nxos/nxos_snmp_contact.py diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py new file mode 100644 index 00000000000..9f0efa76d4a --- /dev/null +++ b/network/nxos/nxos_snmp_contact.py @@ -0,0 +1,370 @@ +#!/usr/bin/env python + +# Copyright 2015 Jason Edelman +# +# 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. + +DOCUMENTATION = ''' +--- +module: nxos_snmp_contact +short_description: Manages SNMP contact info. +description: + - Manages SNMP contact information. +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +notes: + - C(state=absent) removes the contact configuration if it is configured. +options: + contact: + description: + - Contact information. + required: true + state: + description: + - Manage the state of the resource. + required: true + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# ensure snmp contact is configured +- nxos_snmp_contact: + contact=Test + state=present + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"contact": "New_Test"} +existing: + description: k/v pairs of existing snmp contact + type: dict + sample: {"contact": "Test"} +end_state: + description: k/v pairs of snmp contact after module execution + returned: always + type: dict + sample: {"contact": "New_Test"} +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "present" +commands: + description: command string sent to the device + returned: always + type: string + sample: "snmp-server contact New_Test ;" +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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: + body = module.configure(commands) + except ShellError, clie: + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return body + + +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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + response = response[0].replace(command + '\n\n', '').strip() + body = [json.loads(response)] + except ValueError: + module.fail_json(msg='Command does not support JSON output', + command=command) + return body + + +def execute_show(cmds, module, command_type=None): + 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(command), + 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 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 get_snmp_contact(module): + contact = {} + contact_regex = '.*snmp-server\scontact\s(?P\S+).*' + command = 'show run snmp' + + body = execute_show_command(command, module)[0] + + try: + match_contact = re.match(contact_regex, body, re.DOTALL) + group_contact = match_contact.groupdict() + contact['contact'] = group_contact["contact"] + except AttributeError: + contact = {} + + return contact + + +def main(): + argument_spec = dict( + contact=dict(required=True, type='str'), + state=dict(choices=['absent', 'present'], + default='present') + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + contact = module.params['contact'] + state = module.params['state'] + + existing = get_snmp_contact(module) + changed = False + proposed = dict(contact=contact) + end_state = existing + commands = [] + + if state == 'absent': + if existing and existing['contact'] == contact: + commands.append('no snmp-server contact') + elif state == 'present': + if not existing or existing['contact'] != contact: + commands.append('snmp-server contact {0}'.format(contact)) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_snmp_contact(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['state'] = state + results['commands'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From 6a5ab25264261dde60da91d624a5434bf0225e09 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 08:36:09 +0200 Subject: [PATCH 269/770] Fixing docstring --- network/nxos/nxos_snmp_contact.py | 34 +++++++++++++++---------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index 9f0efa76d4a..ad2cf02bff3 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -1,25 +1,29 @@ -#!/usr/bin/env python - -# Copyright 2015 Jason Edelman +#!/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. # -# 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 +# 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. # -# http://www.apache.org/licenses/LICENSE-2.0 +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . # -# 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. DOCUMENTATION = ''' --- module: nxos_snmp_contact +version_added: "2.2" short_description: Manages SNMP contact info. description: - Manages SNMP contact information. +extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) @@ -63,11 +67,6 @@ returned: always type: dict sample: {"contact": "New_Test"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" commands: description: command string sent to the device returned: always @@ -359,7 +358,6 @@ def main(): results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['state'] = state results['commands'] = cmds results['changed'] = changed From eb31af5a47b8918093531447d5aaf1e08af83623 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 08:42:53 +0200 Subject: [PATCH 270/770] Fixing error handling --- network/nxos/nxos_snmp_contact.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index ad2cf02bff3..a090251561f 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -239,7 +239,8 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: body = module.configure(commands) - except ShellError, clie: + except ShellError: + clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) return body From 9c42d723753aa0bfe93a92ccb616eb12932718e3 Mon Sep 17 00:00:00 2001 From: John Barker Date: Wed, 14 Sep 2016 12:06:18 +0100 Subject: [PATCH 271/770] Docs improvements to _config --- network/dnos10/dnos10_config.py | 6 +++--- network/eos/eos_config.py | 19 +++++++++++-------- network/ios/ios_config.py | 10 +++++----- network/iosxr/iosxr_config.py | 12 ++++++------ network/junos/junos_config.py | 4 ++-- network/nxos/nxos_config.py | 20 ++++++++++---------- network/openswitch/ops_config.py | 13 +++++++------ network/sros/sros_config.py | 6 +++--- 8 files changed, 47 insertions(+), 43 deletions(-) diff --git a/network/dnos10/dnos10_config.py b/network/dnos10/dnos10_config.py index 8aa64c973d2..9e630ce33b2 100644 --- a/network/dnos10/dnos10_config.py +++ b/network/dnos10/dnos10_config.py @@ -35,7 +35,7 @@ section. The commands must be the exact same commands as found in the device running-config. Be sure to note the configuration command syntax as some commands are automatically modified by the - device config parser. This argument is mutually exclusive with O(src). + device config parser. This argument is mutually exclusive with I(src). required: false default: null aliases: ['commands'] @@ -53,7 +53,7 @@ or configuration template to load. The path to the source file can either be the full path on the Ansible control host or a relative path from the playbook or role root dir. This argument is mutually - exclusive with O(lines). + exclusive with I(lines). required: false default: null before: @@ -68,7 +68,7 @@ after: description: - The ordered set of commands to append to the end of the command - stack if a changed needs to be made. Just like with I(before) this + stack if a change needs to be made. Just like with I(before) this allows the playbook designer to append a set of commands to be executed after the command set. required: false diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index a91d54653b2..5b4a59ad2d1 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # + DOCUMENTATION = """ --- module: eos_config @@ -52,7 +53,7 @@ to load into the remote system. The path can either be a full system path to the configuration file if the value starts with / or relative to the root of the implemented role or playbook. - This arugment is mutually exclusive with the I(lines) and + This argument is mutually exclusive with the I(lines) and I(parents) arguments. required: false default: null @@ -69,7 +70,7 @@ after: description: - The ordered set of commands to append to the end of the command - stack if a changed needs to be made. Just like with I(before) this + stack if a change needs to be made. Just like with I(before) this allows the playbook designer to append a set of commands to be executed after the command set. required: false @@ -80,8 +81,10 @@ the set of commands against the current device config. If match is set to I(line), commands are matched line by line. If match is set to I(strict), command lines are matched with respect - to position. Finally if match is set to I(exact), command lines - must be an equal match. + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. required: false default: line choices: ['line', 'strict', 'exact', 'none'] @@ -103,7 +106,7 @@ cause the module to push the contents of I(src) into the device without first checking if already configured. - Note this argument should be considered deprecated. To achieve - the equivalient, set the match argument to none. This argument + the equivalent, set the C(match=none) which is idempotent. This argument will be removed in a future release. required: false default: false @@ -131,7 +134,7 @@ version_added: "2.2" save: description: - - The I(save) argument will instruct the module to save the + - The C(save) argument instructs the module to save the running-config to startup-config. This operation is performed after any changes are made to the current running config. If no changes are made, the configuration is still saved to the @@ -188,8 +191,8 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: when lines is specified - type: when lines is specified + returned: Only when C(lines) is specified. + type: list sample: ['...', '...'] backup_path: description: The full path to the backup file diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 426c3183db0..8b918c373b4 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -63,7 +63,7 @@ a change needs to be made. This allows the playbook designer the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched - against the system + against the system. required: false default: null after: @@ -94,7 +94,7 @@ the modified lines are pushed to the device in configuration mode. If the replace argument is set to I(block) then the entire command block is pushed to the device in configuration mode if any - line is not correct + line is not correct. required: false default: line choices: ['line', 'block'] @@ -105,7 +105,7 @@ cause the module to push the contents of I(src) into the device without first checking if already configured. - Note this argument should be considered deprecated. To achieve - the equivalient, set the match argument to none. This argument + the equivalent, set the C(match=none) which is idempotent. This argument will be removed in a future release. required: false default: false @@ -204,8 +204,8 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: always - type: when lines is defined + returned: Only when C(lines) is specified. + type: list sample: ['...', '...'] backup_path: description: The full path to the backup file diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 25ffad7c7e6..9f9e84f0ffa 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -63,13 +63,13 @@ a change needs to be made. This allows the playbook designer the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched - against the system + against the system. required: false default: null after: description: - The ordered set of commands to append to the end of the command - stack if a changed needs to be made. Just like with I(before) this + stack if a change needs to be made. Just like with I(before) this allows the playbook designer to append a set of commands to be executed after the command set. required: false @@ -94,7 +94,7 @@ the modified lines are pushed to the device in configuration mode. If the replace argument is set to I(block) then the entire command block is pushed to the device in configuration mode if any - line is not correct + line is not correct. required: false default: line choices: ['line', 'block', 'config'] @@ -105,7 +105,7 @@ cause the module to push the contents of I(src) into the device without first checking if already configured. - Note this argument should be considered deprecated. To achieve - the equivalent, set the match argument to none. This argument + the equivalent, set the C(match=none) which is idempotent. This argument will be removed in a future release. required: false default: false @@ -177,8 +177,8 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: always - type: when lines is defined + returned: Only when C(lines) is specified. + type: list sample: ['...', '...'] backup_path: description: The full path to the backup file diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 1c14965c381..2a930037dc6 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -43,7 +43,7 @@ to load into the remote system. The path can either be a full system path to the configuration file if the value starts with / or relative to the root of the implemented role or playbook. - This arugment is mutually exclusive with the I(lines) and + This argument is mutually exclusive with the I(lines) and I(parents) arguments. required: false default: null @@ -130,7 +130,7 @@ - junos-eznc notes: - This module requires the netconf system service be enabled on - the remote device being managed + the remote device being managed. """ EXAMPLES = """ diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index a8c993ec792..d478ac5925b 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -53,7 +53,7 @@ to load into the remote system. The path can either be a full system path to the configuration file if the value starts with / or relative to the root of the implemented role or playbook. - This arugment is mutually exclusive with the I(lines) and + This argument is mutually exclusive with the I(lines) and I(parents) arguments. required: false default: null @@ -70,7 +70,7 @@ after: description: - The ordered set of commands to append to the end of the command - stack if a changed needs to be made. Just like with I(before) this + stack if a change needs to be made. Just like with I(before) this allows the playbook designer to append a set of commands to be executed after the command set. required: false @@ -81,11 +81,10 @@ the set of commands against the current device config. If match is set to I(line), commands are matched line by line. If match is set to I(strict), command lines are matched with respect - to position. Finally if match is set to I(exact), command lines - must be an equal match. - - Version 2.2 added a new choice I(none). When match is set to - none, the configure is loaded into the remote device without - consulting the configuration. + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. required: false default: line choices: ['line', 'strict', 'exact', 'none'] @@ -107,7 +106,7 @@ cause the module to push the contents of I(src) into the device without first checking if already configured. - Note this argument should be considered deprecated. To achieve - the equivalient, set the match argument to none. This argument + the equivalent, set the C(match=none) which is idempotent. This argument will be removed in a future release. required: false default: false @@ -135,7 +134,7 @@ version_added: "2.2" save: description: - - The I(save) argument will instruct the module to save the + - The C(save) argument instructs the module to save the running-config to startup-config. This operation is performed after any changes are made to the current running config. If no changes are made, the configuration is still saved to the @@ -190,7 +189,7 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: when list is specified + returned: Only when C(lines) is specified. type: list sample: ['...', '...'] backup_path: @@ -321,5 +320,6 @@ def main(): module.exit_json(**result) + if __name__ == '__main__': main() diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index a83bda2f764..8c65144b079 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -52,7 +52,7 @@ to load into the remote system. The path can either be a full system path to the configuration file if the value starts with / or relative to the root of the implemented role or playbook. - This arugment is mutually exclusive with the I(lines) and + This argument is mutually exclusive with the I(lines) and I(parents) arguments. required: false default: null @@ -69,7 +69,7 @@ after: description: - The ordered set of commands to append to the end of the command - stack if a changed needs to be made. Just like with I(before) this + stack if a change needs to be made. Just like with I(before) this allows the playbook designer to append a set of commands to be executed after the command set. required: false @@ -80,8 +80,10 @@ the set of commands against the current device config. If match is set to I(line), commands are matched line by line. If match is set to I(strict), command lines are matched with respect - to position. Finally if match is set to I(exact), command lines - must be an equal match. + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. required: false default: line choices: ['line', 'strict', 'exact', 'none'] @@ -103,7 +105,7 @@ cause the module to push the contents of I(src) into the device without first checking if already configured. - Note this argument should be considered deprecated. To achieve - the equivalent, set the match argument to none. This argument + the equivalent, set the C(match=none) which is idempotent. This argument will be removed in a future release. required: false default: false @@ -304,4 +306,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/network/sros/sros_config.py b/network/sros/sros_config.py index 2ac47b83074..1ebc2297730 100644 --- a/network/sros/sros_config.py +++ b/network/sros/sros_config.py @@ -63,7 +63,7 @@ a change needs to be made. This allows the playbook designer the opportunity to perform configuration commands prior to pushing any changes without affecting how the set of commands are matched - against the system + against the system. required: false default: null after: @@ -94,7 +94,7 @@ the modified lines are pushed to the device in configuration mode. If the replace argument is set to I(block) then the entire command block is pushed to the device in configuration mode if any - line is not correct + line is not correct. required: false default: line choices: ['line', 'block'] @@ -105,7 +105,7 @@ cause the module to push the contents of I(src) into the device without first checking if already configured. - Note this argument should be considered deprecated. To achieve - the equivalient, set the match argument to none. This argument + the equivalent, set the C(match=none) which is idempotent. This argument will be removed in a future release. required: false default: false From 9794087aa76919a505e63b7ed69204150a135f32 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 14 Sep 2016 07:34:55 -0400 Subject: [PATCH 272/770] rename the argument default to defaults The argument_spec incorrectly had the argument default and it should have been defaults. This corrects the problem. --- network/ios/ios_config.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 426c3183db0..9d80cd1b657 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -142,7 +142,7 @@ required: false default: null version_added: "2.2" - default: + defaults: description: - This argument specifies whether or not to collect all defaults when getting the remote device running config. When enabled, @@ -229,7 +229,7 @@ def check_args(module, warnings): def get_config(module, result): contents = module.params['config'] if not contents: - defaults = module.params['default'] + defaults = module.params['defaults'] contents = module.config.get_config(include_defaults=defaults) return NetworkConfig(indent=1, contents=contents) @@ -320,7 +320,7 @@ def main(): force=dict(default=False, type='bool'), config=dict(), - default=dict(type='bool', default=False), + defaults=dict(type='bool', default=False), save=dict(type='bool', default=False), backup=dict(type='bool', default=False), From 1b8cf3bbd9dbccbb77a5db61ef5988deb96758d5 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 14 Sep 2016 07:36:23 -0400 Subject: [PATCH 273/770] rename arugment from default to defaults This argument_spec incorrectly named an argument default and it should have been defaults. This corrects that issue --- network/sros/sros_config.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/network/sros/sros_config.py b/network/sros/sros_config.py index 2ac47b83074..30bb0ae1bd3 100644 --- a/network/sros/sros_config.py +++ b/network/sros/sros_config.py @@ -131,7 +131,7 @@ required: false default: null version_added: "2.2" - default: + defaults: description: - This argument specifies whether or not to collect all defaults when getting the remote device running config. When enabled, @@ -140,6 +140,7 @@ required: false default: no choices: ['yes', 'no'] + aliases: ['detail'] version_added: "2.2" save: description: @@ -219,7 +220,8 @@ def sanitize_config(lines): def get_config(module, result): contents = module.params['config'] if not contents: - contents = module.config.get_config() + defaults = module.params['defaults'] + contents = module.config.get_config(detail=defaults) return NetworkConfig(device_os='sros', contents=contents) def get_candidate(module): @@ -278,7 +280,7 @@ def main(): match=dict(default='line', choices=['line', 'none']), config=dict(), - default=dict(type='bool', default=False), + defaults=dict(type='bool', default=False, aliases=['detail']), backup=dict(type='bool', default=False), save=dict(type='bool', default=False), From f1ad1d0d64da9520c3da270afca807cde1cd6869 Mon Sep 17 00:00:00 2001 From: John Barker Date: Wed, 14 Sep 2016 12:58:53 +0100 Subject: [PATCH 274/770] RETURNS doesn't support markup, so remove it --- network/eos/eos_config.py | 2 +- network/ios/ios_config.py | 2 +- network/iosxr/iosxr_config.py | 2 +- network/nxos/nxos_config.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index 5b4a59ad2d1..144333842e4 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -191,7 +191,7 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: Only when C(lines) is specified. + returned: Only when lines is specified. type: list sample: ['...', '...'] backup_path: diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 8b918c373b4..e27d079f139 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -204,7 +204,7 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: Only when C(lines) is specified. + returned: Only when lines is specified. type: list sample: ['...', '...'] backup_path: diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 9f9e84f0ffa..0dc6984fc48 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -177,7 +177,7 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: Only when C(lines) is specified. + returned: Only when lines is specified. type: list sample: ['...', '...'] backup_path: diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index d478ac5925b..08b12994a4c 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -189,7 +189,7 @@ RETURN = """ updates: description: The set of commands that will be pushed to the remote device - returned: Only when C(lines) is specified. + returned: Only when lines is specified. type: list sample: ['...', '...'] backup_path: From 53c9ba9011bbcdd56f2eba96d41beae9a1b927a2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:11:15 +0200 Subject: [PATCH 275/770] Fixing docstring and more fix --- network/nxos/nxos_snmp_contact.py | 53 ++++++++++++++++++++++++------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index a090251561f..a1adb1a6793 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -67,11 +67,11 @@ returned: always type: dict sample: {"contact": "New_Test"} -commands: - description: command string sent to the device +updates: + description: commands sent to the device returned: always - type: string - sample: "snmp-server contact New_Test ;" + type: list + sample: ["snmp-server contact New_Test"] changed: description: check to see if a change was made on the device returned: always @@ -238,12 +238,20 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: - body = module.configure(commands) + module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) - return body + 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): @@ -260,8 +268,10 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - response = response[0].replace(command + '\n\n', '').strip() - body = [json.loads(response)] + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -269,6 +279,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -276,14 +291,28 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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) + 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' + if 'show run' not in command: + command += ' | json' cmds = [command] response = execute_show(cmds, module) body = get_cli_body_ssh(command, response, module) @@ -309,7 +338,7 @@ def get_snmp_contact(module): contact_regex = '.*snmp-server\scontact\s(?P\S+).*' command = 'show run snmp' - body = execute_show_command(command, module)[0] + body = execute_show_command(command, module, command_type='cli_show_ascii')[0] try: match_contact = re.match(contact_regex, body, re.DOTALL) @@ -359,7 +388,7 @@ def main(): results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['commands'] = cmds + results['updates'] = cmds results['changed'] = changed module.exit_json(**results) From 04bf398f606b358dd6aa414e70049aaf12395fbc Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:12:42 +0200 Subject: [PATCH 276/770] Adding nxos_snmp_community --- network/nxos/nxos_snmp_community.py | 500 ++++++++++++++++++++++++++++ 1 file changed, 500 insertions(+) create mode 100644 network/nxos/nxos_snmp_community.py diff --git a/network/nxos/nxos_snmp_community.py b/network/nxos/nxos_snmp_community.py new file mode 100644 index 00000000000..9e3aa1de2ec --- /dev/null +++ b/network/nxos/nxos_snmp_community.py @@ -0,0 +1,500 @@ +#!/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_snmp_community +version_added: "2.2" +short_description: Manages SNMP community configs. +description: + - Manages SNMP community configuration. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +options: + community: + description: + - Case-sensitive community string. + required: true + access: + description: + - Access type for community. + required: false + default: null + choices: ['ro','rw'] + group: + description: + - Group to which the community belongs. + required: false + default: null + acl: + description: + - ACL name to filter snmp requests. + required: false + default: 1 + state: + description: + - Manage the state of the resource. + required: true + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# ensure snmp community is configured +- nxos_snmp_community: + community=TESTING7 + group=network-operator + state=present + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"group": "network-operator"} +existing: + description: k/v pairs of existing snmp community + type: dict + sample: {} +end_state: + description: k/v pairs of snmp community after module execution + returned: always + type: dict or null + sample: {"acl": "None", "group": "network-operator"} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server community TESTING7 group network-operator"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_snmp_groups(module): + command = 'show snmp group' + data = execute_show_command(command, module)[0] + + group_list = [] + + try: + group_table = data['TABLE_role']['ROW_role'] + for group in group_table: + group_list.append(group['role_name']) + except (KeyError, AttributeError): + return group_list + + return group_list + + +def get_snmp_community(module, find_filter=None): + command = 'show snmp community' + data = execute_show_command(command, module)[0] + + community_dict = {} + + community_map = { + 'grouporaccess': 'group', + 'aclfilter': 'acl' + } + + try: + community_table = data['TABLE_snmp_community']['ROW_snmp_community'] + for each in community_table: + community = apply_key_map(community_map, each) + key = each['community_name'] + community_dict[key] = community + except (KeyError, AttributeError): + return community_dict + + if find_filter: + find = community_dict.get(find_filter, None) + + if find_filter is None or find is None: + return {} + else: + fix_find = {} + for (key, value) in find.iteritems(): + if isinstance(value, str): + fix_find[key] = value.strip() + else: + fix_find[key] = value + return fix_find + + +def config_snmp_community(delta, community): + CMDS = { + 'group': 'snmp-server community {0} group {group}', + 'acl': 'snmp-server community {0} use-acl {acl}' + } + commands = [] + for k, v in delta.iteritems(): + cmd = CMDS.get(k).format(community, **delta) + if cmd: + commands.append(cmd) + cmd = None + return commands + + +def main(): + argument_spec = dict( + community=dict(required=True, type='str'), + access=dict(choices=['ro', 'rw']), + group=dict(type='str'), + acl=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + required_one_of=[['access', 'group']], + mutually_exclusive=[['access', 'group']], + supports_check_mode=True) + + access = module.params['access'] + group = module.params['group'] + community = module.params['community'] + acl = module.params['acl'] + state = module.params['state'] + + if access: + if access == 'ro': + group = 'network-operator' + elif access == 'rw': + group = 'network-admin' + + # group check - ensure group being configured exists on the device + configured_groups = get_snmp_groups(module) + + if group not in configured_groups: + module.fail_json(msg="group not on switch." + "please add before moving forward") + + existing = get_snmp_community(module, community) + args = dict(group=group, acl=acl) + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + changed = False + end_state = existing + commands = [] + + if state == 'absent': + if existing: + command = "no snmp-server community {0}".format(community) + commands.append(command) + cmds = flatten_list(commands) + elif state == 'present': + if delta: + command = config_snmp_community(dict(delta), community) + commands.append(command) + cmds = flatten_list(commands) + + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_snmp_community(module, community) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() From ae0e9238f2629a14af00788da5027e3ae1423e0b Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:22:08 +0200 Subject: [PATCH 277/770] Adding nxos_snmp_host --- network/nxos/nxos_snmp_host.py | 634 +++++++++++++++++++++++++++++++++ 1 file changed, 634 insertions(+) create mode 100644 network/nxos/nxos_snmp_host.py diff --git a/network/nxos/nxos_snmp_host.py b/network/nxos/nxos_snmp_host.py new file mode 100644 index 00000000000..a3bef8ffd32 --- /dev/null +++ b/network/nxos/nxos_snmp_host.py @@ -0,0 +1,634 @@ +#!/usr/bin/env python + +# Copyright 2015 Jason Edelman +# +# 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. + +DOCUMENTATION = ''' +--- +module: nxos_snmp_host +version_added: "2.2" +short_description: Manages SNMP host configuration. +description: + - Manages SNMP host configuration parameters. +extends_documentation_fragment: nxos +author: Jason Edelman (@jedelman8) +notes: + - C(state=absent) removes the host configuration if it is configured. +options: + snmp_host: + description: + - IP address of hostname of target host. + required: true + version: + description: + - SNMP version. + required: false + default: v2c + choices: ['v2c', 'v3'] + community: + description: + - Community string or v3 username. + required: false + default: null + udp: + description: + - UDP port number (0-65535). + required: false + default: null + type: + description: + - type of message to send to host. + required: false + default: traps + choices: ['trap', 'inform'] + vrf: + description: + - VRF to use to source traffic to source. + required: false + default: null + vrf_filter: + description: + - Name of VRF to filter. + required: false + default: null + src_intf: + description: + - Source interface. + required: false + default: null + state: + description: + - Manage the state of the resource. + required: true + default: present + choices: ['present','absent'] + +''' + +EXAMPLES = ''' +# ensure snmp host is configured +- nxos_snmp_host: + snmp_host=3.3.3.3 + community=TESTING + state=present + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"community": "TESTING", "snmp_host": "3.3.3.3", + "snmp_type": "trap", "version": "v2c", "vrf_filter": "one_more_vrf"} +existing: + description: k/v pairs of existing snmp host + type: dict + sample: {"community": "TESTING", "snmp_type": "trap", + "udp": "162", "v3": "noauth", "version": "v2c", + "vrf": "test_vrf", "vrf_filter": ["test_vrf", + "another_test_vrf"]} +end_state: + description: k/v pairs of switchport after module execution + returned: always + type: dict or null + sample: {"community": "TESTING", "snmp_type": "trap", + "udp": "162", "v3": "noauth", "version": "v2c", + "vrf": "test_vrf", "vrf_filter": ["test_vrf", + "another_test_vrf", "one_more_vrf"]} +updates: + description: commands sent to the device + returned: always + type: list + sample: ["snmp-server host 3.3.3.3 filter-vrf another_test_vrf"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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_snmp_host(host, module): + command = 'show snmp host' + body = execute_show_command(command, module) + + host_map = { + 'port': 'udp', + 'version': 'version', + 'level': 'v3', + 'type': 'snmp_type', + 'secname': 'community' + } + + resource = {} + + if body: + try: + resource_table = body[0]['TABLE_host']['ROW_host'] + + if isinstance(resource_table, dict): + resource_table = [resource_table] + + for each in resource_table: + key = str(each['host']) + src = each.get('src_intf', None) + host_resource = apply_key_map(host_map, each) + + if src: + host_resource['src_intf'] = src.split(':')[1].strip() + + vrf_filt = each.get('TABLE_vrf_filters', None) + if vrf_filt: + vrf_filter = vrf_filt['ROW_vrf_filters']['vrf_filter'].split(':')[1].split(',') + filters = [vrf.strip() for vrf in vrf_filter] + host_resource['vrf_filter'] = filters + + vrf = each.get('vrf', None) + if vrf: + host_resource['vrf'] = vrf.split(':')[1].strip() + resource[key] = host_resource + + except (KeyError, AttributeError, TypeError): + return resource + + find = resource.get(host, None) + + if find: + fix_find = {} + for (key, value) in find.iteritems(): + if isinstance(value, str): + fix_find[key] = value.strip() + else: + fix_find[key] = value + return fix_find + else: + return {} + else: + return {} + + +def remove_snmp_host(host, existing): + commands = [] + if existing['version'] == 'v3': + existing['version'] = '3' + command = 'no snmp-server host {0} {snmp_type} version \ + {version} {v3} {community}'.format(host, **existing) + + elif existing['version'] == 'v2c': + existing['version'] = '2c' + command = 'no snmp-server host {0} {snmp_type} version \ + {version} {community}'.format(host, **existing) + + if command: + commands.append(command) + return commands + + +def config_snmp_host(delta, proposed, existing, module): + commands = [] + command_builder = [] + host = proposed['snmp_host'] + cmd = 'snmp-server host {0}'.format(proposed['snmp_host']) + + snmp_type = delta.get('snmp_type', None) + version = delta.get('version', None) + ver = delta.get('v3', None) + community = delta.get('community', None) + + command_builder.append(cmd) + if any([snmp_type, version, ver, community]): + type_string = snmp_type or existing.get('type') + if type_string: + command_builder.append(type_string) + + version = version or existing.get('version') + if version: + if version == 'v2c': + vn = '2c' + elif version == 'v3': + vn = '3' + + version_string = 'version {0}'.format(vn) + command_builder.append(version_string) + + if ver: + ver_string = ver or existing.get('v3') + command_builder.append(ver_string) + + if community: + community_string = community or existing.get('community') + command_builder.append(community_string) + + cmd = ' '.join(command_builder) + + commands.append(cmd) + + CMDS = { + 'vrf_filter': 'snmp-server host {0} filter-vrf {vrf_filter}', + 'vrf': 'snmp-server host {0} use-vrf {vrf}', + 'udp': 'snmp-server host {0} udp-port {udp}', + 'src_intf': 'snmp-server host {0} source-interface {src_intf}' + } + + for key, value in delta.iteritems(): + if key in ['vrf_filter', 'vrf', 'udp', 'src_intf']: + command = CMDS.get(key, None) + if command: + cmd = command.format(host, **delta) + commands.append(cmd) + cmd = None + return commands + + +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 main(): + argument_spec = dict( + snmp_host=dict(required=True, type='str'), + community=dict(type='str'), + udp=dict(type='str'), + version=dict(choices=['v2c', 'v3'], default='v2c'), + src_intf=dict(type='str'), + v3=dict(choices=['noauth', 'auth', 'priv']), + vrf_filter=dict(type='str'), + vrf=dict(type='str'), + snmp_type=dict(choices=['trap', 'inform'], default='trap'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + + snmp_host = module.params['snmp_host'] + community = module.params['community'] + udp = module.params['udp'] + version = module.params['version'] + src_intf = module.params['src_intf'] + v3 = module.params['v3'] + vrf_filter = module.params['vrf_filter'] + vrf = module.params['vrf'] + snmp_type = module.params['snmp_type'] + + state = module.params['state'] + + if snmp_type == 'inform' and version != 'v3': + module.fail_json(msg='inform requires snmp v3') + + if version == 'v2c' and v3: + module.fail_json(msg='param: "v3" should not be used when ' + 'using version v2c') + + if not any([vrf_filter, vrf, udp, src_intf]): + if not all([snmp_type, version, community]): + module.fail_json(msg='when not configuring options like ' + 'vrf_filter, vrf, udp, and src_intf,' + 'the following params are required: ' + 'type, version, community') + + if version == 'v3' and v3 is None: + module.fail_json(msg='when using version=v3, the param v3 ' + '(options: auth, noauth, priv) is also required') + + existing = get_snmp_host(snmp_host, module) + + # existing returns the list of vrfs configured for a given host + # checking to see if the proposed is in the list + store = existing.get('vrf_filter', None) + if existing and store: + if vrf_filter not in existing['vrf_filter']: + existing['vrf_filter'] = None + else: + existing['vrf_filter'] = vrf_filter + + args = dict( + community=community, + snmp_host=snmp_host, + udp=udp, + version=version, + src_intf=src_intf, + vrf_filter=vrf_filter, + v3=v3, + vrf=vrf, + snmp_type=snmp_type + ) + + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + changed = False + commands = [] + end_state = existing + + if state == 'absent': + if existing: + command = remove_snmp_host(snmp_host, existing) + commands.append(command) + elif state == 'present': + if delta: + command = config_snmp_host(delta, proposed, existing, module) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_snmp_host(snmp_host, module) + + if store: + existing['vrf_filter'] = store + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == "__main__": + main() \ No newline at end of file From a01e686a2186d018e3c63d7772e9075313cac7ed Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:33:30 +0200 Subject: [PATCH 278/770] Fixing docstring --- network/nxos/nxos_snmp_host.py | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/network/nxos/nxos_snmp_host.py b/network/nxos/nxos_snmp_host.py index a3bef8ffd32..16c6f025469 100644 --- a/network/nxos/nxos_snmp_host.py +++ b/network/nxos/nxos_snmp_host.py @@ -1,18 +1,21 @@ -#!/usr/bin/env python - -# Copyright 2015 Jason Edelman +#!/usr/bin/python +# +# This file is part of Ansible # -# 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 +# 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. # -# http://www.apache.org/licenses/LICENSE-2.0 +# 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. # -# 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. +# You should have received a copy of the GNU General Public License +# along with Ansible. If not, see . +# + DOCUMENTATION = ''' --- @@ -22,7 +25,9 @@ description: - Manages SNMP host configuration parameters. extends_documentation_fragment: nxos -author: Jason Edelman (@jedelman8) +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) notes: - C(state=absent) removes the host configuration if it is configured. options: From 4180d10684576fbf285fce2fe0f59dd67425fbc7 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:40:14 +0200 Subject: [PATCH 279/770] Adding nxos_snapshot --- network/nxos/nxos_snapshot.py | 669 ++++++++++++++++++++++++++++++++++ 1 file changed, 669 insertions(+) create mode 100644 network/nxos/nxos_snapshot.py diff --git a/network/nxos/nxos_snapshot.py b/network/nxos/nxos_snapshot.py new file mode 100644 index 00000000000..7b7d81781f0 --- /dev/null +++ b/network/nxos/nxos_snapshot.py @@ -0,0 +1,669 @@ +#!/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_snapshot +version_added: "2.2" +short_description: Manage snapshots of the running states of selected features. +description: + - Create snapshots of the running states of selected features, add + new show commands for snapshot creation, delete and compare + existing snapshots. +extends_documentation_fragment: nxos +author: + - Gabriele Gerbino (@GGabriele) +notes: + - C(transpot=cli) may cause timeout errors. + - The C(element_key1) and C(element_key2) parameter specify the tags used + to distinguish among row entries. In most cases, only the element_key1 + parameter needs to specified to be able to distinguish among row entries. + - C(action=compare) will always store a comparison report on a local file. +options: + action: + description: + - Define what snapshot action the module would perform. + required: true + choices: ['create','add','compare','delete'] + snapshot_name: + description: + - Snapshot name, to be used when C(action=create) + or C(action=delete). + required: false + default: null + description: + description: + - Snapshot description to be used when C(action=create). + required: false + default: null + snapshot1: + description: + - First snapshot to be used when C(action=compare). + required: false + default: null + snapshot2: + description: + - Second snapshot to be used when C(action=compare). + required: false + default: null + comparison_results_file: + description: + - Name of the file where snapshots comparison will be store. + required: false + default: null + compare_option: + description: + - Snapshot options to be used when C(action=compare). + required: false + default: null + choices: ['summary','ipv4routes','ipv6routes'] + section: + description: + - Used to name the show command output, to be used + when C(action=add). + required: false + default: null + show_command: + description: + - Specify a new show command, to be used when C(action=add). + required: false + default: null + row_id: + description: + - Specifies the tag of each row entry of the show command's + XML output, to be used when C(action=add). + required: false + default: null + element_key1: + description: + - Specify the tags used to distinguish among row entries, + to be used when C(action=add). + required: false + default: null + element_key2: + description: + - Specify the tags used to distinguish among row entries, + to be used when C(action=add). + required: false + default: null + save_snapshot_locally: + description: + - Specify to locally store a new created snapshot, + to be used when C(action=create). + required: false + default: false + choices: ['true','false'] + path: + description: + - Specify the path of the file where new created snapshot or + snapshots comparison will be stored, to be used when + C(action=create) and C(save_snapshot_locally=true) or + C(action=compare). + required: false + default: './' +''' + +EXAMPLES = ''' +# Create a snapshot and store it locally +- nxos_snapshot: + action: create + snapshot_name: test_snapshot + description: Done with Ansible + save_snapshot_locally: true + path: /home/user/snapshots/ + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" + +# Delete a snapshot +- nxos_snapshot: + action: delete + snapshot_name: test_snapshot + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" + +# Delete all existing snapshots +- nxos_snapshot: + action: delete_all + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" + +# Add a show command for snapshots creation +- nxos_snapshot: + section: myshow + show_command: show ip interface brief + row_id: ROW_intf + element_key1: intf-name + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" + +# Compare two snapshots +- nxos_snapshot: + action: compare + snapshot1: pre_snapshot + snapshot2: post_snapshot + comparison_results_file: compare_snapshots.txt + compare_option: summary + path: '../snapshot_reports/' + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +''' + +RETURN = ''' +existing_snapshots: + description: list of existing snapshots. + returned: verbose mode + type: list + sample: [{"date": "Tue Sep 13 10:58:08 2016", + "description": "First snapshot", "name": "first_snap"}, + {"date": "Tue Sep 13 10:27:31 2016", "description": "Pre-snapshot", + "name": "pre_snapshot"}] +final_snapshots: + description: list of final snapshots. + returned: verbose mode + type: list + sample: [{"date": "Tue Sep 13 10:58:08 2016", + "description": "First snapshot", "name": "first_snap"}, + {"date": "Tue Sep 13 10:27:31 2016", "description": "Pre-snapshot", + "name": "pre_snapshot"}, + {"date": "Tue Sep 13 10:37:50 2016", "description": "Post-snapshot", + "name": "post_snapshot"}] +report_file: + description: name of the file where the new snapshot or snapshots + comparison have been stored. + returned: verbose mode + type: string + sample: "/home/gabriele/Desktop/ntc-ansible/ansible_snapshot" +updates: + description: commands sent to the device + returned: verbose mode + type: list + sample: ["snapshot create post_snapshot Post-snapshot"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import os +# 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_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, output=command_type) + 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_ascii'): + cmds = [command] + if module.params['transport'] == 'cli': + body = execute_show(cmds, module) + elif module.params['transport'] == 'nxapi': + body = execute_show(cmds, module, command_type=command_type) + + return body + + +def get_existing(module): + existing = [] + command = 'show snapshots' + + body = execute_show_command(command, module)[0] + if body: + split_body = body.splitlines() + snapshot_regex = ('(?P\S+)\s+(?P\w+\s+\w+\s+\d+\s+\d+' + ':\d+:\d+\s+\d+)\s+(?P.*)') + for snapshot in split_body: + temp = {} + try: + match_snapshot = re.match(snapshot_regex, snapshot, re.DOTALL) + snapshot_group = match_snapshot.groupdict() + temp['name'] = snapshot_group['name'] + temp['date'] = snapshot_group['date'] + temp['description'] = snapshot_group['description'] + existing.append(temp) + except AttributeError: + pass + + return existing + + +def action_create(module, existing_snapshots): + commands = list() + exist = False + for snapshot in existing_snapshots: + if module.params['snapshot_name'] == snapshot['name']: + exist = True + + if exist is False: + commands.append('snapshot create {0} {1}'.format( + module.params['snapshot_name'], module.params['description'])) + + return commands + + +def action_add(module, existing_snapshots): + commands = list() + command = 'show snapshot sections' + sections = [] + body = execute_show_command(command, module)[0] + + if body: + section_regex = '.*\[(?P
\S+)\].*' + split_body = body.split('\n\n') + for section in split_body: + temp = {} + for line in section.splitlines(): + try: + match_section = re.match(section_regex, section, re.DOTALL) + temp['section'] = match_section.groupdict()['section'] + except (AttributeError, KeyError): + pass + + if 'show command' in line: + temp['show_command'] = line.split('show command: ')[1] + elif 'row id' in line: + temp['row_id'] = line.split('row id: ')[1] + elif 'key1' in line: + temp['element_key1'] = line.split('key1: ')[1] + elif 'key2' in line: + temp['element_key2'] = line.split('key2: ')[1] + + if temp: + sections.append(temp) + + proposed = { + 'section': module.params['section'], + 'show_command': module.params['show_command'], + 'row_id': module.params['row_id'], + 'element_key1': module.params['element_key1'], + 'element_key2': module.params['element_key2'] or '-', + } + + if proposed not in sections: + if module.params['element_key2']: + commands.append('snapshot section add {0} "{1}" {2} {3} {4}'.format( + module.params['section'], module.params['show_command'], + module.params['row_id'], module.params['element_key1'], + module.params['element_key2'])) + else: + commands.append('snapshot section add {0} "{1}" {2} {3}'.format( + module.params['section'], module.params['show_command'], + module.params['row_id'], module.params['element_key1'])) + + return commands + + +def action_compare(module, existing_snapshots): + command = 'show snapshot compare {0} {1}'.format( + module.params['snapshot1'], module.params['snapshot2']) + + if module.params['compare_option']: + command += ' {0}'.format(module.params['compare_option']) + + body = execute_show_command(command, module)[0] + return body + + +def action_delete(module, existing_snapshots): + commands = list() + + exist = False + for snapshot in existing_snapshots: + if module.params['snapshot_name'] == snapshot['name']: + exist = True + + if exist: + commands.append('snapshot delete {0}'.format( + module.params['snapshot_name'])) + + return commands + + +def action_delete_all(module, existing_snapshots): + commands = list() + if existing_snapshots: + commands.append('snapshot delete all') + return commands + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +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_snapshot(module): + command = 'show snapshot dump {0}'.format(module.params['snapshot_name']) + body = execute_show_command(command, module)[0] + return body + + +def write_on_file(content, filename, module): + path = module.params['path'] + if path[-1] != '/': + path += '/' + filepath = '{0}{1}'.format(path, filename) + try: + with open(filepath, 'w') as report: + report.write(content) + except: + module.fail_json(msg="Error while writing on file.") + + return filepath + +def main(): + argument_spec = dict( + action=dict(required=True, choices=['create', 'add', + 'compare', 'delete', + 'delete_all']), + snapshot_name=dict(required=False, type='str'), + description=dict(required=False, type='str'), + snapshot1=dict(required=False, type='str'), + snapshot2=dict(required=False, type='str'), + compare_option=dict(required=False, + choices=['summary', 'ipv4routes', 'ipv6routes']), + comparison_results_file=dict(required=False, type='str'), + section=dict(required=False, type='str'), + show_command=dict(required=False, type='str'), + row_id=dict(required=False, type='str'), + element_key1=dict(required=False, type='str'), + element_key2=dict(required=False, type='str'), + save_snapshot_locally=dict(required=False, type='bool', + default=False), + path=dict(required=False, type='str', default='./') + ) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['delete_all', + 'delete_snapshot']], + supports_check_mode=True) + + action = module.params['action'] + comparison_results_file = module.params['comparison_results_file'] + + CREATE_PARAMS = ['snapshot_name', 'description'] + ADD_PARAMS = ['section', 'show_command', 'row_id', 'element_key1'] + COMPARE_PARAMS = ['snapshot1', 'snapshot2', 'comparison_results_file'] + + if not os.path.isdir(module.params['path']): + module.fail_json(msg='{0} is not a valid directory name.'.format( + module.params['path'])) + + if action == 'create': + for param in CREATE_PARAMS: + if not module.params[param]: + module.fail_json(msg='snapshot_name and description are ' + 'required when action=create') + elif action == 'add': + for param in ADD_PARAMS: + if not module.params[param]: + module.fail_json(msg='section, show_command, row_id ' + 'and element_key1 are required ' + 'when action=add') + elif action == 'compare': + for param in COMPARE_PARAMS: + if not module.params[param]: + module.fail_json(msg='snapshot1 and snapshot2 are required ' + 'when action=create') + elif action == 'delete' and not module.params['snapshot_name']: + module.fail_json(msg='snapshot_name is required when action=delete') + + existing_snapshots = invoke('get_existing', module) + final_snapshots = existing_snapshots + changed = False + + action_results = invoke('action_%s' % action, module, existing_snapshots) + + result = {} + written_file = '' + if module.check_mode and action != 'compare': + module.exit_json(changed=True, commands=action_results) + else: + if action == 'compare': + written_file = write_on_file(action_results, + module.params['comparison_results_file'], + module) + result['updates'] = [] + else: + if action_results: + execute_config_command(action_results, module) + changed = True + final_snapshots = invoke('get_existing', module) + result['updates'] = action_results + + if (action == 'create' and + module.params['save_snapshot_locally']): + snapshot = get_snapshot(module) + written_file = write_on_file(snapshot, + module.params['snapshot_name'], module) + + result['connected'] = module.connected + result['changed'] = changed + if module._verbosity > 0: + end_state = invoke('get_existing', module) + result['final_snapshots'] = final_snapshots + result['existing_snapshots'] = existing_snapshots + if written_file: + result['report_file'] = written_file + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 9bef6f6597b58e41cb5e5ea61dd53e25dc10f95f Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:52:37 +0200 Subject: [PATCH 280/770] Fixing file writing format --- network/nxos/nxos_snapshot.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_snapshot.py b/network/nxos/nxos_snapshot.py index 7b7d81781f0..ed113c6f700 100644 --- a/network/nxos/nxos_snapshot.py +++ b/network/nxos/nxos_snapshot.py @@ -561,8 +561,9 @@ def write_on_file(content, filename, module): path += '/' filepath = '{0}{1}'.format(path, filename) try: - with open(filepath, 'w') as report: - report.write(content) + report = open(filepath, 'w') + report.write(content) + report.close() except: module.fail_json(msg="Error while writing on file.") From 28adc331c90b87ddda219f3d03abd6a9eece6bce Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 15:56:14 +0200 Subject: [PATCH 281/770] Removing indentation --- network/nxos/nxos_snapshot.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_snapshot.py b/network/nxos/nxos_snapshot.py index ed113c6f700..54a4ae965b4 100644 --- a/network/nxos/nxos_snapshot.py +++ b/network/nxos/nxos_snapshot.py @@ -562,8 +562,8 @@ def write_on_file(content, filename, module): filepath = '{0}{1}'.format(path, filename) try: report = open(filepath, 'w') - report.write(content) - report.close() + report.write(content) + report.close() except: module.fail_json(msg="Error while writing on file.") From 06bb1611f6b97b89f225828f06df974691b6a68e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 16:01:12 +0200 Subject: [PATCH 282/770] Removing indentations --- network/nxos/nxos_snapshot.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_snapshot.py b/network/nxos/nxos_snapshot.py index 54a4ae965b4..f99c2ed294d 100644 --- a/network/nxos/nxos_snapshot.py +++ b/network/nxos/nxos_snapshot.py @@ -561,9 +561,9 @@ def write_on_file(content, filename, module): path += '/' filepath = '{0}{1}'.format(path, filename) try: - report = open(filepath, 'w') - report.write(content) - report.close() + report = open(filepath, 'w') + report.write(content) + report.close() except: module.fail_json(msg="Error while writing on file.") From eb48d9607c1cb458fd1df00babf39aaa5d00b1d4 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 16:05:15 +0200 Subject: [PATCH 283/770] Adding nxos_gir --- network/nxos/nxos_gir.py | 507 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 507 insertions(+) create mode 100644 network/nxos/nxos_gir.py diff --git a/network/nxos/nxos_gir.py b/network/nxos/nxos_gir.py new file mode 100644 index 00000000000..1359547f161 --- /dev/null +++ b/network/nxos/nxos_gir.py @@ -0,0 +1,507 @@ +#!/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_gir +version_added: "2.2" +short_description: Trigger a graceful removal or insertion (GIR) of the switch. +description: + - Trigger a graceful removal or insertion (GIR) of the switch. +extends_documentation_fragment: nxos +author: + - Gabriele Gerbino (@GGabriele) +notes: + - C(state) has effect only in combination with + C(system_mode_maintenance_timeout) or + C(system_mode_maintenance_on_reload_reset_reason). + - Using C(system_mode_maintenance) and + C(system_mode_maintenance_dont_generate_profile) would make the module + fail, but the system mode will be triggered anyway. +options: + system_mode_maintenance: + description: + - When C(system_mode_maintenance=true) it puts all enabled + protocols in maintenance mode (using the isolate command). + When C(system_mode_maintenance=false) it puts all enabled + protocols in normal mode (using the no isolate command). + required: false + default: null + choices: ['true','false'] + system_mode_maintenance_dont_generate_profile: + description: + - When C(system_mode_maintenance_dont_generate_profile=true) it + prevents the dynamic searching of enabled protocols and executes + commands configured in a maintenance-mode profile. + Use this option if you want the system to use a maintenance-mode + profile that you have created. + When C(system_mode_maintenance_dont_generate_profile=false) it + prevents the dynamic searching of enabled protocols and executes + commands configured in a normal-mode profile. Use this option if + you want the system to use a normal-mode profile that + you have created. + required: false + default: null + choices: ['true','false'] + system_mode_maintenance_timeout: + description: + - Keeps the switch in maintenance mode for a specified + number of minutes. Range is 5-65535. + required: false + default: null + system_mode_maintenance_shutdown: + description: + - Shuts down all protocols, vPC domains, and interfaces except + the management interface (using the shutdown command). + This option is disruptive while C(system_mode_maintenance) + (which uses the isolate command) is not. + required: false + default: null + choices: ['true','false'] + system_mode_maintenance_on_reload_reset_reason: + description: + - Boots the switch into maintenance mode automatically in the + event of a specified system crash. + required: false + default: null + choices: ['hw_error','svc_failure','kern_failure','wdog_timeout', + 'fatal_error','lc_failure','match_any','manual_reload'] + state: + description: + - Specify desired state of the resource. + required: true + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# Trigger system maintenance mode +- nxos_gir: + system_mode_maintenance: true + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Trigger system normal mode +- nxos_gir: + system_mode_maintenance: false + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Configure on-reload reset-reason for maintenance mode +- nxos_gir: + system_mode_maintenance_on_reload_reset_reason: manual_reload + state: present + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Add on-reload reset-reason for maintenance mode +- nxos_gir: + system_mode_maintenance_on_reload_reset_reason: hw_error + state: present + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Remove on-reload reset-reason for maintenance mode +- nxos_gir: + system_mode_maintenance_on_reload_reset_reason: manual_reload + state: absent + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Set timeout for maintenance mode +- nxos_gir: + system_mode_maintenance_timeout: 30 + state: present + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Remove timeout for maintenance mode +- nxos_gir: + system_mode_maintenance_timeout: 30 + state: absent + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +''' + +RETURN = ''' +final_system_mode: + description: describe the last system mode + returned: verbose mode + type: string + sample: normal +updates: + description: commands sent to the device + returned: verbose mode + type: list + sample: ["terminal dont-ask", "system mode maintenance timeout 10"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +# 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_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, output=command_type) + 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_ascii'): + cmds = [command] + if module.params['transport'] == 'cli': + body = execute_show(cmds, module) + elif module.params['transport'] == 'nxapi': + body = execute_show(cmds, module, command_type=command_type) + + return body + + +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_system_mode(module): + command = 'show system mode' + body = execute_show_command(command, module)[0] + if 'normal' in body.lower(): + mode = 'normal' + else: + mode = 'maintenance' + return mode + + +def get_maintenance_timeout(module): + command = 'show maintenance timeout' + body = execute_show_command(command, module)[0] + timeout = body.split()[4] + return timeout + + +def get_reset_reasons(module): + command = 'show maintenance on-reload reset-reasons' + body = execute_show_command(command, module)[0] + return body + + +def get_commands(module, state, mode): + commands = list() + system_mode = '' + if module.params['system_mode_maintenance'] is True and mode == 'normal': + commands.append('system mode maintenance') + elif (module.params['system_mode_maintenance'] is False and + mode == 'maintenance'): + commands.append('no system mode maintenance') + + elif (module.params[ + 'system_mode_maintenance_dont_generate_profile'] is True and + mode == 'normal'): + commands.append('system mode maintenance dont-generate-profile') + elif (module.params[ + 'system_mode_maintenance_dont_generate_profile'] is False and + mode == 'maintenance'): + commands.append('no system mode maintenance dont-generate-profile') + + elif module.params['system_mode_maintenance_timeout']: + timeout = get_maintenance_timeout(module) + if (state == 'present' and + timeout != module.params['system_mode_maintenance_timeout']): + commands.append('system mode maintenance timeout {0}'.format( + module.params['system_mode_maintenance_timeout'])) + elif (state == 'absent' and + timeout == module.params['system_mode_maintenance_timeout']): + commands.append('no system mode maintenance timeout {0}'.format( + module.params['system_mode_maintenance_timeout'])) + + elif module.params['system_mode_maintenance_shutdown'] is True: + commands.append('system mode maintenance shutdown') + + elif module.params['system_mode_maintenance_on_reload_reset_reason']: + reset_reasons = get_reset_reasons(module) + if (state == 'present' and + module.params[ + 'system_mode_maintenance_on_reload_reset_reason'].lower() not + in reset_reasons.lower()): + commands.append('system mode maintenance on-reload ' + 'reset-reason {0}'.format( + module.params[ + 'system_mode_maintenance_on_reload_reset_reason'])) + elif (state == 'absent' and + module.params[ + 'system_mode_maintenance_on_reload_reset_reason'].lower() in + reset_reasons.lower()): + commands.append('no system mode maintenance on-reload ' + 'reset-reason {0}'.format( + module.params[ + 'system_mode_maintenance_on_reload_reset_reason'])) + + if commands: + commands.insert(0, 'terminal dont-ask') + return commands + + +def main(): + argument_spec = dict( + system_mode_maintenance=dict(required=False, type='bool'), + system_mode_maintenance_dont_generate_profile=dict(required=False, + type='bool'), + system_mode_maintenance_timeout=dict(required=False, type='str'), + system_mode_maintenance_shutdown=dict(required=False, type='bool'), + system_mode_maintenance_on_reload_reset_reason=dict(required=False, + choices=['hw_error','svc_failure','kern_failure', + 'wdog_timeout','fatal_error','lc_failure', + 'match_any','manual_reload']), + state=dict(choices=['absent', 'present', 'default'], + default='present', required=False) + ) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[[ + 'system_mode_maintenance', + 'system_mode_maintenance_dont_generate_profile', + 'system_mode_maintenance_timeout', + 'system_mode_maintenance_shutdown', + 'system_mode_maintenance_on_reload_reset_reason' + ]], + required_one_of=[[ + 'system_mode_maintenance', + 'system_mode_maintenance_dont_generate_profile', + 'system_mode_maintenance_timeout', + 'system_mode_maintenance_shutdown', + 'system_mode_maintenance_on_reload_reset_reason' + ]], + supports_check_mode=True) + + state = module.params['state'] + mode = get_system_mode(module) + commands = get_commands(module, state, mode) + changed = False + if commands: + if module.check_mode: + module.exit_json(changed=True, commands=commands) + else: + execute_config_command(commands, module) + changed = True + + result = {} + result['connected'] = module.connected + result['changed'] = changed + if module._verbosity > 0: + final_system_mode = get_system_mode(module) + result['final_system_mode'] = final_system_mode + result['updates'] = commands + + module.exit_json(**result) + + +if __name__ == '__main__': + main() \ No newline at end of file From efec74453967bbfa521ae54ebbbad7fe8d414891 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 14 Sep 2016 10:15:48 -0400 Subject: [PATCH 284/770] fixed incorrect example --- utilities/logic/include_role.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 6b9ccf265cc..cdb29738d43 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -58,7 +58,7 @@ - name: Run tasks/other.yml instead of 'main' include_role: - role: myrole + name: myrole tasks_from: other - name: Pass variables to role From dd0e816f56a6ee5b8d2465ba8b63c016720f5fc8 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 16:17:35 +0200 Subject: [PATCH 285/770] Adding nxos_gir_profile_management --- network/nxos/nxos_gir_profile_management.py | 379 ++++++++++++++++++++ 1 file changed, 379 insertions(+) create mode 100644 network/nxos/nxos_gir_profile_management.py diff --git a/network/nxos/nxos_gir_profile_management.py b/network/nxos/nxos_gir_profile_management.py new file mode 100644 index 00000000000..ef99c708757 --- /dev/null +++ b/network/nxos/nxos_gir_profile_management.py @@ -0,0 +1,379 @@ +#!/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_gir_profile +version_added: "2.2" +short_description: Create a maintenance-mode or normal-mode profile for GIR. +description: + - Manage a maintenance-mode or normal-mode profile with configuration + commands that can be applied during graceful removal + or graceful insertion. +extends_documentation_fragment: nxos +author: + - Gabriele Gerbino (@GGabriele) +notes: + - This module is not idempotent when C(state=present). + - C(state=absent) removes the whole profile. +options: + commands: + description: + - List of commands to be included into the profile. + required: false + default: null + mode: + description: + - Configure the profile as Maintenance or Normale mode. + required: true + choices: ['maintenance', 'normal'] + state: + description: + - Specify desired state of the resource. + required: false + default: present + choices: ['present','absent'] + include_defaults: + description: + - Specify to retrieve or not the complete running configuration + for module operations. + required: false + default: false + choices: ['true','false'] + config: + description: + - Specify the configuration string to be used for module operations. + required: false + default: null +''' + +EXAMPLES = ''' +# Create a maintenance-mode profile +- nxos_gir_profile: + mode: maintenance + commands: + - router eigrp 11 + - isolate + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +# Remove the maintenance-mode profile +- nxos_gir_profile: + mode: maintenance + state: absent + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" +''' + +RETURN = ''' +proposed: + description: list of commands passed into module. + returned: verbose mode + type: list + sample: ["router eigrp 11", "isolate"] +existing: + description: list of existing profile commands. + returned: verbose mode + type: list + sample: ["router bgp 65535","isolate","router eigrp 10","isolate", + "diagnostic bootup level complete"] +end_state: + description: list of profile entries after module execution. + returned: verbose mode + type: list + sample: ["router bgp 65535","isolate","router eigrp 10","isolate", + "diagnostic bootup level complete","router eigrp 11", "isolate"] +updates: + description: commands sent to the device + returned: always + type: list + sample: ["configure maintenance profile maintenance-mode", + "router eigrp 11","isolate"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +# 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 get_existing(module): + existing = [] + netcfg = get_config(module) + + if module.params['mode'] == 'maintenance': + parents = ['configure maintenance profile maintenance-mode'] + else: + parents = ['configure maintenance profile normal-mode'] + + config = netcfg.get_section(parents) + if config: + existing = config.splitlines() + existing = [cmd.strip() for cmd in existing] + existing.pop(0) + + return existing + + +def state_present(module, existing, commands): + cmds = list() + cmds.extend(commands) + if module.params['mode'] == 'maintenance': + cmds.insert(0, 'configure maintenance profile maintenance-mode') + else: + cmds.insert(0, 'configure maintenance profile normal-mode') + + return cmds + + +def state_absent(module, existing, commands): + if module.params['mode'] == 'maintenance': + cmds = ['no configure maintenance profile maintenance-mode'] + else: + cmds = ['no configure maintenance profile normal-mode'] + return cmds + + +def invoke(name, *args, **kwargs): + func = globals().get(name) + if func: + return func(*args, **kwargs) + + +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 main(): + argument_spec = dict( + commands=dict(required=False, type='list'), + mode=dict(required=True, choices=['maintenance', 'normal']), + state=dict(choices=['absent', 'present'], + default='present'), + include_defaults=dict(default=False), + config=dict() + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + state = module.params['state'] + commands = module.params['commands'] or [] + + if state == 'absent' and commands: + module.fail_json(msg='when state is absent, no command can be used.') + + existing = invoke('get_existing', module) + end_state = existing + changed = False + + result = {} + cmds = [] + if state == 'present' or (state == 'absent' and existing): + cmds = invoke('state_%s' % state, module, existing, commands) + + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + execute_config_command(cmds, module) + changed = True + end_state = invoke('get_existing', module) + + result['connected'] = module.connected + result['changed'] = changed + if module._verbosity > 0: + end_state = invoke('get_existing', module) + result['end_state'] = end_state + result['existing'] = existing + result['proposed'] = commands + result['updates'] = cmds + + module.exit_json(**result) + + +if __name__ == '__main__': + main() \ No newline at end of file From 44265b37813189f2012c8676aee988083a81ff82 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 16:48:38 +0200 Subject: [PATCH 286/770] Adding nxos_install_os --- network/nxos/nxos_install_os.py | 389 ++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 network/nxos/nxos_install_os.py diff --git a/network/nxos/nxos_install_os.py b/network/nxos/nxos_install_os.py new file mode 100644 index 00000000000..16935771597 --- /dev/null +++ b/network/nxos/nxos_install_os.py @@ -0,0 +1,389 @@ +#!/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_install_os +short_description: Set boot options like boot image and kickstart image. +description: + - Install an operating system by setting the boot options like boot + image and kickstart image. +notes: + - The module will fail due to timeout issues, but the install will go on + anyway. Ansible's block and rescue can be leveraged to handle this kind + of failure and check actual module results. See EXAMPLE for more about + this. + - Do not include full file paths, just the name of the file(s) stored on + the top level flash directory. + - You must know if your platform supports taking a kickstart image as a + parameter. If supplied but not supported, errors may occur. + - This module attempts to install the software immediately, + wich may trigger a reboot. + - In check mode, the module tells you if the current boot images are set + to the desired images. +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbibo (@GGabriele) +version_added: 2.2 +options: + system_image_file: + description: + - Name of the system (or combined) image file on flash. + required: true + kickstart_image_file: + description: + - Name of the kickstart image file on flash. + required: false + default: null +''' + +EXAMPLES = ''' +- block: + - name: Install OS + nxos_install_os: + system_image_file: nxos.7.0.3.I2.2d.bin + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" + transport: nxapi + rescue: + - name: Wait for device to come back up + wait_for: + port: 22 + state: started + timeout: 300 + delay: 60 + host: "{{ inventory_hostname }}" + - name: Check installed OS + nxos_command: + commands: + - show version + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" + transport: nxapi + register: output + - assert: + that: + - output['stdout'][0]['kickstart_ver_str'] == '7.0(3)I4(1)' +''' + + +# 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_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, output=command_type) + 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_ascii'): + cmds = [command] + if module.params['transport'] == 'cli': + body = execute_show(cmds, module) + elif module.params['transport'] == 'nxapi': + body = execute_show(cmds, module, command_type=command_type) + + return body + + +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_boot_options(module): + """Get current boot variables + like system image and kickstart image. + Returns: + A dictionary, e.g. { 'kick': router_kick.img, 'sys': 'router_sys.img'} + """ + command = 'show boot' + body = execute_show_command(command, module)[0] + boot_options_raw_text = body.split('Boot Variables on next reload')[1] + + if 'kickstart' in boot_options_raw_text: + kick_regex = r'kickstart variable = bootflash:/(\S+)' + sys_regex = r'system variable = bootflash:/(\S+)' + + kick = re.search(kick_regex, boot_options_raw_text).group(1) + sys = re.search(sys_regex, boot_options_raw_text).group(1) + retdict = dict(kick=kick, sys=sys) + else: + nxos_regex = r'NXOS variable = bootflash:/(\S+)' + nxos = re.search(nxos_regex, boot_options_raw_text).group(1) + retdict = dict(sys=nxos) + + command = 'show install all status' + retdict['status'] = execute_show_command(command, module)[0] + + return retdict + + +def already_set(current_boot_options, system_image_file, kickstart_image_file): + return current_boot_options.get('sys') == system_image_file \ + and current_boot_options.get('kick') == kickstart_image_file + + +def set_boot_options(module, image_name, kickstart=None): + """Set boot variables + like system image and kickstart image. + Args: + The main system image file name. + Keyword Args: many implementors may choose + to supply a kickstart parameter to specicify a kickstart image. + """ + commands = ['terminal dont-ask'] + if kickstart is None: + commands.append('install all nxos %s' % image_name) + else: + commands.append( + 'install all system %s kickstart %s' % (image_name, kickstart)) + execute_config_command(commands, module) + + +def main(): + argument_spec = dict( + system_image_file=dict(required=True), + kickstart_image_file=dict(required=False), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + system_image_file = module.params['system_image_file'] + kickstart_image_file = module.params['kickstart_image_file'] + + if kickstart_image_file == 'null': + kickstart_image_file = None + + current_boot_options = get_boot_options(module) + changed = False + if not already_set(current_boot_options, + system_image_file, + kickstart_image_file): + changed = True + + if not module.check_mode and changed == True: + set_boot_options(module, + system_image_file, + kickstart=kickstart_image_file) + + if not already_set(install_state, + system_image_file, + kickstart_image_file): + module.fail_json(msg='Install not successful', + install_state=install_state) + else: + install_state = current_boot_options + + module.exit_json(changed=changed, install_state=install_state) + + +if __name__ == '__main__': + main() \ No newline at end of file From 3b918a7be2d4425cb646bd9fa2418626c7e6e138 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 16:57:06 +0200 Subject: [PATCH 287/770] Adding RETURN --- network/nxos/nxos_install_os.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/network/nxos/nxos_install_os.py b/network/nxos/nxos_install_os.py index 16935771597..566b03f5152 100644 --- a/network/nxos/nxos_install_os.py +++ b/network/nxos/nxos_install_os.py @@ -83,6 +83,23 @@ - output['stdout'][0]['kickstart_ver_str'] == '7.0(3)I4(1)' ''' +RETURN = ''' +install_state: + returned: always + type: dictionary + sample: { + "kick": "n5000-uk9-kickstart.7.2.1.N1.1.bin", + "sys": "n5000-uk9.7.2.1.N1.1.bin", + "status": "This is the log of last installation.\n + Continuing with installation process, please wait.\n + The login will be disabled until the installation is completed.\n + Performing supervisor state verification. \n + SUCCESS\n + Supervisor non-disruptive upgrade successful.\n + Install has been successful.\n", + } +''' + # COMMON CODE FOR MIGRATION import re From 6a2f9d35e13ebd36baa6a2959e3a019eddc4e41f Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 17:02:02 +0200 Subject: [PATCH 288/770] Adding nxos_snmp_location --- network/nxos/nxos_snmp_location.py | 417 +++++++++++++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 network/nxos/nxos_snmp_location.py diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py new file mode 100644 index 00000000000..6e82604a21e --- /dev/null +++ b/network/nxos/nxos_snmp_location.py @@ -0,0 +1,417 @@ +#!/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_snmp_location +short_description: Manages SNMP location information. +description: + - Manages SNMP location configuration. +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +options: + location: + description: + - Location information. + required: true + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# ensure snmp location is configured +- nxos_snmp_location: + location=Test + state=present + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# ensure snmp location is not configured +- nxos_snmp_location: + location=Test + state=absent + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"location": "New_Test"} +existing: + description: k/v pairs of existing snmp location + type: dict + sample: {"location": "Test"} +end_state: + description: k/v pairs of location info after module execution + returned: always + type: dict or null + sample: {"location": "New_Test"} +updates: + description: command sent to the device + returned: always + type: string + sample: ["snmp-server location New_Test"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_snmp_location(module): + location = {} + location_regex = '.*snmp-server\slocation\s(?P\S+).*' + command = 'show run snmp' + + body = execute_show_command(command, module, command_type='cli_show_ascii') + try: + match_location = re.match(location_regex, body[0], re.DOTALL) + group_location = match_location.groupdict() + location['location'] = group_location["location"] + except (AttributeError, TypeError): + location = {} + + return location + + +def main(): + argument_spec = dict( + location=dict(required=True, type='str'), + state=dict(choices=['absent', 'present'], + default='present') + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + + location = module.params['location'] + state = module.params['state'] + + existing = get_snmp_location(module) + changed = False + commands = [] + proposed = dict(location=location) + end_state = existing + + if state == 'absent': + if existing and existing['location'] == location: + commands.append('no snmp-server location') + elif state == 'present': + if not existing or existing['location'] != location: + commands.append('snmp-server location {0}'.format(location)) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_snmp_location(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +from ansible.module_utils.basic import * +if __name__ == "__main__": + main() \ No newline at end of file From 1747d82d4448e079fcd9af9a2b122238a04a5cdc Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 17:04:24 +0200 Subject: [PATCH 289/770] Fixing docstring --- network/nxos/nxos_snmp_location.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py index 6e82604a21e..5b1cda222a9 100644 --- a/network/nxos/nxos_snmp_location.py +++ b/network/nxos/nxos_snmp_location.py @@ -20,9 +20,11 @@ DOCUMENTATION = ''' --- module: nxos_snmp_location +version_added: "2.2" short_description: Manages SNMP location information. description: - Manages SNMP location configuration. +extends_documentation_fragment: nxos author: - Jason Edelman (@jedelman8) - Gabriele Gerbino (@GGabriele) From 03be83cd353a56ee66aa28ab3ac9c2b6da3a4a1d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 17:10:21 +0200 Subject: [PATCH 290/770] Fixing return string --- network/nxos/nxos_snmp_location.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py index 5b1cda222a9..eb9520a931a 100644 --- a/network/nxos/nxos_snmp_location.py +++ b/network/nxos/nxos_snmp_location.py @@ -77,7 +77,7 @@ updates: description: command sent to the device returned: always - type: string + type: list sample: ["snmp-server location New_Test"] changed: description: check to see if a change was made on the device From 60cf27eab87754a7f902204c17102fe6af56529a Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 17:32:42 +0200 Subject: [PATCH 291/770] Adding nxos_snmp_traps --- network/nxos/nxos_snmp_traps.py | 496 ++++++++++++++++++++++++++++++++ 1 file changed, 496 insertions(+) create mode 100644 network/nxos/nxos_snmp_traps.py diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py new file mode 100644 index 00000000000..24d13f8bdd7 --- /dev/null +++ b/network/nxos/nxos_snmp_traps.py @@ -0,0 +1,496 @@ +#!/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_snmp_trap +version_added: "2.2" +short_description: Manages SNMP traps. +description: + - Manages SNMP traps configurations. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - This module works at the group level for traps. If you need to only + enable/disable 1 specific trap within a group, use the M(nxos_command) + module. + IMPORTANT: Be aware that you can set a trap only for an enabled feature. +options: + group: + description: + - Case sensitive group. + required: true + choices: ['aaa', 'bridge', 'callhome', 'cfs', 'config', 'entity', + 'feature-control', 'hsrp', 'license', 'link', 'lldp', 'ospf', 'pim', + 'rf', 'rmon', 'snmp', 'storm-control', 'stpx', 'sysmgr', 'system', + 'upgrade', 'vtp', 'all'] + state: + description: + - Manage the state of the resource. + required: false + default: enabled + choices: ['enabled','disabled'] +''' + +EXAMPLES = ''' +# ensure lldp trap configured +- nxos_snmp_traps: + group=lldp + state=enabled + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# ensure lldp trap is not configured +- nxos_snmp_traps: + group=lldp + state=disabled + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"group": "lldp"} +existing: + description: k/v pairs of existing trap status + type: dict + sample: {"lldp": [{"enabled": "No", + "trap": "lldpRemTablesChange"}]} +end_state: + description: k/v pairs of trap info after module execution + returned: always + type: dict + sample: {"lldp": [{"enabled": "Yes", + "trap": "lldpRemTablesChange"}]} +updates: + description: command sent to the device + returned: always + type: list + sample: "snmp-server enable traps lldp ;" +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_snmp_traps(group, module): + command = 'show snmp trap' + body = execute_show_command(command, module) + + trap_key = { + 'description': 'trap', + 'isEnabled': 'enabled' + } + + resource = {} + + try: + resource_table = body[0]['TABLE_snmp_trap']['ROW_snmp_trap'] + + for each_feature in ['aaa', 'bridge', 'callhome', 'cfs', 'config', + 'entity', 'feature-control', 'hsrp', 'license', + 'link', 'lldp', 'ospf', 'pim', 'rf', 'rmon', + 'snmp', 'storm-control', 'stpx', 'sysmgr', + 'system', 'upgrade', 'vtp']: + + resource[each_feature] = [] + + for each_resource in resource_table: + key = str(each_resource['trap_type']) + mapped_trap = apply_key_map(trap_key, each_resource) + + if key != 'Generic': + resource[key].append(mapped_trap) + + except (KeyError, AttributeError): + return resource + + find = resource.get(group, None) + + if group == 'all'.lower(): + return resource + elif find: + trap_resource = {group: resource[group]} + return trap_resource + else: + # if 'find' is None, it means that 'group' is a + # currently disabled feature. + return {} + + +def get_trap_commands(group, state, existing, module): + commands = [] + enabled = False + disabled = False + + if group == 'all': + if state == 'disabled': + for feature in existing: + trap_commands = ['no snmp-server enable traps {0}'.format(feature) for + trap in existing[feature] if trap['enabled'] == 'Yes'] + trap_commands = list(set(trap_commands)) + commands.append(trap_commands) + + elif state == 'enabled': + for feature in existing: + trap_commands = ['snmp-server enable traps {0}'.format(feature) for + trap in existing[feature] if trap['enabled'] == 'No'] + trap_commands = list(set(trap_commands)) + commands.append(trap_commands) + + else: + if group in existing: + for each_trap in existing[group]: + check = each_trap['enabled'] + if check.lower() == 'yes': + enabled = True + if check.lower() == 'no': + disabled = True + + if state == 'disabled' and enabled: + commands.append(['no snmp-server enable traps {0}'.format(group)]) + elif state == 'enabled' and disabled: + commands.append(['snmp-server enable traps {0}'.format(group)]) + else: + module.fail_json(msg='{0} is not a currently ' + 'enabled feature.'.format(group)) + + return commands + + +def main(): + argument_spec = dict( + state=dict(choices=['enabled', 'disabled'], required=True), + group=dict(choices=['aaa', 'bridge', 'callhome', 'cfs', 'config', + 'entity', 'feature-control', 'hsrp', + 'license', 'link', 'lldp', 'ospf', 'pim', 'rf', + 'rmon', 'snmp', 'storm-control', 'stpx', + 'sysmgr', 'system', 'upgrade', 'vtp', 'all'], + required=True), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + group = module.params['group'].lower() + state = module.params['state'] + + existing = get_snmp_traps(group, module) + proposed = {'group': group} + + changed = False + end_state = existing + commands = get_trap_commands(group, state, existing, module) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_snmp_traps(group, module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() \ No newline at end of file From 8fda0770475a120d3089ca0aaa5ff99c64109ffc Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 17:40:36 +0200 Subject: [PATCH 292/770] Fixing docstring format --- network/nxos/nxos_snmp_traps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py index 24d13f8bdd7..47cf312b5cb 100644 --- a/network/nxos/nxos_snmp_traps.py +++ b/network/nxos/nxos_snmp_traps.py @@ -31,7 +31,7 @@ - This module works at the group level for traps. If you need to only enable/disable 1 specific trap within a group, use the M(nxos_command) module. - IMPORTANT: Be aware that you can set a trap only for an enabled feature. + - Be aware that you can set a trap only for an enabled feature. options: group: description: From 79e0890cd0b19c6bc0d641548d4bf9dec8e6f8b9 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 17:49:02 +0200 Subject: [PATCH 293/770] Fixing argument_spec --- network/nxos/nxos_snmp_traps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py index 47cf312b5cb..98c779fe3eb 100644 --- a/network/nxos/nxos_snmp_traps.py +++ b/network/nxos/nxos_snmp_traps.py @@ -452,7 +452,7 @@ def get_trap_commands(group, state, existing, module): def main(): argument_spec = dict( - state=dict(choices=['enabled', 'disabled'], required=True), + state=dict(choices=['enabled', 'disabled'], default='enabled'), group=dict(choices=['aaa', 'bridge', 'callhome', 'cfs', 'config', 'entity', 'feature-control', 'hsrp', 'license', 'link', 'lldp', 'ospf', 'pim', 'rf', From 67a3d87f7ddb0a1e1c22e5ded5aedc4ccc70cce2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Wed, 14 Sep 2016 18:25:38 +0200 Subject: [PATCH 294/770] Adding nxos_ntp --- network/nxos/nxos_ntp.py | 632 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 632 insertions(+) create mode 100644 network/nxos/nxos_ntp.py diff --git a/network/nxos/nxos_ntp.py b/network/nxos/nxos_ntp.py new file mode 100644 index 00000000000..7e64fccdb50 --- /dev/null +++ b/network/nxos/nxos_ntp.py @@ -0,0 +1,632 @@ +#!/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_ntp +version_added: "2.2" +short_description: Manages core NTP configuration. +description: + - Manages core NTP configuration. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +options: + server: + description: + - Network address of NTP server. + required: false + default: null + peer: + description: + - Network address of NTP peer. + required: false + default: null + key_id: + description: + - Authentication key identifier to use with + given NTP server or peer. + required: false + default: null + prefer: + description: + - Makes given NTP server or peer the preferred + NTP server or peer for the device. + required: false + default: null + choices: ['enabled', 'disabled'] + vrf_name: + description: + - Makes the device communicate with the given + NTP server or peer over a specific VRF. + required: false + default: null + source_addr: + description: + - Local source address from which NTP messages are sent. + required: false + default: null + source_int: + description: + - Local source interface from which NTP messages are sent. + Must be fully qualified interface name. + required: false + default: null + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# Set NTP Server with parameters +- nxos_ntp: + server=1.2.3.4 + key_id=32 + prefer=enabled + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"address": "2.2.2.2", "key_id": "48", + "peer_type": "server", "prefer": "enabled", + "source": "3.3.3.3", "source_type": "source"} +existing: + description: + - k/v pairs of existing ntp server/peer + type: dict + sample: {"address": "2.2.2.2", "key_id": "32", + "peer_type": "server", "prefer": "enabled", + "source": "ethernet2/1", "source_type": "source-interface"} +end_state: + description: k/v pairs of ntp info after module execution + returned: always + type: dict + sample: {"address": "2.2.2.2", "key_id": "48", + "peer_type": "server", "prefer": "enabled", + "source": "3.3.3.3", "source_type": "source"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["ntp server 2.2.2.2 prefer key 48", + "no ntp source-interface ethernet2/1", "ntp source 3.3.3.3"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_ntp_source(module): + source_type = None + source = None + command = 'show run | inc ntp.source' + output = execute_show_command(command, module, command_type='cli_show_ascii') + + if output: + try: + if 'interface' in output[0]: + source_type = 'source-interface' + else: + source_type = 'source' + source = output[0].split()[2].lower() + except AttributeError: + source_type = None + source = None + + return source_type, source + + +def get_ntp_peer(module): + command = 'show run | inc ntp.(server|peer)' + ntp_peer_list = [] + ntp = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + + ntp_regex = ( + ".*ntp\s(server\s(?P
\S+)|peer\s(?P\S+))" + "\s*((?Pprefer)\s*)?(use-vrf\s(?P\S+)\s*)?" + "(key\s(?P\d+))?.*" + ) + + split_ntp = ntp.splitlines() + for peer_line in split_ntp: + ntp_peer = {} + try: + peer_address = None + vrf_name = None + prefer = None + key_id = None + match_ntp = re.match(ntp_regex, peer_line, re.DOTALL) + group_ntp = match_ntp.groupdict() + + address = group_ntp["address"] + peer_address = group_ntp['peer_address'] + prefer = group_ntp['prefer'] + vrf_name = group_ntp['vrf_name'] + key_id = group_ntp['key_id'] + + if prefer is not None: + prefer = 'enabled' + else: + prefer = 'disabled' + + if address is not None: + peer_type = 'server' + elif peer_address is not None: + peer_type = 'peer' + address = peer_address + + args = dict(peer_type=peer_type, address=address, prefer=prefer, + vrf_name=vrf_name, key_id=key_id) + + ntp_peer = dict((k, v) for k, v in args.iteritems()) + ntp_peer_list.append(ntp_peer) + except AttributeError: + ntp_peer_list = [] + + return ntp_peer_list + + +def get_ntp_existing(address, peer_type, module): + peer_dict = {} + peer_server_list = [] + + peer_list = get_ntp_peer(module) + for peer in peer_list: + if peer['address'] == address: + peer_dict.update(peer) + else: + peer_server_list.append(peer) + + source_type, source = get_ntp_source(module) + + if (source_type is not None and source is not None): + peer_dict['source_type'] = source_type + peer_dict['source'] = source + + return (peer_dict, peer_server_list) + + +def set_ntp_server_peer(peer_type, address, prefer, key_id, vrf_name): + command_strings = [] + + if prefer: + command_strings.append(' prefer') + if key_id: + command_strings.append(' key {0}'.format(key_id)) + if vrf_name: + command_strings.append(' use-vrf {0}'.format(vrf_name)) + + command_strings.insert(0, 'ntp {0} {1}'.format(peer_type, address)) + + command = ''.join(command_strings) + + return command + + +def config_ntp(delta, existing): + address = delta.get('address', existing.get('address')) + peer_type = delta.get('peer_type', existing.get('peer_type')) + vrf_name = delta.get('vrf_name', existing.get('vrf_name')) + key_id = delta.get('key_id', existing.get('key_id')) + prefer = delta.get('prefer', existing.get('prefer')) + + source_type = delta.get('source_type') + source = delta.get('source') + + if prefer: + if prefer == 'enabled': + prefer = True + elif prefer == 'disabled': + prefer = False + + if source: + source_type = delta.get('source_type', existing.get('source_type')) + + ntp_cmds = [] + if peer_type: + ntp_cmds.append(set_ntp_server_peer( + peer_type, address, prefer, key_id, vrf_name)) + if source: + existing_source_type = existing.get('source_type') + existing_source = existing.get('source') + if existing_source_type and source_type != existing_source_type: + ntp_cmds.append('no ntp {0} {1}'.format(existing_source_type, existing_source)) + ntp_cmds.append('ntp {0} {1}'.format(source_type, source)) + + return ntp_cmds + + +def main(): + argument_spec = dict( + server=dict(type='str'), + peer=dict(type='str'), + key_id=dict(type='str'), + prefer=dict(type='str', choices=['enabled', 'disabled']), + vrf_name=dict(type='str'), + source_addr=dict(type='str'), + source_int=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[ + ['server','peer'], + ['source_addr','source_int']], + supports_check_mode=True) + + server = module.params['server'] or None + peer = module.params['peer'] or None + key_id = module.params['key_id'] + prefer = module.params['prefer'] + vrf_name = module.params['vrf_name'] + source_addr = module.params['source_addr'] + source_int = module.params['source_int'] + state = module.params['state'] + if source_int is not None: + source_int = source_int.lower() + + if server: + peer_type = 'server' + address = server + elif peer: + peer_type = 'peer' + address = peer + else: + peer_type = None + address = None + + source_type = None + source = None + if source_addr: + source_type = 'source' + source = source_addr + elif source_int: + source_type = 'source-interface' + source = source_int + + if key_id or vrf_name or prefer: + if not server and not peer: + module.fail_json( + msg='Please supply the server or peer parameter') + + args = dict(peer_type=peer_type, address=address, key_id=key_id, + prefer=prefer, vrf_name=vrf_name, source_type=source_type, + source=source) + + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + existing, peer_server_list = get_ntp_existing(address, peer_type, module) + + end_state = existing + changed = False + commands = [] + + if state == 'present': + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + if delta: + command = config_ntp(delta, existing) + if command: + commands.append(command) + + elif state == 'absent': + if existing.get('peer_type') and existing.get('address'): + command = 'no ntp {0} {1}'.format( + existing['peer_type'], existing['address']) + if command: + commands.append([command]) + + existing_source_type = existing.get('source_type') + existing_source = existing.get('source') + proposed_source_type = proposed.get('source_type') + proposed_source = proposed.get('source') + + if proposed_source_type: + if proposed_source_type == existing_source_type: + if proposed_source == existing_source: + command = 'no ntp {0} {1}'.format( + existing_source_type, existing_source) + if command: + commands.append([command]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_ntp_existing(address, peer_type, module)[0] + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['updates'] = cmds + results['changed'] = changed + results['end_state'] = end_state + results['peer_server_list'] = peer_server_list + + module.exit_json(**results) + + +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() \ No newline at end of file From 683e5e4d1ad1e779d27d58af4ae7fdcda9881ab2 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 14 Sep 2016 13:26:09 -0700 Subject: [PATCH 295/770] Fix python 3 issues with apt* modules. (#4848) - Use range instead of xrange. - Use python3-apt package for python 3. - Eliminate unsupported for/else/raise usage. - Use list on dict.items when modifying dict. - Update requirements documentation. Also made non-intrustive style fixes (adding blank lines). --- packaging/os/apt.py | 41 ++++++++++++++++++++++++++-------- packaging/os/apt_key.py | 9 +++++++- packaging/os/apt_repository.py | 26 ++++++++++++++------- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 9dd54adfaf9..29b8fb8f84e 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -116,8 +116,10 @@ required: false default: false version_added: "2.1" - -requirements: [ python-apt, aptitude ] +requirements: + - python-apt (python 2) + - python3-apt (python 3) + - aptitude author: "Matthew Williams (@mgwilliams)" notes: - Three of the upgrade modes (C(full), C(safe) and its alias C(yes)) require C(aptitude), otherwise @@ -197,6 +199,7 @@ import datetime import fnmatch import itertools +import sys from ansible.module_utils._text import to_native @@ -226,6 +229,12 @@ except ImportError: HAS_PYTHON_APT = False +if sys.version_info[0] < 3: + PYTHON_APT = 'python-apt' +else: + PYTHON_APT = 'python3-apt' + + def package_split(pkgspec): parts = pkgspec.split('=', 1) if len(parts) > 1: @@ -233,6 +242,7 @@ def package_split(pkgspec): else: return parts[0], None + def package_versions(pkgname, pkg, pkg_cache): try: versions = set(p.version for p in pkg.versions) @@ -245,12 +255,14 @@ def package_versions(pkgname, pkg, pkg_cache): return versions + def package_version_compare(version, other_version): try: return apt_pkg.version_compare(version, other_version) except AttributeError: return apt_pkg.VersionCompare(version, other_version) + def package_status(m, pkgname, version, cache, state): try: # get the package from the cache, as well as the @@ -327,6 +339,7 @@ def package_status(m, pkgname, version, cache, state): return package_is_installed, package_is_upgradable, has_files + def expand_dpkg_options(dpkg_options_compressed): options_list = dpkg_options_compressed.split(',') dpkg_options = "" @@ -335,6 +348,7 @@ def expand_dpkg_options(dpkg_options_compressed): % (dpkg_options, dpkg_option) return dpkg_options.strip() + def expand_pkgspec_from_fnmatches(m, pkgspec, cache): # Note: apt-get does implicit regex matching when an exact package name # match is not found. Something like this: @@ -373,6 +387,7 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache): new_pkgspec.append(pkgspec_pattern) return new_pkgspec + def parse_diff(output): diff = to_native(output).splitlines() try: @@ -394,6 +409,7 @@ def parse_diff(output): diff_end += 1 return {'prepared': '\n'.join(diff[diff_start:diff_end])} + def install(m, pkgspec, cache, upgrade=False, default_release=None, install_recommends=None, force=False, dpkg_options=expand_dpkg_options(DPKG_OPTIONS), @@ -471,6 +487,7 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None, else: return (True, dict(changed=False)) + def get_field_of_deb(m, deb_file, field="Version"): cmd_dpkg = m.get_bin_path("dpkg", True) cmd = cmd_dpkg + " --field %s %s" % (deb_file, field) @@ -479,6 +496,7 @@ def get_field_of_deb(m, deb_file, field="Version"): m.fail_json(msg="%s failed" % cmd, stdout=stdout, stderr=stderr) return to_native(stdout).strip('\n') + def install_deb(m, debs, cache, force, install_recommends, allow_unauthenticated, dpkg_options): changed=False deps_to_install = [] @@ -553,6 +571,7 @@ def install_deb(m, debs, cache, force, install_recommends, allow_unauthenticated else: m.exit_json(changed=changed, stdout=retvals.get('stdout',''), stderr=retvals.get('stderr',''), diff=retvals.get('diff', '')) + def remove(m, pkgspec, cache, purge=False, force=False, dpkg_options=expand_dpkg_options(DPKG_OPTIONS), autoremove=False): pkg_list = [] @@ -598,6 +617,7 @@ def remove(m, pkgspec, cache, purge=False, force=False, m.fail_json(msg="'apt-get remove %s' failed: %s" % (packages, err), stdout=out, stderr=err) m.exit_json(changed=True, stdout=out, stderr=err, diff=diff) + def upgrade(m, mode="yes", force=False, default_release=None, dpkg_options=expand_dpkg_options(DPKG_OPTIONS)): if m.check_mode: @@ -648,6 +668,7 @@ def upgrade(m, mode="yes", force=False, default_release=None, m.exit_json(changed=False, msg=out, stdout=out, stderr=err) m.exit_json(changed=True, msg=out, stdout=out, stderr=err, diff=diff) + def download(module, deb): tempdir = os.path.dirname(__file__) package = os.path.join(tempdir, str(deb.rsplit('/', 1)[1])) @@ -674,6 +695,7 @@ def download(module, deb): return deb + def main(): module = AnsibleModule( argument_spec = dict( @@ -701,16 +723,18 @@ def main(): if not HAS_PYTHON_APT: if module.check_mode: - module.fail_json(msg="python-apt must be installed to use check mode. If run normally this module can autoinstall it") + module.fail_json(msg="%s must be installed to use check mode. " + "If run normally this module can auto-install it." % PYTHON_APT) try: - module.run_command('apt-get update', check_rc=True) - module.run_command('apt-get install python-apt -y -q', check_rc=True) + module.run_command(['apt-get', 'update'], check_rc=True) + module.run_command(['apt-get', 'install', PYTHON_APT, '-y', '-q'], check_rc=True) global apt, apt_pkg import apt import apt.debfile import apt_pkg except ImportError: - module.fail_json(msg="Could not import python modules: apt, apt_pkg. Please install python-apt package.") + module.fail_json(msg="Could not import python modules: apt, apt_pkg. " + "Please install %s package." % PYTHON_APT) global APTITUDE_CMD APTITUDE_CMD = module.get_bin_path("aptitude", False) @@ -772,15 +796,14 @@ def main(): updated_cache_time = int(time.mktime(mtimestamp.timetuple())) if cache_valid is not True: - for retry in xrange(3): + for retry in range(3): try: cache.update() break except apt.cache.FetchFailedException: pass else: - #out of retries, pass on the exception - raise + module.fail_json(msg='Failed to update apt cache.') cache.open(progress=None) updated_cache = True updated_cache_time = int(time.mktime(now.timetuple())) diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index c2993be1af7..129c467e7f7 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -123,6 +123,7 @@ def check_missing_binaries(module): if len(missing): module.fail_json(msg="binaries are missing", names=missing) + def all_keys(module, keyring, short_format): if keyring: cmd = "apt-key --keyring %s adv --list-public-keys --keyid-format=long" % keyring @@ -141,6 +142,7 @@ def all_keys(module, keyring, short_format): results = shorten_key_ids(results) return results + def shorten_key_ids(key_id_list): """ Takes a list of key ids, and converts them to the 'short' format, @@ -151,6 +153,7 @@ def shorten_key_ids(key_id_list): short.append(key[-8:]) return short + def download_key(module, url): # FIXME: move get_url code to common, allow for in-memory D/L, support proxies # and reuse here @@ -166,12 +169,13 @@ def download_key(module, url): except Exception: module.fail_json(msg="error getting key id from url: %s" % url, traceback=format_exc()) + def import_key(module, keyring, keyserver, key_id): if keyring: cmd = "apt-key --keyring %s adv --keyserver %s --recv %s" % (keyring, keyserver, key_id) else: cmd = "apt-key adv --keyserver %s --recv %s" % (keyserver, key_id) - for retry in xrange(5): + for retry in range(5): (rc, out, err) = module.run_command(cmd) if rc == 0: break @@ -181,6 +185,7 @@ def import_key(module, keyring, keyserver, key_id): rc=rc, stdout=out, stderr=err) return True + def add_key(module, keyfile, keyring, data=None): if data is not None: if keyring: @@ -196,6 +201,7 @@ def add_key(module, keyfile, keyring, data=None): (rc, out, err) = module.run_command(cmd, check_rc=True) return True + def remove_key(module, key_id, keyring): # FIXME: use module.run_command, fail at point of error and don't discard useful stdin/stdout if keyring: @@ -205,6 +211,7 @@ def remove_key(module, key_id, keyring): (rc, out, err) = module.run_command(cmd, check_rc=True) return True + def main(): module = AnsibleModule( argument_spec=dict( diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index cecd94ac322..eae6228034f 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -28,7 +28,7 @@ description: - Add or remove an APT repositories in Ubuntu and Debian. notes: - - This module works on Debian and Ubuntu and requires C(python-apt). + - This module works on Debian and Ubuntu. - This module supports Debian Squeeze (version 6) as well as its successors. - This module treats Debian and Ubuntu distributions separately. So PPA could be installed only on Ubuntu machines. options: @@ -72,7 +72,9 @@ required: false author: "Alexander Saltanov (@sashka)" version_added: "0.7" -requirements: [ python-apt ] +requirements: + - python-apt (python 2) + - python3-apt (python 3) ''' EXAMPLES = ''' @@ -96,6 +98,7 @@ import glob import os import re +import sys import tempfile try: @@ -108,10 +111,16 @@ distro = None HAVE_PYTHON_APT = False +if sys.version_info[0] < 3: + PYTHON_APT = 'python-apt' +else: + PYTHON_APT = 'python3-apt' + DEFAULT_SOURCES_PERM = int('0644', 8) VALID_SOURCE_TYPES = ('deb', 'deb-src') + def install_python_apt(module): if not module.check_mode: @@ -119,8 +128,8 @@ def install_python_apt(module): if apt_get_path: rc, so, se = module.run_command([apt_get_path, 'update']) if rc != 0: - module.fail_json(msg="Failed to auto-install python-apt. Error was: '%s'" % se.strip()) - rc, so, se = module.run_command([apt_get_path, 'install', 'python-apt', '-y', '-q']) + module.fail_json(msg="Failed to auto-install %s. Error was: '%s'" % (PYTHON_APT, se.strip())) + rc, so, se = module.run_command([apt_get_path, 'install', PYTHON_APT, '-y', '-q']) if rc == 0: global apt, apt_pkg, aptsources_distro, distro, HAVE_PYTHON_APT import apt @@ -129,9 +138,10 @@ def install_python_apt(module): distro = aptsources_distro.get_distro() HAVE_PYTHON_APT = True else: - module.fail_json(msg="Failed to auto-install python-apt. Error was: '%s'" % se.strip()) + module.fail_json(msg="Failed to auto-install %s. Error was: '%s'" % (PYTHON_APT, se.strip())) else: - module.fail_json(msg="python-apt must be installed to use check mode") + module.fail_json(msg="%s must be installed to use check mode" % PYTHON_APT) + class InvalidSource(Exception): pass @@ -255,7 +265,7 @@ def load(self, file): self.files[file] = group def save(self): - for filename, sources in self.files.items(): + for filename, sources in list(self.files.items()): if sources: d, fn = os.path.split(filename) fd, tmp_path = tempfile.mkstemp(prefix=".%s-" % fn, dir=d) @@ -475,7 +485,7 @@ def main(): if params['install_python_apt']: install_python_apt(module) else: - module.fail_json(msg='python-apt is not installed, and install_python_apt is False') + module.fail_json(msg='%s is not installed, and install_python_apt is False' % PYTHON_APT) if isinstance(distro, aptsources_distro.UbuntuDistribution): sourceslist = UbuntuSourcesList(module, From 63a15db4fe5a3b3c47147b33f509ed714c423cce Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 08:24:16 +0200 Subject: [PATCH 296/770] Fixing docstring --- network/nxos/nxos_vxlan_vtep_vni.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py index 041f10f8b72..45232d20394 100644 --- a/network/nxos/nxos_vxlan_vtep_vni.py +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -78,6 +78,26 @@ required: false default: present choices: ['present','absent'] + include_defaults: + description: + - Specify to use or not the complete runnning configuration + for module operations. + required: false + default: true + choices: ['true','true'] + config: + description: + - Configuration string to be used for module operations. If not + specified, the module will use the current running configuration. + required: false + default: null + save: + description: + - Specify to save the running configuration after + module operations. + required: false + default: false + choices: ['true','false'] ''' EXAMPLES = ''' - nxos_vxlan_vtep_vni: @@ -466,7 +486,6 @@ def main(): suppress_arp=dict(required=False, type='bool'), ingress_replication=dict(required=False, type='str', choices=['bgp', 'static', 'default']), - m_facts=dict(required=False, default=False, type='bool'), state=dict(choices=['present', 'absent'], default='present', required=False), include_defaults=dict(default=True), From 2e2a9b3900afd1ff6f0d99c3a07ff74b7a8c5f51 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 08:29:59 +0200 Subject: [PATCH 297/770] Fixing nxos_interface --- network/nxos/nxos_interface.py | 501 +++++++++++++++++++++++++-------- 1 file changed, 379 insertions(+), 122 deletions(-) diff --git a/network/nxos/nxos_interface.py b/network/nxos/nxos_interface.py index 412895e19b3..b8ca51fe57b 100644 --- a/network/nxos/nxos_interface.py +++ b/network/nxos/nxos_interface.py @@ -18,12 +18,11 @@ DOCUMENTATION = ''' --- - module: nxos_interface version_added: "2.1" -short_description: Manages physical attributes of interfaces +short_description: Manages physical attributes of interfaces. description: - - Manages physical attributes of interfaces of NX-OS switches + - Manages physical attributes of interfaces of NX-OS switches. author: Jason Edelman (@jedelman8) notes: - This module is also used to create logical interfaces such as @@ -31,12 +30,21 @@ - Be cautious of platform specific idiosyncrasies. For example, when you default a loopback interface, the admin state toggles on certain versions of NX-OS. + - The M(nxos_overlay_global) C(anycast_gateway_mac) attribute must be + set before setting the C(fabric_forwarding_anycast_gateway) property. options: interface: description: - Full name of interface, i.e. Ethernet1/1, port-channel10. required: true default: null + interface_type: + description: + - Interface type to be unconfigured from the device. + required: false + default: null + choices: ['loopback', 'portchannel', 'svi', 'nve'] + version_added: "2.2" admin_state: description: - Administrative state of the interface. @@ -50,10 +58,24 @@ default: null mode: description: - - Manage I(Layer2) or I(Layer3) state of the interface. + - Manage Layer 2 or Layer 3 state of the interface. required: false default: null choices: ['layer2','layer3'] + ip_forward: + description: + - Enable/Disable ip forward feature on SVIs. + required: false + default: null + choices: ['enable','disable'] + version_added: "2.2" + fabric_forwarding_anycast_gateway: + description: + - Associate SVI with anycast gateway under VLAN configuration mode. + required: false + default: null + choices: ['true','false'] + version_added: "2.2" state: description: - Specify desired state of the resource. @@ -64,30 +86,24 @@ EXAMPLES = ''' # Ensure an interface is a Layer 3 port and that it has the proper description -- nxos_interface: interface=Ethernet1/1 description='Configured by Ansible' mode=layer3 host={{ inventory_hostname }} - +- nxos_interface: interface=Ethernet1/1 description='Configured by Ansible' mode=layer3 host=68.170.147.165 # Admin down an interface -- nxos_interface: interface=Ethernet2/1 host={{ inventory_hostname }} admin_state=down - +- nxos_interface: interface=Ethernet2/1 host=68.170.147.165 admin_state=down # Remove all loopback interfaces -- nxos_interface: interface=loopback state=absent host={{ inventory_hostname }} - +- nxos_interface: interface=loopback state=absent host=68.170.147.165 # Remove all logical interfaces -- nxos_interface: interface={{ item }} state=absent host={{ inventory_hostname }} +- nxos_interface: interface_type={{ item }} state=absent host={{ inventory_hostname }} with_items: - loopback - portchannel - svi - + - nve # Admin up all ethernet interfaces -- nxos_interface: interface=ethernet host={{ inventory_hostname }} admin_state=up - +- nxos_interface: interface=ethernet host=68.170.147.165 admin_state=up # Admin down ALL interfaces (physical and logical) -- nxos_interface: interface=all host={{ inventory_hostname }} admin_state=down - +- nxos_interface: interface=all host=68.170.147.165 admin_state=down ''' RETURN = ''' - proposed: description: k/v pairs of parameters passed into module returned: always @@ -96,17 +112,16 @@ existing: description: k/v pairs of existing switchport type: dict - sample: {"admin_state": "up", "description": "None", "interface": "port-channel101", "mode": "layer2", "type": "portchannel"} + sample: {"admin_state": "up", "description": "None", + "interface": "port-channel101", "mode": "layer2", + "type": "portchannel", "ip_forward": "enable"} end_state: description: k/v pairs of switchport after module execution returned: always type: dict or null - sample: {"admin_state": "down", "description": "None", "interface": "port-channel101", "mode": "layer2", "type": "portchannel"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" + sample: {"admin_state": "down", "description": "None", + "interface": "port-channel101", "mode": "layer2", + "type": "portchannel", "ip_forward": "enable"} updates: description: command list sent to the device returned: always @@ -117,29 +132,184 @@ returned: always type: boolean sample: true - ''' +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 + +BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 'True', 1, True] +BOOLEANS_FALSE = ['no', 'off', '0', 'false', 'False', 0, False] +ACCEPTED = BOOLEANS_TRUE + BOOLEANS_FALSE def is_default_interface(interface, module): """Checks to see if interface exists and if it is a default config - Args: interface (str): full name of interface, i.e. vlan10, Ethernet1/1, loopback10 - Returns: True: if interface has default config False: if it does not have a default config DNE (str): if the interface does not exist - loopbacks, SVIs, etc. - """ command = 'show run interface ' + interface try: body = execute_show_command(command, module, command_type='cli_show_ascii')[0] - # module.exit_json(abcd='asdasdfasdf', body=body, c=command) except IndexError: body = [] @@ -159,15 +329,12 @@ def is_default_interface(interface, module): def get_interface_type(interface): """Gets the type of interface - Args: interface (str): full name of interface, i.e. Ethernet1/1, loopback10, port-channel20, vlan20 - Returns: type of interface: ethernet, svi, loopback, management, portchannel, or unknown - """ if interface.upper().startswith('ET'): return 'ethernet' @@ -181,20 +348,19 @@ def get_interface_type(interface): return 'management' elif interface.upper().startswith('PO'): return 'portchannel' + elif interface.upper().startswith('NV'): + return 'nve' else: return 'unknown' def get_manual_interface_attributes(interface, module): """Gets admin state and description of a SVI interface. Hack due to API. - Args: interface (str): full name of SVI interface, i.e. vlan10 - Returns: dictionary that has two k/v pairs: admin_state & description if not an svi, returns None - """ if get_interface_type(interface) == 'svi': @@ -221,15 +387,12 @@ def get_manual_interface_attributes(interface, module): def get_interface(intf, module): """Gets current config/state of interface - Args: intf (string): full name of interface, i.e. Ethernet1/1, loopback10, port-channel20, vlan20 - Returns: dictionary that has relevant config/state data about the given interface based on the type of interface it is - """ base_key_map = { 'interface': 'interface', @@ -266,7 +429,6 @@ def get_interface(intf, module): if body: interface_table = body['TABLE_interface']['ROW_interface'] - intf_type = get_interface_type(intf) if intf_type in ['portchannel', 'ethernet']: if not interface_table.get('eth_mode'): @@ -288,6 +450,18 @@ def get_interface(intf, module): 'nxapibug')) interface['description'] = str(attributes.get('description', 'nxapi_bug')) + command = 'show run interface ' + intf + body = execute_show_command(command, module, + command_type='cli_show_ascii')[0] + if 'ip forward' in body: + interface['ip_forward'] = 'enable' + else: + interface['ip_forward'] = 'disable' + if 'fabric forwarding mode anycast-gateway' in body: + interface['fabric_forwarding_anycast_gateway'] = True + else: + interface['fabric_forwarding_anycast_gateway'] = False + elif intf_type == 'loopback': key_map.update(base_key_map) key_map.pop('admin_state') @@ -311,6 +485,13 @@ def get_interface(intf, module): temp_dict['description'] = "None" interface.update(temp_dict) + elif intf_type == 'nve': + key_map.update(base_key_map) + temp_dict = apply_key_map(key_map, interface_table) + if not temp_dict.get('description'): + temp_dict['description'] = "None" + interface.update(temp_dict) + interface['type'] = intf_type return interface @@ -323,18 +504,18 @@ def get_intf_args(interface): if intf_type in ['ethernet', 'portchannel']: arguments.extend(['mode']) + if intf_type == 'svi': + arguments.extend(['ip_forward', 'fabric_forwarding_anycast_gateway']) return arguments def get_interfaces_dict(module): """Gets all active interfaces on a given switch - Returns: dictionary with interface type (ethernet,svi,loop,portchannel) as the keys. Each value is a list of interfaces of given interface (key) type. - """ command = 'show interface status' try: @@ -348,12 +529,13 @@ def get_interfaces_dict(module): 'loopback': [], 'management': [], 'portchannel': [], + 'nve': [], 'unknown': [] } interface_list = body.get('TABLE_interface')['ROW_interface'] - for i in interface_list: - intf = i['interface'] + for index in interface_list: + intf = index ['interface'] intf_type = get_interface_type(intf) interfaces[intf_type].append(intf) @@ -379,6 +561,8 @@ def _get_number(if_name): if_type = 'loopback' elif if_name.lower().startswith('po'): if_type = 'port-channel' + elif if_name.lower().startswith('nv'): + if_type = 'nve' else: if_type = None @@ -417,19 +601,15 @@ def apply_value_map(value_map, resource): def get_interface_config_commands(interface, intf, existing): """Generates list of commands to configure on device - Args: interface (str): k/v pairs in the form of a set that should be configured on the device intf (str): full name of interface, i.e. Ethernet1/1 - Returns: list: ordered list of commands to be sent to device - """ commands = [] - desc = interface.get('description') if desc: commands.append('description {0}'.format(desc)) @@ -447,6 +627,21 @@ def get_interface_config_commands(interface, intf, existing): command = get_admin_state(interface, intf, admin_state) commands.append(command) + ip_forward = interface.get('ip_forward') + if ip_forward: + if ip_forward == 'enable': + commands.append('ip forward') + else: + commands.append('no ip forward') + + fabric_forwarding_anycast_gateway = interface.get( + 'fabric_forwarding_anycast_gateway') + if fabric_forwarding_anycast_gateway is not None: + if fabric_forwarding_anycast_gateway is True: + commands.append('fabric forwarding mode anycast-gateway') + elif fabric_forwarding_anycast_gateway is False: + commands.append('no fabric forwarding mode anycast-gateway') + if commands: commands.insert(0, 'interface ' + intf) @@ -471,7 +666,7 @@ def get_proposed(existing, normalized_interface, args): # retrieves proper interface params from args (user defined params) for param in allowed_params: temp = args.get(param) - if temp: + if temp is not None: proposed[param] = temp return proposed @@ -489,7 +684,7 @@ def smart_existing(module, intf_type, normalized_interface): if intf_type == 'ethernet': module.fail_json(msg='Invalid Ethernet interface provided.', interface=normalized_interface) - elif intf_type in ['loopback', 'portchannel', 'svi']: + elif intf_type in ['loopback', 'portchannel', 'svi', 'nve']: existing = {} is_default = 'DNE' return existing, is_default @@ -525,6 +720,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -532,8 +732,21 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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, output=command_type) + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending {0}'.format(cmds), + error=str(clie)) return response @@ -553,7 +766,10 @@ def execute_show_command(command, module, command_type='cli_show'): def execute_modified_show_for_cli_text(command, module): cmds = [command] - response = execute_show(cmds, module) + if module.params['transport'] == 'cli': + response = execute_show(cmds, module) + else: + response = execute_show(cmds, module, command_type='cli_show_ascii') body = response return body @@ -568,81 +784,125 @@ def flatten_list(command_lists): return flat_command_list +def get_interface_type_removed_cmds(interfaces): + commands = [] + + for interface in interfaces: + if interface != 'Vlan1': + commands.append('no interface {0}'.format(interface)) + + return commands + + def main(): argument_spec = dict( - interface=dict(required=True,), + interface=dict(required=False,), admin_state=dict(default='up', choices=['up', 'down'], required=False), description=dict(required=False, default=None), mode=dict(choices=['layer2', 'layer3'], required=False), + interface_type=dict(required=False, + choices=['loopback', 'portchannel', 'svi', 'nve']), + ip_forward=dict(required=False, choices=['enable', 'disable']), + fabric_forwarding_anycast_gateway=dict(required=False, type='bool', + choices=ACCEPTED), state=dict(choices=['absent', 'present', 'default'], - default='present', required=False) + default='present', required=False), + include_defaults=dict(default=True), + config=dict(), + save=dict(type='bool', default=False) ) - module = get_module(argument_spec=argument_spec, - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['interface', 'interface_type']], + supports_check_mode=True) - interface = module.params['interface'].lower() + interface = module.params['interface'] + interface_type = module.params['interface_type'] admin_state = module.params['admin_state'] description = module.params['description'] mode = module.params['mode'] + ip_forward = module.params['ip_forward'] + fabric_forwarding_anycast_gateway = module.params['fabric_forwarding_anycast_gateway'] state = module.params['state'] - changed = False - - args = dict(interface=interface, admin_state=admin_state, - description=description, mode=mode) - - intf_type = get_interface_type(interface) - - normalized_interface = normalize_interface(interface) - - if normalized_interface == 'Vlan1' and state == 'absent': - module.fail_json(msg='ERROR: CANNOT REMOVE VLAN 1!') - - if intf_type == 'unknown': - module.fail_json( - msg='unknown interface type found-1', - interface=interface) - - existing, is_default = smart_existing(module, intf_type, normalized_interface) - proposed = get_proposed(existing, normalized_interface, args) + if interface: + interface = interface.lower() + intf_type = get_interface_type(interface) + normalized_interface = normalize_interface(interface) + + if normalized_interface == 'Vlan1' and state == 'absent': + module.fail_json(msg='ERROR: CANNOT REMOVE VLAN 1!') + + if intf_type == 'nve': + if description or mode: + module.fail_json(msg='description and mode params are not ' + 'supported in this module. Use ' + 'nxos_vxlan_vtep instead.') + if ((ip_forward or fabric_forwarding_anycast_gateway) and + intf_type != 'svi'): + module.fail_json(msg='The ip_forward and ' + 'fabric_forwarding_anycast_gateway features ' + ' are only available for SVIs.') + args = dict(interface=interface, admin_state=admin_state, + description=description, mode=mode, ip_forward=ip_forward, + fabric_forwarding_anycast_gateway=fabric_forwarding_anycast_gateway) + + if intf_type == 'unknown': + module.fail_json( + msg='unknown interface type found-1', + interface=interface) + + existing, is_default = smart_existing(module, intf_type, normalized_interface) + proposed = get_proposed(existing, normalized_interface, args) + else: + intf_type = normalized_interface = interface_type + proposed = dict(interface_type=interface_type) - delta = dict() + changed = False commands = [] - - if state == 'absent': - if intf_type in ['svi', 'loopback', 'portchannel']: - if is_default != 'DNE': - cmds = ['no interface {0}'.format(normalized_interface)] + if interface: + delta = dict() + + if state == 'absent': + if intf_type in ['svi', 'loopback', 'portchannel', 'nve']: + if is_default != 'DNE': + cmds = ['no interface {0}'.format(normalized_interface)] + commands.append(cmds) + elif intf_type in ['ethernet']: + if is_default is False: + cmds = ['default interface {0}'.format(normalized_interface)] + commands.append(cmds) + elif state == 'present': + if not existing: + cmds = get_interface_config_commands(proposed, + normalized_interface, + existing) commands.append(cmds) - elif intf_type in ['ethernet']: + else: + delta = dict(set(proposed.iteritems()).difference( + existing.iteritems())) + if delta: + cmds = get_interface_config_commands(delta, + normalized_interface, + existing) + commands.append(cmds) + elif state == 'default': if is_default is False: cmds = ['default interface {0}'.format(normalized_interface)] commands.append(cmds) - elif state == 'present': - if not existing: - cmds = get_interface_config_commands(proposed, - normalized_interface, - existing) - commands.append(cmds) - else: - delta = dict(set(proposed.iteritems()).difference( - existing.iteritems())) - if delta: - cmds = get_interface_config_commands(delta, - normalized_interface, - existing) - commands.append(cmds) - elif state == 'default': - if is_default is False: - cmds = ['default interface {0}'.format(normalized_interface)] - commands.append(cmds) - elif is_default == 'DNE': - module.exit_json(msg='interface you are trying to default does' - ' not exist') + elif is_default == 'DNE': + module.exit_json(msg='interface you are trying to default does' + ' not exist') + elif interface_type: + if state == 'present': + module.fail_json(msg='The interface_type param can be used ' + 'only with state absent.') + + existing = get_interfaces_dict(module)[interface_type] + cmds = get_interface_type_removed_cmds(existing) + commands.append(cmds) cmds = flatten_list(commands) - end_state = existing if cmds: @@ -650,35 +910,32 @@ def main(): module.exit_json(changed=True, commands=cmds) else: execute_config_command(cmds, module) - if delta.get('mode'): # or delta.get('admin_state'): - # if the mode changes from L2 to L3, the admin state - # seems to change after the API call, so adding a second API - # call to ensure it's in the desired state. - admin_state = delta.get('admin_state') or admin_state - c1 = 'interface {0}'.format(normalized_interface) - c2 = get_admin_state(delta, normalized_interface, admin_state) - cmds2 = [c1, c2] - execute_config_command(cmds2, module) - cmds.extend(cmds2) changed = True - end_state, is_default = smart_existing(module, intf_type, - normalized_interface) + if module.params['interface']: + if delta.get('mode'): # or delta.get('admin_state'): + # if the mode changes from L2 to L3, the admin state + # seems to change after the API call, so adding a second API + # call to ensure it's in the desired state. + admin_state = delta.get('admin_state') or admin_state + c1 = 'interface {0}'.format(normalized_interface) + c2 = get_admin_state(delta, normalized_interface, admin_state) + cmds2 = [c1, c2] + execute_config_command(cmds2, module) + cmds.extend(cmds2) + end_state, is_default = smart_existing(module, intf_type, + normalized_interface) + else: + end_state = get_interfaces_dict(module)[interface_type] results = {} results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['state'] = state results['updates'] = cmds results['changed'] = changed module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * if __name__ == '__main__': main() From 7774632876add504b5ac1c911ceff30c950ba5e0 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 09:34:28 +0200 Subject: [PATCH 298/770] Adding nxos_aaa_server --- network/nxos/nxos_aaa_server.py | 567 ++++++++++++++++++++++++++++++++ 1 file changed, 567 insertions(+) create mode 100644 network/nxos/nxos_aaa_server.py diff --git a/network/nxos/nxos_aaa_server.py b/network/nxos/nxos_aaa_server.py new file mode 100644 index 00000000000..147a9137a71 --- /dev/null +++ b/network/nxos/nxos_aaa_server.py @@ -0,0 +1,567 @@ +#!/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_aaa_server +version_added: "2.2" +short_description: Manages AAA server global configuration. +description: + - Manages AAA server global configuration +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - The server_type parameter is always required. + - If encrypt_type is not supplied, the global AAA server key will be + stored as encrypted (type 7). + - Changes to the global AAA server key with encrypt_type=0 + are not idempotent. + - If global AAA server key is not found, it's shown as "unknown" + - state=default will set the supplied parameters to their default values. + The parameters that you want to default must also be set to default. + If global_key=default, the global key will be removed. +options: + server_type: + description: + - The server type is either radius or tacacs. + required: true + choices: ['radius', 'tacacs'] + global_key: + description: + - Global AAA shared secret. + required: false + default: null + encrypt_type: + description: + - The state of encryption applied to the entered global key. + O clear text, 7 encrypted. Type-6 encryption is not supported. + required: false + default: null + choices: ['0', '7'] + deadtime: + description: + - Duration for which a non-reachable AAA server is skipped, + in minutes. Range is 1-1440. Device default is 0. + required: false + default: null + server_timeout: + description: + - Global AAA server timeout period, in seconds. Range is 1-60. + Device default is 5. + required: false + default: null + directed_request: + description: + - Enables direct authentication requests to AAA server. + Device default is disabled. + required: false + default: null + choices: ['enabled', 'disabled'] + state: + description: + - Manage the state of the resource. + required: true + default: present + choices: ['present','default'] +''' + +EXAMPLES = ''' +# Radius Server Basic settings + - name: "Radius Server Basic settings" + nxos_aaa_server: + server_type=radius + server_timeout=9 + deadtime=20 + directed_request=enabled + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Tacacs Server Basic settings + - name: "Tacacs Server Basic settings" + nxos_aaa_server: + server_type=tacacs + server_timeout=8 + deadtime=19 + directed_request=disabled + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Setting Global Key + - name: "AAA Server Global Key" + nxos_aaa_server: + server_type=radius + global_key=test_key + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"deadtime": "22", "directed_request": "enabled", + "server_type": "radius", "server_timeout": "11"} +existing: + description: + - k/v pairs of existing aaa server + type: dict + sample: {"deadtime": "0", "directed_request": "disabled", + "global_key": "unknown", "server_timeout": "5"} +end_state: + description: k/v pairs of aaa params after module execution + returned: always + type: dict + sample: {"deadtime": "22", "directed_request": "enabled", + "global_key": "unknown", "server_timeout": "11"} +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "present" +updates: + description: command sent to the device + returned: always + type: list + sample: ["radius-server deadtime 22", "radius-server timeout 11", + "radius-server directed-request"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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 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) + 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'): + cmds = [command] + if module.params['transport'] == 'cli': + body = execute_show(cmds, module) + elif module.params['transport'] == 'nxapi': + 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 get_aaa_server_info(server_type, module): + aaa_server_info = {} + server_command = 'show {0}-server'.format(server_type) + request_command = 'show {0}-server directed-request'.format(server_type) + global_key_command = 'show run | sec {0}'.format(server_type) + aaa_regex = '.*{0}-server\skey\s\d\s+(?P\S+).*'.format(server_type) + + server_body = execute_show_command( + server_command, module, command_type='cli_show_ascii')[0] + + split_server = server_body.splitlines() + + for line in split_server: + if line.startswith('timeout'): + aaa_server_info['server_timeout'] = line.split(':')[1] + + elif line.startswith('deadtime'): + aaa_server_info['deadtime'] = line.split(':')[1] + + request_body = execute_show_command( + request_command, module, command_type='cli_show_ascii')[0] + aaa_server_info['directed_request'] = request_body.replace('\n', '') + + key_body = execute_show_command( + global_key_command, module, command_type='cli_show_ascii')[0] + + try: + match_global_key = re.match(aaa_regex, key_body, re.DOTALL) + group_key = match_global_key.groupdict() + aaa_server_info['global_key'] = group_key["key"].replace('\"', '') + except (AttributeError, TypeError): + aaa_server_info['global_key'] = 'unknown' + + return aaa_server_info + + +def set_aaa_server_global_key(encrypt_type, key, server_type): + if not encrypt_type: + encrypt_type = '' + return '{0}-server key {1} {2}'.format( + server_type, encrypt_type, key) + + +def config_aaa_server(params, server_type): + cmds = [] + + deadtime = params.get('deadtime') + server_timeout = params.get('server_timeout') + directed_request = params.get('directed_request') + encrypt_type = params.get('encrypt_type', '7') + global_key = params.get('global_key') + + if deadtime is not None: + cmds.append('{0}-server deadtime {1}'.format(server_type, deadtime)) + + if server_timeout is not None: + cmds.append('{0}-server timeout {1}'.format(server_type, server_timeout)) + + if directed_request is not None: + if directed_request == 'enabled': + cmds.append('{0}-server directed-request'.format(server_type)) + elif directed_request == 'disabled': + cmds.append('no {0}-server directed-request'.format(server_type)) + + if global_key is not None: + cmds.append('{0}-server key {1} {2}'.format(server_type, encrypt_type, + global_key)) + + return cmds + + +def default_aaa_server(existing, params, server_type): + cmds = [] + + deadtime = params.get('deadtime') + server_timeout = params.get('server_timeout') + directed_request = params.get('directed_request') + global_key = params.get('global_key') + existing_key = existing.get('global_key') + + if deadtime is not None: + cmds.append('no {0}-server deadtime 1'.format(server_type)) + + if server_timeout is not None: + cmds.append('no {0}-server timeout 1'.format(server_type)) + + if directed_request is not None: + cmds.append('no {0}-server directed-request'.format(server_type)) + + if global_key is not None and existing_key is not None: + cmds.append('no {0}-server key 7 {1}'.format(server_type, existing_key)) + + return cmds + + +def main(): + argument_spec = dict( + server_type=dict(type='str', + choices=['radius', 'tacacs'], required=True), + global_key=dict(type='str'), + encrypt_type=dict(type='str', choices=['0', '7']), + deadtime=dict(type='str'), + server_timeout=dict(type='str'), + directed_request=dict(type='str', + choices=['enabled', 'disabled', 'default']), + state=dict(choices=['default', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + server_type = module.params['server_type'] + global_key = module.params['global_key'] + encrypt_type = module.params['encrypt_type'] + deadtime = module.params['deadtime'] + server_timeout = module.params['server_timeout'] + directed_request = module.params['directed_request'] + state = module.params['state'] + + if encrypt_type and not global_key: + module.fail_json(msg='encrypt_type must be used with global_key.') + + args = dict(server_type=server_type, global_key=global_key, + encrypt_type=encrypt_type, deadtime=deadtime, + server_timeout=server_timeout, directed_request=directed_request) + + changed = False + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + existing = get_aaa_server_info(server_type, module) + end_state = existing + + commands = [] + if state == 'present': + if deadtime: + try: + if int(deadtime) < 0 or int(deadtime) > 1440: + raise ValueError + except ValueError: + module.fail_json( + msg='deadtime must be an integer between 0 and 1440') + + if server_timeout: + try: + if int(server_timeout) < 1 or int(server_timeout) > 60: + raise ValueError + except ValueError: + module.fail_json( + msg='server_timeout must be an integer between 1 and 60') + + delta = dict(set(proposed.iteritems()).difference( + existing.iteritems())) + if delta: + command = config_aaa_server(delta, server_type) + if command: + commands.append(command) + + elif state == 'default': + for key, value in proposed.iteritems(): + if key != 'server_type' and value != 'default': + module.fail_json( + msg='Parameters must be set to "default"' + 'when state=default') + command = default_aaa_server(existing, proposed, server_type) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_aaa_server_info(server_type, module) + + results = {} + 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() \ No newline at end of file From 06998e592ae9ceee218144aa5a76ecaca217eac6 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 10:12:08 +0200 Subject: [PATCH 299/770] Adding nxos_ntp_auth --- network/nxos/nxos_ntp_auth.py | 566 ++++++++++++++++++++++++++++++++++ 1 file changed, 566 insertions(+) create mode 100644 network/nxos/nxos_ntp_auth.py diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py new file mode 100644 index 00000000000..2cd6b7ded3b --- /dev/null +++ b/network/nxos/nxos_ntp_auth.py @@ -0,0 +1,566 @@ +#!/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_ntp_auth +version_added: "2.2" +short_description: Manages NTP authentication. +description: + - Manages NTP authentication. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - If C(state=absent), the moudle will attempt to remove the given key configuration. + If a matching key configuration isn't found on the device, the module will fail. + - If C(state=absent) and C(authentication=on), authentication will be turned off. + - If C(state=absent) and C(authentication=off), authentication will be turned on. +options: + key_id: + description: + - Authentication key identifier (numeric). + required: true + md5string: + description: + - MD5 String. + required: true + default: null + auth_type: + description: + - Whether the given md5string is in cleartext or + has been encrypted. If in cleartext, the device + will encrypt it before storing it. + required: false + default: text + choices: ['text', 'encrypt'] + trusted_key: + description: + - Whether the given key is required to be supplied by a time source + for the device to synchronize to the time source. + required: false + default: false + choices: ['true', 'false'] + authentication: + description: + - Turns NTP authenication on or off. + required: false + default: null + choices: ['on', 'off'] + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# Basic NTP authentication configuration +- nxos_ntp_auth: + key_id=32 + md5string=hello + auth_type=text + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"auth_type": "text", "authentication": "off", + "key_id": "32", "md5string": "helloWorld", + "trusted_key": "true"} +existing: + description: + - k/v pairs of existing ntp authentication + type: dict + sample: {"authentication": "off", "trusted_key": "false"} +end_state: + description: k/v pairs of ntp autherntication after module execution + returned: always + type: dict + sample: {"authentication": "off", "key_id": "32", + "md5string": "kapqgWjwdg", "trusted_key": "true"} +state: + description: state as sent in from the playbook + returned: always + type: string + sample: "present" +updates: + description: command sent to the device + returned: always + type: list + sample: ["ntp authentication-key 32 md5 helloWorld 0", "ntp trusted-key 32"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_ntp_auth(module): + command = 'show ntp authentication-status' + + body = execute_show_command(command, module)[0] + ntp_auth_str = body['authentication'] + + if 'enabled' in ntp_auth_str: + ntp_auth = True + else: + ntp_auth = False + + return ntp_auth + + +def get_ntp_trusted_key(module): + trusted_key_list = [] + command = 'show run | inc ntp.trusted-key' + + trusted_key_str = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + if trusted_key_str: + trusted_keys = trusted_key_str.splitlines() + + else: + trusted_keys = [] + + for line in trusted_keys: + if line: + trusted_key_list.append(str(line.split()[2])) + + return trusted_key_list + + +def get_ntp_auth_key(key_id, module): + authentication_key = {} + command = 'show run | inc ntp.authentication-key.{0}'.format(key_id) + auth_regex = (".*ntp\sauthentication-key\s(?P\d+)\s" + "md5\s(?P\S+).*") + + body = execute_show_command(command, module, command_type='cli_show_ascii') + + try: + match_authentication = re.match(auth_regex, body[0], re.DOTALL) + group_authentication = match_authentication.groupdict() + key_id = group_authentication["key_id"] + md5string = group_authentication['md5string'] + authentication_key['key_id'] = key_id + authentication_key['md5string'] = md5string + except (AttributeError, TypeError): + authentication_key = {} + + return authentication_key + + +def get_ntp_auth_info(key_id, module): + auth_info = get_ntp_auth_key(key_id, module) + trusted_key_list = get_ntp_trusted_key(module) + auth_power = get_ntp_auth(module) + + if key_id in trusted_key_list: + auth_info['trusted_key'] = 'true' + else: + auth_info['trusted_key'] = 'false' + + if auth_power: + auth_info['authentication'] = 'on' + else: + auth_info['authentication'] = 'off' + + return auth_info + + +def auth_type_to_num(auth_type): + return '7' if auth_type == 'encrypt' else '0' + + +def set_ntp_auth_key(key_id, md5string, auth_type, trusted_key, authentication): + ntp_auth_cmds = [] + auth_type_num = auth_type_to_num(auth_type) + ntp_auth_cmds.append( + 'ntp authentication-key {0} md5 {1} {2}'.format( + key_id, md5string, auth_type_num)) + + if trusted_key == 'true': + ntp_auth_cmds.append( + 'ntp trusted-key {0}'.format(key_id)) + elif trusted_key == 'false': + ntp_auth_cmds.append( + 'no ntp trusted-key {0}'.format(key_id)) + + if authentication == 'on': + ntp_auth_cmds.append( + 'ntp authenticate') + elif authentication == 'off': + ntp_auth_cmds.append( + 'no ntp authenticate') + + return ntp_auth_cmds + + +def remove_ntp_auth_key(key_id, md5string, auth_type, trusted_key, authentication): + auth_remove_cmds = [] + auth_type_num = auth_type_to_num(auth_type) + auth_remove_cmds.append( + 'no ntp authentication-key {0} md5 {1} {2}'.format( + key_id, md5string, auth_type_num)) + + if authentication == 'on': + auth_remove_cmds.append( + 'no ntp authenticate') + elif authentication == 'off': + auth_remove_cmds.append( + 'ntp authenticate') + + return auth_remove_cmds + + +def main(): + argument_spec = dict( + key_id=dict(required=True, type='str'), + md5string=dict(required=True, type='str'), + auth_type=dict(choices=['text', 'encrypt'], default='text'), + trusted_key=dict(choices=['true', 'false'], default='false'), + authentication=dict(choices=['on', 'off']), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + key_id = module.params['key_id'] + md5string = module.params['md5string'] + auth_type = module.params['auth_type'] + trusted_key = module.params['trusted_key'] + authentication = module.params['authentication'] + state = module.params['state'] + + args = dict(key_id=key_id, md5string=md5string, + auth_type=auth_type, trusted_key=trusted_key, + authentication=authentication) + + changed = False + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + existing = get_ntp_auth_info(key_id, module) + end_state = existing + + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + commands = [] + if state == 'present': + if delta: + command = set_ntp_auth_key( + key_id, md5string, auth_type, trusted_key, delta.get('authentication')) + if command: + commands.append(command) + elif state == 'absent': + if existing: + auth_toggle = None + if authentication == existing.get('authentication'): + auth_toggle = authentication + command = remove_ntp_auth_key( + key_id, md5string, auth_type, trusted_key, auth_toggle) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + try: + execute_config_command(cmds, module) + except ShellError: + clie = get_exception() + module.fail_json(msg=str(clie) + ": " + cmds) + end_state = get_ntp_auth_info(key_id, module) + delta = dict(set(end_state.iteritems()).difference(existing.iteritems())) + if delta or (len(existing) != len(end_state)): + changed = True + + results = {} + 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() \ No newline at end of file From db07414e76e484268e5f942ccf24ed2f8551fd46 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 11:02:33 +0200 Subject: [PATCH 300/770] Fixing conditional format --- network/nxos/nxos_ntp_auth.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py index 2cd6b7ded3b..fc2d1c52a3f 100644 --- a/network/nxos/nxos_ntp_auth.py +++ b/network/nxos/nxos_ntp_auth.py @@ -446,7 +446,10 @@ def get_ntp_auth_info(key_id, module): def auth_type_to_num(auth_type): - return '7' if auth_type == 'encrypt' else '0' + if auth_type == 'encrypt' : + return '7' + else: + return '0' def set_ntp_auth_key(key_id, md5string, auth_type, trusted_key, authentication): From dfacfd4f592681ada827759efebb054e05267749 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 11:28:59 +0200 Subject: [PATCH 301/770] Adding nxos_ntp_options --- network/nxos/nxos_ntp_options.py | 515 +++++++++++++++++++++++++++++++ 1 file changed, 515 insertions(+) create mode 100644 network/nxos/nxos_ntp_options.py diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py new file mode 100644 index 00000000000..7615a0401aa --- /dev/null +++ b/network/nxos/nxos_ntp_options.py @@ -0,0 +1,515 @@ +#!/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_ntp_options +short_description: Manages NTP options. +description: + - Manages NTP options, e.g. authoritative server and logging. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - At least one of C(master) or C(logging) params must be supplied. + - When C(state=absent), boolean parameters are flipped, + e.g. C(master=true) will disable the authoritative server. + - When C(state=absent) and C(master=true), the stratum will be removed as well. + - When C(state=absent) and C(master=false), the stratum will be configured + to its default value, 8. +options: + master: + description: + - Sets whether the device is an authoritative NTP server. + required: false + default: null + choices: ['true','false'] + stratrum: + description: + - If C(master=true), an optional stratum can be supplied (1-15). + The device default is 8. + required: false + default: null + logging: + description: + - Sets whether NTP logging is enabled on the device. + required: false + default: null + choices: ['true','false'] + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +# Basic NTP options configuration +- nxos_ntp_options: + master=true + stratum=12 + logging=false + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"logging": false, "master": true, "stratum": "11"} +existing: + description: + - k/v pairs of existing ntp options + type: dict + sample: {"logging": true, "master": true, "stratum": "8"} +end_state: + description: k/v pairs of ntp options after module execution + returned: always + type: dict + sample: {"logging": false, "master": true, "stratum": "11"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["no ntp logging", "ntp master 11"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 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_ntp_master(module): + command = 'show run | inc ntp.master' + master_string = execute_show_command(command, module, command_type='cli_show_ascii') + + if master_string: + if master_string[0]: + master = True + else: + master = False + else: + master = False + + if master is True: + stratum = str(master_string[0].split()[2]) + else: + stratum = None + + return master, stratum + + +def get_ntp_log(module): + command = 'show ntp logging' + body = execute_show_command(command, module)[0] + + logging_string = body['loggingstatus'] + if 'enabled' in logging_string: + ntp_log = True + else: + ntp_log = False + + return ntp_log + + +def get_ntp_options(module): + existing = {} + existing['logging'] = get_ntp_log(module) + existing['master'], existing['stratum'] = get_ntp_master(module) + + return existing + + +def config_ntp_options(delta, flip=False): + master = delta.get('master') + stratum = delta.get('stratum') + log = delta.get('logging') + ntp_cmds = [] + + if flip: + log = not log + master = not master + + if log is not None: + if log is True: + ntp_cmds.append('ntp logging') + elif log is False: + ntp_cmds.append('no ntp logging') + if master is not None: + if master is True: + if not stratum: + stratum = '' + ntp_cmds.append('ntp master {0}'.format(stratum)) + elif master is False: + ntp_cmds.append('no ntp master') + + return ntp_cmds + + +def main(): + argument_spec = dict( + master=dict(required=False, type='bool'), + stratum=dict(type='str'), + logging=dict(required=False, type='bool'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + required_one_of=[['master', 'logging']], + supports_check_mode=True) + + master = module.params['master'] + stratum = module.params['stratum'] + logging = module.params['logging'] + state = module.params['state'] + + if stratum: + if master is None: + module.fail_json(msg='The master param must be supplied when ' + 'stratum is supplied') + try: + stratum_int = int(stratum) + if stratum_int < 1 or stratum_int > 15: + raise ValueError + except ValueError: + module.fail_json(msg='Stratum must be an integer between 1 and 15') + + existing = get_ntp_options(module) + end_state = existing + + args = dict(master=master, stratum=stratum, logging=logging) + + changed = False + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + if master is False: + proposed['stratum'] = None + stratum = None + + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + delta_stratum = delta.get('stratum') + + if delta_stratum: + delta['master'] = True + + commands = [] + if state == 'present': + if delta: + command = config_ntp_options(delta) + if command: + commands.append(command) + elif state == 'absent': + if existing: + isection = dict(set(proposed.iteritems()).intersection( + existing.iteritems())) + command = config_ntp_options(isection, flip=True) + if command: + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_ntp_options(module) + + results = {} + 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() \ No newline at end of file From 51fdf0703fa169b787769ada2cdd532f8c40f5e2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 11:33:50 +0200 Subject: [PATCH 302/770] Fixing docstring --- network/nxos/nxos_ntp_options.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py index 7615a0401aa..a8a0fe2d77f 100644 --- a/network/nxos/nxos_ntp_options.py +++ b/network/nxos/nxos_ntp_options.py @@ -20,6 +20,7 @@ --- module: nxos_ntp_options +version_added: "2.2" short_description: Manages NTP options. description: - Manages NTP options, e.g. authoritative server and logging. From 8684840000e2921fa50ea30d87f9062f0f54cce2 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 11:51:07 +0200 Subject: [PATCH 303/770] Adding nxos_udld --- network/nxos/nxos_udld.py | 502 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 502 insertions(+) create mode 100644 network/nxos/nxos_udld.py diff --git a/network/nxos/nxos_udld.py b/network/nxos/nxos_udld.py new file mode 100644 index 00000000000..04f8373e234 --- /dev/null +++ b/network/nxos/nxos_udld.py @@ -0,0 +1,502 @@ +#!/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_udld +version_added: "2.2" +short_description: Manages UDLD global configuration params. +description: + - Manages UDLD global configuration params. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - When C(state=absent), it unconfigures existing setings C(msg_time) and set it + to its default value of 15. It is cleaner to always use C(state=present). + - Module will fail if the udld feature has not been previously enabled. +options: + aggressive: + description: + - Toggles aggressive mode. + required: false + default: null + choices: ['enabled','disabled'] + msg_time: + description: + - Message time in seconds for UDLD packets. + required: false + default: null + reset: + description: + - Ability to reset UDLD down interfaces. + required: false + default: null + choices: ['true','false'] + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] + +''' +EXAMPLES = ''' +# ensure udld aggressive mode is globally disabled and se global message interval is 20 +- nxos_udld: + aggressive=disabled + msg_time=20 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Ensure agg mode is globally enabled and msg time is 15 +- nxos_udld: + aggressive=enabled + msg_time=15 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"aggressive": "enabled", "msg_time": "40"} +existing: + description: + - k/v pairs of existing udld configuration + type: dict + sample: {"aggressive": "disabled", "msg_time": "15"} +end_state: + description: k/v pairs of udld configuration after module execution + returned: always + type: dict + sample: {"aggressive": "enabled", "msg_time": "40"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["udld message-time 40", "udld aggressive"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 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_commands_config_udld_global(delta, reset): + config_args = { + 'enabled': 'udld aggressive', + 'disabled': 'no udld aggressive', + 'msg_time': 'udld message-time {msg_time}' + } + commands = [] + for param, value in delta.iteritems(): + if param == 'aggressive': + if value == 'enabled': + command = 'udld aggressive' + elif value == 'disabled': + command = 'no udld aggressive' + else: + command = config_args.get(param, 'DNE').format(**delta) + if command and command != 'DNE': + commands.append(command) + command = None + + if reset: + command = 'udld reset' + commands.append(command) + return commands + + +def get_commands_remove_udld_global(delta): + config_args = { + 'aggressive': 'no udld aggressive', + 'msg_time': 'no udld message-time {msg_time}', + } + commands = [] + for param, value in delta.iteritems(): + command = config_args.get(param, 'DNE').format(**delta) + if command and command != 'DNE': + commands.append(command) + command = None + return commands + + +def get_udld_global(module): + command = 'show udld global' + udld_table = execute_show_command(command, module)[0] + + status = str(udld_table.get('udld-global-mode', None)) + if status == 'enabled-aggressive': + aggressive = 'enabled' + else: + aggressive = 'disabled' + + interval = str(udld_table.get('message-interval', None)) + udld = dict(msg_time=interval, aggressive=aggressive) + + return udld + + +def main(): + argument_spec = dict( + aggressive=dict(required=False, choices=['enabled', 'disabled']), + msg_time=dict(required=False, type='str'), + reset=dict(required=False, type='bool'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + required_one_of=[['aggressive', 'msg_time', 'reset']], + supports_check_mode=True) + + aggressive = module.params['aggressive'] + msg_time = module.params['msg_time'] + reset = module.params['reset'] + state = module.params['state'] + + if (aggressive or reset) and state == 'absent': + module.fail_json(msg="It's better to use state=present when " + "configuring or unconfiguring aggressive mode " + "or using reset flag. state=absent is just for " + "when using msg_time param.") + + if msg_time: + try: + msg_time_int = int(msg_time) + if msg_time_int < 7 or msg_time_int > 90: + raise ValueError + except ValueError: + module.fail_json(msg='msg_time must be an integer' + 'between 7 and 90') + + args = dict(aggressive=aggressive, msg_time=msg_time, reset=reset) + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + existing = get_udld_global(module) + end_state = existing + + delta = set(proposed.iteritems()).difference(existing.iteritems()) + changed = False + + commands = [] + if state == 'present': + if delta: + command = get_commands_config_udld_global(dict(delta), reset) + commands.append(command) + + elif state == 'absent': + common = set(proposed.iteritems()).intersection(existing.iteritems()) + if common: + command = get_commands_remove_udld_global(dict(common)) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_udld_global(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() \ No newline at end of file From 5a4d361145c432281d59070468e5afef694c8618 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 12:29:04 +0200 Subject: [PATCH 304/770] Adding nxos_vtp_password --- network/nxos/nxos_vtp_password.py | 463 ++++++++++++++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 network/nxos/nxos_vtp_password.py diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py new file mode 100644 index 00000000000..ff1991c0331 --- /dev/null +++ b/network/nxos/nxos_vtp_password.py @@ -0,0 +1,463 @@ +#!/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_vtp +version_added: "2.2" +short_description: Manages VTP configuration. +description: + - Manages VTP configuration +extends_documentation_fragment: nxos +author: + - Gabriele Gerbino (@GGabriele) +notes: + - VTP feature must be active on the device to use this module. + - This module is used to manage only VTP passwords. + - Use this in combination with M(nxos_vtp_domain) and M(nxos_vtp_version) + to fully manage VTP operations. + - You can set/remove password only if a VTP domain already exist. + - If C(state=absent) and no C(vtp_password) is provided, it remove the current + VTP password. + - If C(state=absent) and C(vtp_password) is provided, the proposed C(vtp_password) + has to match the existing one in order to remove it. +options: + vtp_password: + description: + - VTP password + required: false + default: null + state: + description: + - Manage the state of the resource + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# ENSURE VTP PASSWORD IS SET +- nxos_vtp_password: + password=ntc + state=present + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# ENSURE VTP PASSWORD IS REMOVED +- nxos_vtp_password: + password=ntc + state=absent + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"vtp_password": "new_ntc"} +existing: + description: + - k/v pairs of existing vtp + type: dict + sample: {"domain": "ntc", "version": "1", "vtp_password": "ntc"} +end_state: + description: k/v pairs of vtp after module execution + returned: always + type: dict + sample: {"domain": "ntc", "version": "1", "vtp_password": "new_ntc"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["vtp password new_ntc"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 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_vtp_config(module): + command = 'show vtp status' + body = execute_show_command(command, module) + vtp_parsed = {} + + vtp_key = { + 'running-version': 'version', + 'domain_name': 'domain', + } + + if body: + vtp_parsed = apply_key_map(vtp_key, body[0]) + vtp_parsed['vtp_password'] = get_vtp_password(module) + + return vtp_parsed + + +def get_vtp_password(module): + command = 'show vtp password' + body = execute_show_command(command, module)[0] + password = body['passwd'] + if password: + return str(password) + else: + return "" + + +def main(): + argument_spec = dict( + vtp_password=dict(type='str'), + state=dict(choices=['absent', 'present'], + default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + vtp_password = module.params['vtp_password'] or None + state = module.params['state'] + + existing = get_vtp_config(module) + end_state = existing + + args = dict(vtp_password=vtp_password) + + changed = False + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + commands = [] + if state == 'absent': + if vtp_password is not None: + if existing['vtp_password'] == proposed['vtp_password']: + commands.append(['no vtp password']) + else: + module.fail_json(msg="Proposed vtp password doesn't match " + "current vtp password. It cannot be " + "removed when state=absent. If you are " + "trying to change the vtp password, use " + "state=present.") + else: + if not existing.get('domain'): + module.fail_json(msg='Cannot remove a vtp password ' + 'before vtp domain is set.') + + elif existing['vtp_password'] != ('\\'): + commands.append(['no vtp password']) + + elif state == 'present': + if delta: + if not existing.get('domain'): + module.fail_json(msg='Cannot set vtp password ' + 'before vtp domain is set.') + + else: + commands.append(['vtp password {0}'.format(vtp_password)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_vtp_config(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() \ No newline at end of file From 47015829c0e6d06f1c240691ac204df7644edabe Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 12:30:20 +0200 Subject: [PATCH 305/770] Fixing docstring --- network/nxos/nxos_vtp_password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index ff1991c0331..a4170763cd1 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -21,7 +21,7 @@ module: nxos_vtp version_added: "2.2" -short_description: Manages VTP configuration. +short_description: Manages VTP password configuration. description: - Manages VTP configuration extends_documentation_fragment: nxos From c3b8814df04742c36651d7dda9671848bf3460dd Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 12:33:27 +0200 Subject: [PATCH 306/770] Fixing docstring --- network/nxos/nxos_vtp_password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index a4170763cd1..d4826ae2f3f 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -23,7 +23,7 @@ version_added: "2.2" short_description: Manages VTP password configuration. description: - - Manages VTP configuration + - Manages VTP password configuration. extends_documentation_fragment: nxos author: - Gabriele Gerbino (@GGabriele) From 691f3ee7b21d2f6224a5243face98ce8101e5651 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 13:15:40 +0200 Subject: [PATCH 307/770] Improved function --- network/nxos/nxos_vtp_password.py | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index d4826ae2f3f..29ae405ee78 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -366,17 +366,31 @@ def apply_key_map(key_map, table): def get_vtp_config(module): command = 'show vtp status' - body = execute_show_command(command, module) - vtp_parsed = {} - vtp_key = { - 'running-version': 'version', - 'domain_name': 'domain', - } + body = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + vtp_parsed = {} if body: - vtp_parsed = apply_key_map(vtp_key, body[0]) - vtp_parsed['vtp_password'] = get_vtp_password(module) + version_regex = '.*VTP version running\s+:\s+(?P\d).*' + domain_regex = '.*VTP Domain Name\s+:\s+(?P\S+).*' + + try: + match_version = re.match(version_regex, body, re.DOTALL) + version = match_version.groupdict()['version'] + except AttributeError: + version = '' + + try: + match_domain = re.match(domain_regex, body, re.DOTALL) + domain = match_domain.groupdict()['domain'] + except AttributeError: + domain = '' + + if domain and version: + vtp_parsed['domain'] = domain + vtp_parsed['version'] = version + vtp_parsed['vtp_password'] = get_vtp_password(module) return vtp_parsed From 2425ed9c25f1f7e90f347b95a51d625baf034c16 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 13:17:20 +0200 Subject: [PATCH 308/770] Adding nxos_vtp_domain --- network/nxos/nxos_vtp_domain.py | 433 ++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) create mode 100644 network/nxos/nxos_vtp_domain.py diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py new file mode 100644 index 00000000000..2e4d33a9c53 --- /dev/null +++ b/network/nxos/nxos_vtp_domain.py @@ -0,0 +1,433 @@ +#!/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_vtp +short_description: Manages VTP domain configuration. +description: + - Manages VTP domain configuration. +extends_documentation_fragment: nxos +author: + - Gabriele Gerbino (@GGabriele) +notes: + - VTP feature must be active on the device to use this module. + - This module is used to manage only VTP domain names. + - VTP domain names are case-sensible. + - If it's never been configured before, VTP version is setted to 1 by default. + Otherwise, it leaves the previous configured version untouched. + Use M(nxos_vtp_version) to change it. + - Use this in combination with M(nxos_vtp_password) and M(nxos_vtp_version) + to fully manage VTP operations. +options: + domain: + description: + - VTP domain name. + required: true +''' + +EXAMPLES = ''' +# ENSURE VTP DOMAIN IS CONFIGURED +- nxos_vtp_domain: + domain=ntc + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"domain": "ntc"} +existing: + description: + - k/v pairs of existing vtp domain + type: dict + sample: {"domain": "testing", "version": "2", "vtp_password": "\"} +end_state: + description: k/v pairs of vtp domain after module execution + returned: always + type: dict + sample: {"domain": "ntc", "version": "2", "vtp_password": "\"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["vtp domain ntc"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'status' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'status' not in command: + 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 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 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_vtp_config(module): + command = 'show vtp status' + + body = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + vtp_parsed = {} + + if body: + version_regex = '.*VTP version running\s+:\s+(?P\d).*' + domain_regex = '.*VTP Domain Name\s+:\s+(?P\S+).*' + + try: + match_version = re.match(version_regex, body, re.DOTALL) + version = match_version.groupdict()['version'] + except AttributeError: + version = '' + + try: + match_domain = re.match(domain_regex, body, re.DOTALL) + domain = match_domain.groupdict()['domain'] + except AttributeError: + domain = '' + + if domain and version: + vtp_parsed['domain'] = domain + vtp_parsed['version'] = version + vtp_parsed['vtp_password'] = get_vtp_password(module) + + return vtp_parsed + + +def get_vtp_password(module): + command = 'show vtp password' + body = execute_show_command(command, module)[0] + password = body['passwd'] + if password: + return str(password) + else: + return "" + + +def main(): + argument_spec = dict( + domain=dict(type='str', required=True), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + domain = module.params['domain'] + + existing = get_vtp_config(module) + end_state = existing + + args = dict(domain=domain) + + changed = False + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + commands = [] + if delta: + commands.append(['vtp domain {0}'.format(domain)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_vtp_config(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() \ No newline at end of file From 6df128e7ebcd7d4d778df57da03be9ced2bc7997 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 13:19:53 +0200 Subject: [PATCH 309/770] Adding missing version_added --- network/nxos/nxos_vtp_domain.py | 1 + 1 file changed, 1 insertion(+) diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 2e4d33a9c53..d833eaedb63 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -18,6 +18,7 @@ DOCUMENTATION = ''' --- module: nxos_vtp +version_added: "2.2" short_description: Manages VTP domain configuration. description: - Manages VTP domain configuration. From 4254d322bee8396d0610c1a58ccc2240e87cca19 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 13:22:45 +0200 Subject: [PATCH 310/770] Fixing module name --- network/nxos/nxos_vtp_domain.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index d833eaedb63..83449e0ab62 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -17,7 +17,7 @@ # DOCUMENTATION = ''' --- -module: nxos_vtp +module: nxos_vtp_domain version_added: "2.2" short_description: Manages VTP domain configuration. description: From c6ed635d97f53d9b8ab14c5881819919569dfe09 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 13:25:48 +0200 Subject: [PATCH 311/770] Removing unused function --- network/nxos/nxos_vtp_domain.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 83449e0ab62..68fc8e20a22 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -334,20 +334,6 @@ def flatten_list(command_lists): return flat_command_list - -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_vtp_config(module): command = 'show vtp status' From fc2d182a8e1aa5cd5fe2d5ec365e9e6baaf1398d Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 13:32:41 +0200 Subject: [PATCH 312/770] Adding nxos_vtp_version --- network/nxos/nxos_vtp_version.py | 415 +++++++++++++++++++++++++++++++ 1 file changed, 415 insertions(+) create mode 100644 network/nxos/nxos_vtp_version.py diff --git a/network/nxos/nxos_vtp_version.py b/network/nxos/nxos_vtp_version.py new file mode 100644 index 00000000000..4979d60a1fe --- /dev/null +++ b/network/nxos/nxos_vtp_version.py @@ -0,0 +1,415 @@ +#!/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_vtp_version +version_added: "2.2" +short_description: Manages VTP version configuration. +description: + - Manages VTP version configuration. +extends_documentation_fragment: nxos +author: + - Gabriele Gerbino (@GGabriele) +notes: + - VTP feature must be active on the device to use this module. + - This module is used to manage only VTP version. + - Use this in combination with M(nxos_vtp_password) and M(nxos_vtp_version) + to fully manage VTP operations. +options: + version: + description: + - VTP version number. + required: true + choices: ['1', '2'] +''' +EXAMPLES = ''' +# ENSURE VTP VERSION IS 2 +- nxos_vtp_version: + version=2 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"version": "2"} +existing: + description: + - k/v pairs of existing vtp + type: dict + sample: {"domain": "testing", "version": "1", "vtp_password": "\"} +end_state: + description: k/v pairs of vtp after module execution + returned: always + type: dict + sample: {"domain": "testing", "version": "2", "vtp_password": "\"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["vtp version 2"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'status' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'status' not in command: + 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 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 get_vtp_config(module): + command = 'show vtp status' + + body = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + vtp_parsed = {} + + if body: + version_regex = '.*VTP version running\s+:\s+(?P\d).*' + domain_regex = '.*VTP Domain Name\s+:\s+(?P\S+).*' + + try: + match_version = re.match(version_regex, body, re.DOTALL) + version = match_version.groupdict()['version'] + except AttributeError: + version = '' + + try: + match_domain = re.match(domain_regex, body, re.DOTALL) + domain = match_domain.groupdict()['domain'] + except AttributeError: + domain = '' + + if domain and version: + vtp_parsed['domain'] = domain + vtp_parsed['version'] = version + vtp_parsed['vtp_password'] = get_vtp_password(module) + + return vtp_parsed + + +def get_vtp_password(module): + command = 'show vtp password' + body = execute_show_command(command, module)[0] + password = body['passwd'] + if password: + return str(password) + else: + return "" + + +def main(): + argument_spec = dict( + version=dict(type='str', choices=['1', '2'], required=True), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + version = module.params['version'] + + existing = get_vtp_config(module) + end_state = existing + + args = dict(version=version) + + changed = False + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + commands = [] + if delta: + commands.append(['vtp version {0}'.format(version)]) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_vtp_config(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() \ No newline at end of file From a2306fa9ce171ed54a1539c4435db4d1f2845107 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 14:10:08 +0200 Subject: [PATCH 313/770] Adding nxos_mtu --- network/nxos/nxos_mtu.py | 594 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 594 insertions(+) create mode 100644 network/nxos/nxos_mtu.py diff --git a/network/nxos/nxos_mtu.py b/network/nxos/nxos_mtu.py new file mode 100644 index 00000000000..c4df536c76b --- /dev/null +++ b/network/nxos/nxos_mtu.py @@ -0,0 +1,594 @@ +#!/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_mtu +version_added: "2.2" +short_description: Manages MTU settings on Nexus switch. +description: + - Manages MTU settings on Nexus switch. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - Either C(sysmtu) param is required or C(interface) AND C(mtu) params are req'd. + - C(state=absent) unconfigures a given MTU if that value is currently present. +options: + interface: + description: + - Full name of interface, i.e. Ethernet1/1. + required: false + default: null + mtu: + description: + - MTU for a specific interface. + required: false + default: null + sysmtu: + description: + - System jumbo MTU. + required: false + default: null + state: + description: + - Specify desired state of the resource. + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +# Ensure system mtu is 9126 +- nxos_mtu: + sysmtu=9216 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Config mtu on Eth1/1 (routed interface) +- nxos_mtu: + interface=Ethernet1/1 + mtu=1600 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Config mtu on Eth1/3 (switched interface) +- nxos_mtu: + interface=Ethernet1/3 + mtu=9216 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Unconfigure mtu on a given interface +- nxos_mtu: + interface=Ethernet1/3 + mtu=9216 host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + state=absent +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"mtu": "1700"} +existing: + description: + - k/v pairs of existing mtu/sysmtu on the interface/system + type: dict + sample: {"mtu": "1600", "sysmtu": "9216"} +end_state: + description: k/v pairs of mtu/sysmtu values after module execution + returned: always + type: dict + sample: {"mtu": "1700", sysmtu": "9216"} +commands: + description: command sent to the device + returned: always + type: list + sample: ["interface vlan10", "mtu 1700"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_mtu(interface, module): + command = 'show interface {0}'.format(interface) + mtu = {} + + body = execute_show_command(command, module) + + try: + mtu_table = body[0]['TABLE_interface']['ROW_interface'] + mtu['mtu'] = str( + mtu_table.get('eth_mtu', + mtu_table.get('svi_mtu', 'unreadable_via_api'))) + mtu['sysmtu'] = get_system_mtu(module)['sysmtu'] + except KeyError: + mtu = {} + + return mtu + + +def get_system_mtu(module): + command = 'show run all | inc jumbomtu' + sysmtu = '' + + body = execute_show_command(command, module, command_type='cli_show_ascii') + + if body: + sysmtu = str(body[0].split(' ')[-1]) + try: + sysmtu = int(sysmtu) + except: + sysmtu = "" + + return dict(sysmtu=str(sysmtu)) + + +def get_commands_config_mtu(delta, interface): + CONFIG_ARGS = { + 'mtu': 'mtu {mtu}', + 'sysmtu': 'system jumbomtu {sysmtu}', + } + + commands = [] + for param, value in delta.iteritems(): + command = CONFIG_ARGS.get(param, 'DNE').format(**delta) + if command and command != 'DNE': + commands.append(command) + command = None + mtu_check = delta.get('mtu', None) + if mtu_check: + commands.insert(0, 'interface {0}'.format(interface)) + return commands + + +def get_commands_remove_mtu(delta, interface): + CONFIG_ARGS = { + 'mtu': 'no mtu {mtu}', + 'sysmtu': 'no system jumbomtu {sysmtu}', + } + commands = [] + for param, value in delta.iteritems(): + command = CONFIG_ARGS.get(param, 'DNE').format(**delta) + if command and command != 'DNE': + commands.append(command) + command = None + mtu_check = delta.get('mtu', None) + if mtu_check: + commands.insert(0, 'interface {0}'.format(interface)) + return commands + + +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 is_default(interface, module): + command = 'show run interface {0}'.format(interface) + + try: + body = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + if body == 'DNE': + return 'DNE' + else: + raw_list = body.split('\n') + if raw_list[-1].startswith('interface'): + return True + else: + return False + except (KeyError): + return 'DNE' + + +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 main(): + argument_spec = dict( + mtu=dict(type='str'), + interface=dict(type='str'), + sysmtu=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + required_together=[['mtu', 'interface']], + supports_check_mode=True) + + interface = module.params['interface'] + mtu = module.params['mtu'] + sysmtu = module.params['sysmtu'] + state = module.params['state'] + + if sysmtu and (interface or mtu): + module.fail_json(msg='Proper usage-- either just use the sysmtu param ' + 'or use interface AND mtu params') + + if interface: + intf_type = get_interface_type(interface) + if intf_type != 'ethernet': + if is_default(interface, module) == 'DNE': + module.fail_json(msg='Invalid interface. It does not exist ' + 'on the switch.') + + existing = get_mtu(interface, module) + else: + existing = get_system_mtu(module) + + if interface and mtu: + if intf_type == 'loopback': + module.fail_json(msg='Cannot set MTU for loopback interface.') + mode = get_interface_mode(interface, intf_type, module) + if mode == 'layer2': + if intf_type in ['ethernet', 'portchannel']: + if mtu not in [existing['sysmtu'], '1500']: + module.fail_json(msg='MTU on L2 interfaces can only be set' + ' to the system default (1500) or ' + 'existing sysmtu value which is ' + ' {0}'.format(existing['sysmtu'])) + elif mode == 'layer3': + if intf_type in ['ethernet', 'portchannel', 'svi']: + if ((int(mtu) < 576 or int(mtu) > 9216) or + ((int(mtu) % 2) != 0)): + module.fail_json(msg='Invalid MTU for Layer 3 interface' + 'needs to be an even number between' + '576 and 9216') + if sysmtu: + if ((int(sysmtu) < 576 or int(sysmtu) > 9216 or + ((int(sysmtu) % 2) != 0))): + module.fail_json(msg='Invalid MTU- needs to be an even ' + 'number between 576 and 9216') + + args = dict(mtu=mtu, sysmtu=sysmtu) + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + changed = False + end_state = existing + commands = [] + + if state == 'present': + if delta: + command = get_commands_config_mtu(delta, interface) + commands.append(command) + + elif state == 'absent': + common = set(proposed.iteritems()).intersection(existing.iteritems()) + if common: + command = get_commands_remove_mtu(dict(common), interface) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + if interface: + end_state = get_mtu(interface, module) + else: + end_state = get_system_mtu(module) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + + +if __name__ == '__main__': + main() \ No newline at end of file From fd1593aeac00f996e1398f18034030522ea1ef8e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 16:19:36 +0200 Subject: [PATCH 314/770] Fixing docstring --- network/nxos/nxos_mtu.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_mtu.py b/network/nxos/nxos_mtu.py index c4df536c76b..379eca9cdb5 100644 --- a/network/nxos/nxos_mtu.py +++ b/network/nxos/nxos_mtu.py @@ -102,7 +102,7 @@ returned: always type: dict sample: {"mtu": "1700", sysmtu": "9216"} -commands: +updates: description: command sent to the device returned: always type: list From 668778874a73781a7921ae44ca972ae91f11d14e Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 16:25:32 +0200 Subject: [PATCH 315/770] Adding nxos_pim_interface --- network/nxos/nxos_pim_interface.py | 929 +++++++++++++++++++++++++++++ 1 file changed, 929 insertions(+) create mode 100644 network/nxos/nxos_pim_interface.py diff --git a/network/nxos/nxos_pim_interface.py b/network/nxos/nxos_pim_interface.py new file mode 100644 index 00000000000..42413f537de --- /dev/null +++ b/network/nxos/nxos_pim_interface.py @@ -0,0 +1,929 @@ +#!/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: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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(), + hello_auth_key=dict(), + hello_interval=dict(type='int'), + jp_policy_out=dict(), + jp_policy_in=dict(), + jp_type_out=dict(choices=['prefix', 'routemap']), + jp_type_in=dict(choices=['prefix', 'routemap']), + border=dict(type='bool'), + neighbor_policy=dict(), + 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) + + 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() \ No newline at end of file From affa177fed6034ec2a17752524f17e580d0c1aa8 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 16:34:26 +0200 Subject: [PATCH 316/770] Improving argument_spec --- network/nxos/nxos_pim_interface.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_pim_interface.py b/network/nxos/nxos_pim_interface.py index 42413f537de..00ace4b9fb8 100644 --- a/network/nxos/nxos_pim_interface.py +++ b/network/nxos/nxos_pim_interface.py @@ -800,15 +800,15 @@ def main(): argument_spec=dict( interface=dict(required=True), sparse=dict(type='bool', default=True), - dr_prio=dict(), - hello_auth_key=dict(), + dr_prio=dict(type='str'), + hello_auth_key=dict(type='str'), hello_interval=dict(type='int'), - jp_policy_out=dict(), - jp_policy_in=dict(), + 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(), + neighbor_policy=dict(type='str'), neighbor_type=dict(choices=['prefix', 'routemap']), state=dict(choices=['present', 'absent', 'default'], default='present'), From 344927a6b11bf1ea56693f8ba0fc7a5afa11dedc Mon Sep 17 00:00:00 2001 From: Riccardo Murri Date: Thu, 15 Sep 2016 16:58:33 +0200 Subject: [PATCH 317/770] hostname: Support "Scientific Linux CERN" (#4855) CERN maintains its own fork of "Scientific Linux", which identifies as "Scientific Linux CERN SLC". This commit lets Ansible know that this is again another variant of RHEL. --- system/hostname.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/hostname.py b/system/hostname.py index d16b1ccca28..c56572ebade 100644 --- a/system/hostname.py +++ b/system/hostname.py @@ -567,6 +567,11 @@ class ScientificLinuxHostname(Hostname): distribution = 'Scientific linux' strategy_class = RedHatStrategy +class ScientificLinuxCERNHostname(Hostname): + platform = 'Linux' + distribution = 'Scientific linux cern slc' + strategy_class = RedHatStrategy + class OracleLinuxHostname(Hostname): platform = 'Linux' distribution = 'Oracle linux server' From 4a74c58ead987158ee6b8113f5e32c1cc0db91ec Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Thu, 15 Sep 2016 08:42:10 -0700 Subject: [PATCH 318/770] add win_shell/win_command modules + docs (#4827) --- windows/win_command.ps1 | 131 ++++++++++++++++++++++++++++++++++++++++ windows/win_command.py | 121 +++++++++++++++++++++++++++++++++++++ windows/win_shell.ps1 | 101 +++++++++++++++++++++++++++++++ windows/win_shell.py | 129 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 482 insertions(+) create mode 100644 windows/win_command.ps1 create mode 100644 windows/win_command.py create mode 100644 windows/win_shell.ps1 create mode 100644 windows/win_shell.py diff --git a/windows/win_command.ps1 b/windows/win_command.ps1 new file mode 100644 index 00000000000..5e934abe0f0 --- /dev/null +++ b/windows/win_command.ps1 @@ -0,0 +1,131 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + +# TODO: add check mode support + +Set-StrictMode -Version 2 +$ErrorActionPreference = "Stop" + +$parsed_args = Parse-Args $args $false + +$raw_command_line = $(Get-AnsibleParam $parsed_args "_raw_params" -failifempty $true).Trim() +$chdir = Get-AnsibleParam $parsed_args "chdir" +$creates = Get-AnsibleParam $parsed_args "creates" +$removes = Get-AnsibleParam $parsed_args "removes" + +$result = @{changed=$true; warnings=@(); cmd=$raw_command_line} + +If($creates -and $(Test-Path $creates)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $creates exists"; changed=$false; skipped=$true; rc=0} +} + +If($removes -and -not $(Test-Path $removes)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $removes does not exist"; changed=$false; skipped=$true; rc=0} +} + +$util_def = @' +using System; +using System.ComponentModel; +using System.Linq; +using System.Runtime.InteropServices; + +namespace Ansible.Command +{ + public static class NativeUtil + { + [DllImport("shell32.dll", SetLastError = true)] + static extern IntPtr CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, out int pNumArgs); + + public static string[] ParseCommandLine(string cmdline) + { + int numArgs; + IntPtr ret = CommandLineToArgvW(cmdline, out numArgs); + + if (ret == IntPtr.Zero) + throw new Exception(String.Format("Error parsing command line: {0}", new Win32Exception(Marshal.GetLastWin32Error()).Message)); + + IntPtr[] strptrs = new IntPtr[numArgs]; + Marshal.Copy(ret, strptrs, 0, numArgs); + string[] cmdlineParts = strptrs.Select(s=>Marshal.PtrToStringUni(s)).ToArray(); + + Marshal.FreeHGlobal(ret); + + return cmdlineParts; + } + } +} +'@ + +$util_type = Add-Type -TypeDefinition $util_def + +# FUTURE: extract this code to separate module_utils as Windows module API version of run_command + +$exec_args = $null + +# Parse the command-line with the Win32 parser to get the application name to run. The Win32 parser +# will deal with quoting/escaping for us... +# FUTURE: no longer necessary once we switch to raw Win32 CreateProcess +$parsed_command_line = [Ansible.Command.NativeUtil]::ParseCommandLine($raw_command_line); +$exec_application = $parsed_command_line[0] +If($parsed_command_line.Length -gt 1) { + # lop the application off, then rejoin the args as a single string + $exec_args = $parsed_command_line[1..$($parsed_command_line.Length-1)] -join " " +} + +$proc = New-Object System.Diagnostics.Process +$psi = $proc.StartInfo +$psi.FileName = $exec_application +$psi.Arguments = $exec_args +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false + +If ($chdir) { + $psi.WorkingDirectory = $chdir +} + +$start_datetime = [DateTime]::UtcNow + +Try { + $proc.Start() | Out-Null # will always return $true for non shell-exec cases +} +Catch [System.ComponentModel.Win32Exception] { + # fail nicely for "normal" error conditions + # FUTURE: this probably won't work on Nano Server + $excep = $_ + Exit-Json @{failed=$true;changed=$false;cmd=$raw_command_line;rc=$excep.Exception.NativeErrorCode;msg=$excep.Exception.Message} +} + +# TODO: resolve potential deadlock here if stderr fills buffer (~4k) before stdout is closed, +# perhaps some async stream pumping with Process Output/ErrorDataReceived events... + +$result.stdout = $proc.StandardOutput.ReadToEnd() +$result.stderr = $proc.StandardError.ReadToEnd() + +$proc.WaitForExit() | Out-Null + +$result.rc = $proc.ExitCode + +$end_datetime = [DateTime]::UtcNow + +$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff") + +ConvertTo-Json -Depth 99 $result diff --git a/windows/win_command.py b/windows/win_command.py new file mode 100644 index 00000000000..2d76a0b8534 --- /dev/null +++ b/windows/win_command.py @@ -0,0 +1,121 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Ansible, inc +# +# 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: win_command +short_description: Executes a command on a remote Windows node +version_added: 2.2 +description: + - The M(win_command) module takes the command name followed by a list of space-delimited arguments. + - The given command will be executed on all selected nodes. It will not be + processed through the shell, so variables like C($env:HOME) and operations + like C("<"), C(">"), C("|"), and C(";") will not work (use the M(win_shell) + module if you need these features). +options: + free_form: + description: + - the win_command module takes a free form command to run. There is no parameter actually named 'free form'. + See the examples! + required: true + creates: + description: + - a path or path filter pattern; when the referenced path exists on the target host, the task will be skipped. + removes: + description: + - a path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped. + chdir: + description: + - set the specified path as the current working directory before executing a command +notes: + - If you want to run a command through a shell (say you are using C(<), + C(>), C(|), etc), you actually want the M(win_shell) module instead. The + M(win_command) module is much more secure as it's not affected by the user's + environment. + - " C(creates), C(removes), and C(chdir) can be specified after the command. For instance, if you only want to run a command if a certain file does not exist, use this." +author: + - Matt Davis +''' + +EXAMPLES = ''' +# Example from Ansible Playbooks. +- win_command: whoami + register: whoami_out + +# Run the command only if the specified file does not exist. +- win_command: wbadmin -backupTarget:c:\\backup\\ creates=c:\\backup\\ + +# You can also use the 'args' form to provide the options. This command +# will change the working directory to c:\\somedir\\ and will only run when +# c:\\backup\\ doesn't exist. +- win_command: wbadmin -backupTarget:c:\\backup\\ creates=c:\\backup\\ + args: + chdir: c:\\somedir\\ + creates: c:\\backup\\ +''' + +RETURN = ''' +msg: + description: changed + returned: always + type: boolean + sample: True +start: + description: The command execution start time + returned: always + type: string + sample: '2016-02-25 09:18:26.429568' +end: + description: The command execution end time + returned: always + type: string + sample: '2016-02-25 09:18:26.755339' +delta: + description: The command execution delta time + returned: always + type: string + sample: '0:00:00.325771' +stdout: + description: The command standard output + returned: always + type: string + sample: 'Clustering node rabbit@slave1 with rabbit@master ...' +stderr: + description: The command standard error + returned: always + type: string + sample: 'ls: cannot access foo: No such file or directory' +cmd: + description: The command executed by the task + returned: always + type: string + sample: 'rabbitmqctl join_cluster rabbit@master' +rc: + description: The command return code (0 means success) + returned: always + type: int + sample: 0 +stdout_lines: + description: The command standard output split in lines + returned: always + type: list of strings + sample: [u'Clustering node rabbit@slave1 with rabbit@master ...'] +''' diff --git a/windows/win_shell.ps1 b/windows/win_shell.ps1 new file mode 100644 index 00000000000..750ca121ad4 --- /dev/null +++ b/windows/win_shell.ps1 @@ -0,0 +1,101 @@ +#!powershell +# 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 . + +# WANT_JSON +# POWERSHELL_COMMON + +# TODO: add check mode support + +Set-StrictMode -Version 2 +$ErrorActionPreference = "Stop" + +$parsed_args = Parse-Args $args $false + +$raw_command_line = $(Get-AnsibleParam $parsed_args "_raw_params" -failifempty $true).Trim() +$chdir = Get-AnsibleParam $parsed_args "chdir" +$executable = Get-AnsibleParam $parsed_args "executable" +$creates = Get-AnsibleParam $parsed_args "creates" +$removes = Get-AnsibleParam $parsed_args "removes" + +$result = @{changed=$true; warnings=@(); cmd=$raw_command_line} + +If($creates -and $(Test-Path $creates)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $creates exists"; changed=$false; skipped=$true; rc=0} +} + +If($removes -and -not $(Test-Path $removes)) { + Exit-Json @{cmd=$raw_command_line; msg="skipped, since $removes does not exist"; changed=$false; skipped=$true; rc=0} +} + +$exec_args = $null + +If(-not $executable -or $executable -eq "powershell") { + $exec_application = "powershell" + + # Base64 encode the command so we don't have to worry about the various levels of escaping + $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_command_line)) + + $exec_args = @("-noninteractive", "-encodedcommand", $encoded_command) +} +Else { + # FUTURE: support arg translation from executable (or executable_args?) to process arguments for arbitrary interpreter? + $exec_application = $executable + $exec_args = @("/c", $raw_command_line) +} + +$proc = New-Object System.Diagnostics.Process +$psi = $proc.StartInfo +$psi.FileName = $exec_application +$psi.Arguments = $exec_args +$psi.RedirectStandardOutput = $true +$psi.RedirectStandardError = $true +$psi.UseShellExecute = $false + +If ($chdir) { + $psi.WorkingDirectory = $chdir +} + +$start_datetime = [DateTime]::UtcNow + +Try { + $proc.Start() | Out-Null # will always return $true for non shell-exec cases +} +Catch [System.ComponentModel.Win32Exception] { + # fail nicely for "normal" error conditions + # FUTURE: this probably won't work on Nano Server + $excep = $_ + Exit-Json @{failed=$true;changed=$false;cmd=$raw_command_line;rc=$excep.Exception.NativeErrorCode;msg=$excep.Exception.Message} +} + +# TODO: resolve potential deadlock here if stderr fills buffer (~4k) before stdout is closed, +# perhaps some async stream pumping with Process Output/ErrorDataReceived events... + +$result.stdout = $proc.StandardOutput.ReadToEnd() +$result.stderr = $proc.StandardError.ReadToEnd() + +# TODO: decode CLIXML stderr output (and other streams?) + +$proc.WaitForExit() | Out-Null + +$result.rc = $proc.ExitCode + +$end_datetime = [DateTime]::UtcNow + +$result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") +$result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff") + +ConvertTo-Json -Depth 99 $result diff --git a/windows/win_shell.py b/windows/win_shell.py new file mode 100644 index 00000000000..7c4dc68df63 --- /dev/null +++ b/windows/win_shell.py @@ -0,0 +1,129 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2016, Ansible, inc +# +# 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: win_shell +short_description: Execute shell commands on target hosts. +version_added: 2.2 +description: + - The M(win_shell) module takes the command name followed by a list of space-delimited arguments. + It is similar to the M(win_command) module, but runs + the command via a shell (defaults to PowerShell) on the target host. +options: + free_form: + description: + - the win_shell module takes a free form command to run. There is no parameter actually named 'free form'. + See the examples! + required: true + creates: + description: + - a path or path filter pattern; when the referenced path exists on the target host, the task will be skipped. + removes: + description: + - a path or path filter pattern; when the referenced path B(does not) exist on the target host, the task will be skipped. + chdir: + description: + - set the specified path as the current working directory before executing a command + executable: + description: + - change the shell used to execute the command (eg, C(cmd)). The target shell must accept a C(/c) parameter followed by the raw command line to be executed. +notes: + - If you want to run an executable securely and predictably, it may be + better to use the M(win_command) module instead. Best practices when writing + playbooks will follow the trend of using M(win_command) unless M(win_shell) is + explicitly required. When running ad-hoc commands, use your best judgement. + - WinRM will not return from a command execution until all child processes created have exited. Thus, it is not possible to use win_shell to spawn long-running child or background processes. + Consider creating a Windows service for managing background processes. +author: + - Matt Davis +''' + +EXAMPLES = ''' +# Execute a command in the remote shell; stdout goes to the specified +# file on the remote. +- win_shell: C:\\somescript.ps1 >> c:\\somelog.txt + +# Change the working directory to somedir/ before executing the command. +- win_shell: C:\\somescript.ps1 >> c:\\somelog.txt chdir=c:\\somedir + +# You can also use the 'args' form to provide the options. This command +# will change the working directory to somedir/ and will only run when +# somedir/somelog.txt doesn't exist. +- win_shell: C:\\somescript.ps1 >> c:\\somelog.txt + args: + chdir: c:\\somedir + creates: c:\\somelog.txt + +# Run a command under a non-Powershell interpreter (cmd in this case) +- win_shell: echo %HOMEDIR% + args: + executable: cmd + register: homedir_out +''' + +RETURN = ''' +msg: + description: changed + returned: always + type: boolean + sample: True +start: + description: The command execution start time + returned: always + type: string + sample: '2016-02-25 09:18:26.429568' +end: + description: The command execution end time + returned: always + type: string + sample: '2016-02-25 09:18:26.755339' +delta: + description: The command execution delta time + returned: always + type: string + sample: '0:00:00.325771' +stdout: + description: The command standard output + returned: always + type: string + sample: 'Clustering node rabbit@slave1 with rabbit@master ...' +stderr: + description: The command standard error + returned: always + type: string + sample: 'ls: cannot access foo: No such file or directory' +cmd: + description: The command executed by the task + returned: always + type: string + sample: 'rabbitmqctl join_cluster rabbit@master' +rc: + description: The command return code (0 means success) + returned: always + type: int + sample: 0 +stdout_lines: + description: The command standard output split in lines + returned: always + type: list of strings + sample: [u'Clustering node rabbit@slave1 with rabbit@master ...'] +''' From e38b79cd41b9cbc0593b9e97b660c16bd962786e Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Thu, 15 Sep 2016 09:03:01 -0700 Subject: [PATCH 319/770] dnos6_config (New Module) (#4824) * Added support for dnos6_config module * Corrected the documention failure * Addressed @gundalow comments --- network/dnos6/dnos6_config.py | 290 ++++++++++++++++++++++++++++++++++ 1 file changed, 290 insertions(+) create mode 100644 network/dnos6/dnos6_config.py diff --git a/network/dnos6/dnos6_config.py b/network/dnos6/dnos6_config.py new file mode 100644 index 00000000000..2293a2deb31 --- /dev/null +++ b/network/dnos6/dnos6_config.py @@ -0,0 +1,290 @@ +#!/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: dnos6_config +version_added: "2.2" +author: "Abirami N(@abirami-n)" +short_description: Manage Dell OS6 configuration sections +description: + - Dell OS6 configurations use a simple block indent file syntax + for segmenting configuration into sections. This module provides + an implementation for working with Dell OS6 configuration sections in + a deterministic way. +extends_documentation_fragment: dnos6 +options: + lines: + description: + - The ordered set of commands that should be configured in the + section. The commands must be the exact same commands as found + in the device running-config. Be sure to note the configuration + command syntax as some commands are automatically modified by the + device config parser. This argument is mutually exclusive with I(src). + required: false + default: null + aliases: ['commands'] + parents: + description: + - The ordered set of parents that uniquely identify the section + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top + level or global commands. + required: false + default: null + src: + description: + - Specifies the source path to the file that contains the configuration + or configuration template to load. The path to the source file can + either be the full path on the Ansible control host or a relative + path from the playbook or role root dir. This argument is mutually + exclusive with I(lines). + required: false + default: null + before: + description: + - The ordered set of commands to push on to the command stack if + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing + any changes without affecting how the set of commands are matched + against the system. + required: false + default: null + after: + description: + - The ordered set of commands to append to the end of the command + stack if a change needs to be made. Just like with I(before) this + allows the playbook designer to append a set of commands to be + executed after the command set. + required: false + default: null + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. If + match is set to I(line), commands are matched line by line. If + match is set to I(strict), command lines are matched with respect + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. + required: false + default: line + choices: ['line', 'strict', 'exact', 'none'] + replace: + description: + - Instructs the module on the way to perform the configuration + on the device. If the replace argument is set to I(line) then + the modified lines are pushed to the device in configuration + mode. If the replace argument is set to I(block) then the entire + command block is pushed to the device in configuration mode if any + line is not correct. + required: false + default: line + choices: ['line', 'block'] + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) and I(check). When the argument is set to + I(merge), the configuration changes are merged with the current + device running configuration. When the argument is set to I(check) + the configuration updates are determined but not actually configured + on the remote device. + required: false + default: merge + choices: ['merge', 'check'] + save: + description: + - The C(save) argument instructs the module to save the running- + config to the startup-config at the conclusion of the module + running. If check mode is specified, this argument is ignored. + required: false + default: no + choices: ['yes', 'no'] + config: + description: + - The C(config) argument allows the playbook designer to supply + the base configuration to be used to validate configuration + changes necessary. If this argument is provided, the module + will not download the running-config from the remote node. + required: false + default: null + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no + choices: ['yes', 'no'] +""" + +EXAMPLES = """ +- dnos6_config: + lines: ['hostname {{ inventory_hostname }}'] + provider: "{{ cli }}" + +- dnos6_config: + lines: + - 10 permit ip 1.1.1.1 any log + - 20 permit ip 2.2.2.2 any log + - 30 permit ip 3.3.3.3 any log + - 40 permit ip 4.4.4.4 any log + - 50 permit ip 5.5.5.5 any log + parents: ['ip access-list test'] + before: ['no ip access-list test'] + match: exact + provider: "{{ cli }}" + +- dnos6_config: + lines: + - 10 permit ip 1.1.1.1 any log + - 20 permit ip 2.2.2.2 any log + - 30 permit ip 3.3.3.3 any log + - 40 permit ip 4.4.4.4 any log + parents: ['ip access-list test'] + before: ['no ip access-list test'] + replace: block + provider: "{{ cli }}" + +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + returned: when not check_mode + type: list + sample: ['...', '...'] + +saved: + description: Returns whether the configuration is saved to the startup + configuration or not. + returned: when not check_mode + type: bool + sample: True + +""" +from ansible.module_utils.netcfg import NetworkConfig, dumps, ConfigLine +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.dnos6 import get_config + +def get_candidate(module): + candidate = NetworkConfig(indent=1) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) + return candidate + +def get_contents(other,module): + contents =list() + + parent = ''.join(module.params['parents']) + start = False + for item in other.items: + if item.text == parent: + start = True + elif item.text != 'exit' and start: + contents.append(item.text) + elif item.text == 'exit' and start: + start = False + break + return contents + +def main(): + argument_spec = dict( + lines=dict(aliases=['commands'], type='list'), + parents=dict(type='list'), + + src=dict(type='path'), + + before=dict(type='list'), + after=dict(type='list'), + + match=dict(default='line', + choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + update=dict(choices=['merge', 'check'], default='merge'), + save=dict(type='bool', default=False), + config=dict(), + backup=dict(type='bool', default=False) + ) + + mutually_exclusive = [('lines', 'src')] + + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + parents = module.params['parents'] or list() + + match = module.params['match'] + replace = module.params['replace'] + before = module.params['before'] + result = dict(changed=False, saved=False) + candidate = get_candidate(module) + + if module.params['match'] != 'none': + config = get_config(module) + if parents: + con = get_contents(config,module) + config = NetworkConfig(indent=1) + config.add(con,parents=module.params['parents']) + configobjs = candidate.difference(config, match=match, replace=replace) + else: + configobjs = candidate.items + + if module.params['backup']: + result['__backup__'] = module.cli('show running-config')[0] + commands = list() + if configobjs: + commands = dumps(configobjs, 'commands') + commands = commands.split('\n') + if module.params['before']: + commands[:0] = before + if module.params['after']: + commands.extend(module.params['after']) + if not module.check_mode and module.params['update'] == 'merge': + response = module.config.load_config(commands) + result['responses'] = response + + if module.params['save']: + module.config.save_config() + result['saved'] = True + + + result['changed'] = True + + result['updates'] = commands + + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 44a23e2a201fd0d0edfeb89e4b7e0efd22048335 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Thu, 15 Sep 2016 09:03:17 -0700 Subject: [PATCH 320/770] dnos9_template (New Module) (#4819) * Added support for dnos9_template module * Addressed @gundalow comments --- network/dnos9/dnos9_template.py | 175 ++++++++++++++++++++++++++++++++ 1 file changed, 175 insertions(+) create mode 100755 network/dnos9/dnos9_template.py diff --git a/network/dnos9/dnos9_template.py b/network/dnos9/dnos9_template.py new file mode 100755 index 00000000000..0e9bad427f5 --- /dev/null +++ b/network/dnos9/dnos9_template.py @@ -0,0 +1,175 @@ +#!/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: dnos9_template +version_added: "2.2" +author: "Dhivya P (@dhivyap)" +short_description: Manage Dell OS9 device configurations over SSH. +description: + - Manages Dell OS9 network device configurations over SSH. This module + allows implementors to work with the device running-config. It + provides a way to push a set of commands onto a network device + by evaluating the current running-config and only pushing configuration + commands that are not already configured. The config source can + be a set of commands or a template. +extends_documentation_fragment: dnos9 +options: + src: + description: + - The path to the config source. The source can be either a + file with config or a template that will be merged during + runtime. By default the task will first search for the source + file in role or playbook root folder in templates unless a full + path to the file is given. + required: true + force: + description: + - The force argument instructs the module not to consider the + current device running-config. When set to true, this will + cause the module to push the contents of I(src) into the device + without first checking if already configured. This argument is + mutually exclusive with I(config). + required: false + default: false + choices: [ "true", "false" ] + backup: + description: + - When this argument is configured true, the module will backup + the running-config from the node prior to making any changes. + The backup file will be written to backup_{{ hostname }} in + the root of the playbook directory. This argument is + mutually exclusive with I(config). + + required: false + default: false + choices: [ "true", "false" ] + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task. The I(config) argument allows the implementer to + pass in the configuration to use as the base config for + comparison. This argument is mutually exclusive with + I(force) and I(backup). + + required: false + default: null +""" + +EXAMPLES = """ +- name: push a configuration onto the device + dnos9_template: + host: hostname + username: foo + src: config.j2 + +- name: forceable push a configuration onto the device + dnos9_template: + host: hostname + username: foo + src: config.j2 + force: yes + +- name: provide the base configuration for comparison + dnos9_template: + host: hostname + username: foo + src: candidate_config.txt + config: current_config.txt +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +_backup: + description: The current running config of the remote device. + returned: when running config is present in the remote device. + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + returned: when not check_mode + type: list + sample: ['...', '...'] +""" +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.dnos9 + + +def get_config(module): + config = module.params['config'] or dict() + if not config and not module.params['force']: + config = module.config.get_config() + return config + + +def main(): + """ main entry point for module execution + """ + + argument_spec = dict( + src=dict(), + force=dict(default=False, type='bool'), + backup=dict(default=False, type='bool'), + config=dict(), + ) + + mutually_exclusive = [('config', 'backup'), ('config', 'force')] + + module = NetworkModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + result = dict(changed=False) + + candidate = NetworkConfig(contents=module.params['src'], indent=1) + + contents = get_config(module) + + if contents: + config = NetworkConfig(contents=contents[0], indent=1) + result['_backup'] = contents[0] + + commands = list() + if not module.params['force']: + commands = dumps(candidate.difference(config), 'commands') + else: + commands = str(candidate) + + if commands: + commands = commands.split('\n') + if not module.check_mode: + response = module.config(commands) + result['responses'] = response + result['changed'] = True + + result['updates'] = commands + module.exit_json(**result) + + +if __name__ == '__main__': + main() From 690730b2c85c39a5efce858832422ab67021f317 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Thu, 15 Sep 2016 09:03:35 -0700 Subject: [PATCH 321/770] dnos9_config (New Module) (#4816) * Added support for dnos9_config module * Addressed @gundalow comments --- network/dnos9/dnos9_config.py | 281 ++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100755 network/dnos9/dnos9_config.py diff --git a/network/dnos9/dnos9_config.py b/network/dnos9/dnos9_config.py new file mode 100755 index 00000000000..762e7b3ac86 --- /dev/null +++ b/network/dnos9/dnos9_config.py @@ -0,0 +1,281 @@ +#!/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: dnos9_config +version_added: "2.2" +author: "Dhivya P (@dhivyap)" +short_description: Manage Dell OS9 configuration sections +description: + - Dell OS9 configurations use a simple block indent file syntax + for segmenting configuration into sections. This module provides + an implementation for working with Dell OS9 configuration sections in + a deterministic way. +extends_documentation_fragment: dnos9 +options: + lines: + description: + - The ordered set of commands that should be configured in the + section. The commands must be the exact same commands as found + in the device running-config. Be sure to note the configuration + command syntax as some commands are automatically modified by the + device config parser. This argument is mutually exclusive with I(src). + required: false + default: null + aliases: ['commands'] + parents: + description: + - The ordered set of parents that uniquely identify the section + the commands should be checked against. If the parents argument + is omitted, the commands are checked against the set of top + level or global commands. + required: false + default: null + src: + description: + - Specifies the source path to the file that contains the configuration + or configuration template to load. The path to the source file can + either be the full path on the Ansible control host or a relative + path from the playbook or role root dir. This argument is mutually + exclusive with I(lines). + required: false + default: null + before: + description: + - The ordered set of commands to push on to the command stack if + a change needs to be made. This allows the playbook designer + the opportunity to perform configuration commands prior to pushing + any changes without affecting how the set of commands are matched + against the system. + required: false + default: null + after: + description: + - The ordered set of commands to append to the end of the command + stack if a change needs to be made. Just like with I(before) this + allows the playbook designer to append a set of commands to be + executed after the command set. + required: false + default: null + match: + description: + - Instructs the module on the way to perform the matching of + the set of commands against the current device config. If + match is set to I(line), commands are matched line by line. If + match is set to I(strict), command lines are matched with respect + to position. If match is set to I(exact), command lines + must be an equal match. Finally, if match is set to I(none), the + module will not attempt to compare the source configuration with + the running configuration on the remote device. + required: false + default: line + choices: ['line', 'strict', 'exact', 'none'] + replace: + description: + - Instructs the module on the way to perform the configuration + on the device. If the replace argument is set to I(line) then + the modified lines are pushed to the device in configuration + mode. If the replace argument is set to I(block) then the entire + command block is pushed to the device in configuration mode if any + line is not correct. + required: false + default: line + choices: ['line', 'block'] + update: + description: + - The I(update) argument controls how the configuration statements + are processed on the remote device. Valid choices for the I(update) + argument are I(merge) and I(check). When the argument is set to + I(merge), the configuration changes are merged with the current + device running configuration. When the argument is set to I(check) + the configuration updates are determined but not actually configured + on the remote device. + required: false + default: merge + choices: ['merge', 'check'] + save: + description: + - The C(save) argument instructs the module to save the running- + config to the startup-config at the conclusion of the module + running. If check mode is specified, this argument is ignored. + required: false + default: no + choices: ['yes', 'no'] + config: + description: + - The C(config) argument allows the playbook designer to supply + the base configuration to be used to validate configuration + changes necessary. If this argument is provided, the module + will not download the running-config from the remote node. + required: false + default: null + backup: + description: + - This argument will cause the module to create a full backup of + the current C(running-config) from the remote device before any + changes are made. The backup file is written to the C(backup) + folder in the playbook root directory. If the directory does not + exist, it is created. + required: false + default: no + choices: ['yes', 'no'] +""" + +EXAMPLES = """ +- dnos9_config: + lines: ['hostname {{ inventory_hostname }}'] + provider: "{{ cli }}" + +- dnos9_config: + lines: + - 10 permit ip host 1.1.1.1 any log + - 20 permit ip host 2.2.2.2 any log + - 30 permit ip host 3.3.3.3 any log + - 40 permit ip host 4.4.4.4 any log + - 50 permit ip host 5.5.5.5 any log + parents: ['ip access-list extended test'] + before: ['no ip access-list extended test'] + match: exact + provider: "{{ cli }}" + +- dnos9_config: + lines: + - 10 permit ip host 1.1.1.1 any log + - 20 permit ip host 2.2.2.2 any log + - 30 permit ip host 3.3.3.3 any log + - 40 permit ip host 4.4.4.4 any log + parents: ['ip access-list extended test'] + before: ['no ip access-list extended test'] + replace: block + provider: "{{ cli }}" + +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + retured: when not check_mode + type: list + sample: ['...', '...'] + +saved: + description: Returns whether the configuration is saved to the startup + configuration or not. + retured: when not check_mode + type: bool + sample: True + +""" +from ansible.module_utils.netcfg import NetworkConfig, dumps +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.dnos9 import get_config, get_sublevel_config + + +def get_candidate(module): + candidate = NetworkConfig(indent=1) + if module.params['src']: + candidate.load(module.params['src']) + elif module.params['lines']: + parents = module.params['parents'] or list() + candidate.add(module.params['lines'], parents=parents) + return candidate + + +def main(): + + argument_spec = dict( + lines=dict(aliases=['commands'], type='list'), + parents=dict(type='list'), + + src=dict(type='path'), + + before=dict(type='list'), + after=dict(type='list'), + + match=dict(default='line', + choices=['line', 'strict', 'exact', 'none']), + replace=dict(default='line', choices=['line', 'block']), + + update=dict(choices=['merge', 'check'], default='merge'), + save=dict(type='bool', default=False), + config=dict(), + backup=dict(type='bool', default=False) + ) + + mutually_exclusive = [('lines', 'src')] + + module = NetworkModule(argument_spec=argument_spec, + connect_on_load=False, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + parents = module.params['parents'] or list() + + match = module.params['match'] + replace = module.params['replace'] + result = dict(changed=False, saved=False) + + candidate = get_candidate(module) + + if match != 'none': + config = get_config(module) + if parents: + contents = get_sublevel_config(config, module) + config = NetworkConfig(contents=contents, indent=1) + configobjs = candidate.difference(config, match=match, replace=replace) + + else: + configobjs = candidate.items + + if module.params['backup']: + result['__backup__'] = module.cli('show running-config')[0] + + commands = list() + if configobjs: + commands = dumps(configobjs, 'commands') + commands = commands.split('\n') + + if module.params['before']: + commands[:0] = module.params['before'] + + if module.params['after']: + commands.extend(module.params['after']) + + if not module.check_mode and module.params['update'] == 'merge': + response = module.config.load_config(commands) + result['responses'] = response + + if module.params['save']: + module.config.save_config() + result['saved'] = True + + result['changed'] = True + + result['updates'] = commands + + module.exit_json(**result) + +if __name__ == '__main__': + main() From 5c9afbcdb8473c9cfe49739949f99b49244772ed Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 18:44:34 +0200 Subject: [PATCH 322/770] Adding nxos_snmp_user --- network/nxos/nxos_snmp_user.py | 558 +++++++++++++++++++++++++++++++++ 1 file changed, 558 insertions(+) create mode 100644 network/nxos/nxos_snmp_user.py diff --git a/network/nxos/nxos_snmp_user.py b/network/nxos/nxos_snmp_user.py new file mode 100644 index 00000000000..5e9eec29a48 --- /dev/null +++ b/network/nxos/nxos_snmp_user.py @@ -0,0 +1,558 @@ +#!/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_snmp_user +version_added: "2.2" +short_description: Manages SNMP users for monitoring. +description: + - Manages SNMP user configuration. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - Authentication parameters not idempotent. +options: + user: + description: + - Name of the user. + required: true + group: + description: + - Group to which the user will belong to. + required: true + auth: + description: + - Auth parameters for the user. + required: false + default: null + choices: ['md5', 'sha'] + pwd: + description: + - Auth password when using md5 or sha. + required: false + default: null + privacy: + description: + - Privacy password for the user. + required: false + default: null + encrypt: + description: + - Enables AES-128 bit encryption when using privacy password. + required: false + default: null + choices: ['true','false'] + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' + +EXAMPLES = ''' +- nxos_snmp_user: + user=ntc + group=network-operator + auth=md5 + pwd=test_password + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"authentication": "md5", "group": "network-operator", + "pwd": "test_password", "user": "ntc"} +existing: + description: + - k/v pairs of existing configuration + type: dict + sample: {"authentication": "no", "encrypt": "none", + "group": ["network-operator"], "user": "ntc"} +end_state: + description: k/v pairs configuration vtp after module execution + returned: always + type: dict + sample: {"authentication": "md5", "encrypt": "none", + "group": ["network-operator"], "user": "ntc"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["snmp-server user ntc network-operator auth md5 test_password"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' +import json + +# 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': + body = [] + elif 'show run' in command or text: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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 get_snmp_groups(module): + command = 'show snmp group' + body = execute_show_command(command, module) + g_list = [] + + try: + group_table = body[0]['TABLE_role']['ROW_role'] + for each in group_table: + g_list.append(each['role_name']) + + except (KeyError, AttributeError, IndexError): + return g_list + + return g_list + + +def get_snmp_user(user, module): + command = 'show snmp user {0}'.format(user) + body = execute_show_command(command, module, text=True) + + if 'No such entry' not in body[0]: + body = execute_show_command(command, module) + + resource = {} + group_list = [] + try: + resource_table = body[0]['TABLE_snmp_users']['ROW_snmp_users'] + resource['user'] = str(resource_table['user']) + resource['authentication'] = str(resource_table['auth']).strip() + encrypt = str(resource_table['priv']).strip() + if encrypt.startswith('aes'): + resource['encrypt'] = 'aes-128' + else: + resource['encrypt'] = 'none' + + group_table = resource_table['TABLE_groups']['ROW_groups'] + + groups = [] + try: + for group in group_table: + groups.append(str(group['group']).strip()) + except TypeError: + groups.append(str(group_table['group']).strip()) + + resource['group'] = groups + + except (KeyError, AttributeError, IndexError, TypeError): + return resource + + return resource + + +def remove_snmp_user(user): + return ['no snmp-server user {0}'.format(user)] + + +def config_snmp_user(proposed, user, reset, new): + if reset and not new: + commands = remove_snmp_user(user) + else: + commands = [] + + group = proposed.get('group', None) + + cmd = '' + + if group: + cmd = 'snmp-server user {0} {group}'.format(user, **proposed) + + auth = proposed.get('authentication', None) + pwd = proposed.get('pwd', None) + + if auth and pwd: + cmd += ' auth {authentication} {pwd}'.format(**proposed) + + encrypt = proposed.get('encrypt', None) + privacy = proposed.get('privacy', None) + + if encrypt and privacy: + cmd += ' priv {encrypt} {privacy}'.format(**proposed) + elif privacy: + cmd += ' priv {privacy}'.format(**proposed) + + if cmd: + commands.append(cmd) + + return commands + + +def main(): + argument_spec = dict( + user=dict(required=True, type='str'), + group=dict(type='str', required=True), + pwd=dict(type='str'), + privacy=dict(type='str'), + authentication=dict(choices=['md5', 'sha']), + encrypt=dict(type='bool'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + required_together=[['authentication', 'pwd'], + ['encrypt', 'privacy']], + supports_check_mode=True) + + user = module.params['user'] + group = module.params['group'] + pwd = module.params['pwd'] + privacy = module.params['privacy'] + encrypt = module.params['encrypt'] + authentication = module.params['authentication'] + state = module.params['state'] + + if privacy and encrypt: + if not pwd and authentication: + module.fail_json(msg='pwd and authentication must be proviced ' + 'when using privacy and encrypt') + + if group and group not in get_snmp_groups(module): + module.fail_json(msg='group not configured yet on switch.') + + existing = get_snmp_user(user, module) + end_state = existing + + store = existing.get('group', None) + if existing: + if group not in existing['group']: + existing['group'] = None + else: + existing['group'] = group + + changed = False + commands = [] + proposed = {} + + if state == 'absent' and existing: + commands.append(remove_snmp_user(user)) + + elif state == 'present': + new = False + reset = False + + args = dict(user=user, pwd=pwd, group=group, privacy=privacy, + encrypt=encrypt, authentication=authentication) + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + + if not existing: + if encrypt: + proposed['encrypt'] = 'aes-128' + commands.append(config_snmp_user(proposed, user, reset, new)) + + elif existing: + if encrypt and not existing['encrypt'].startswith('aes'): + reset = True + proposed['encrypt'] = 'aes-128' + + elif encrypt: + proposed['encrypt'] = 'aes-128' + + delta = dict( + set(proposed.iteritems()).difference(existing.iteritems())) + + if delta.get('pwd'): + delta['authentication'] = authentication + + if delta: + delta['group'] = group + + command = config_snmp_user(delta, user, reset, new) + commands.append(command) + + 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) + end_state = get_snmp_user(user, module) + + if store: + existing['group'] = store + + 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() \ No newline at end of file From 48d932643b5ad2c66261e8a11cbf402d53f8d83b Mon Sep 17 00:00:00 2001 From: mzizzi Date: Thu, 15 Sep 2016 12:55:28 -0400 Subject: [PATCH 323/770] cloudformation stack events itertools.imap bugfix (#4868) --- cloud/amazon/cloudformation.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 331bd394286..d3da86413ac 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -193,28 +193,28 @@ def stack_operation(cfn, stack_name, operation): if 'yes' in existed: result = dict(changed=True, output='Stack Deleted', - events=map(str, list(stack.describe_events()))) + events=list(map(str, list(stack.describe_events())))) else: result = dict(changed= True, output='Stack Not Found') break if '%s_COMPLETE' % operation == stack.stack_status: result = dict(changed=True, - events = map(str, list(stack.describe_events())), + events = list(map(str, list(stack.describe_events()))), output = 'Stack %s complete' % operation) break if 'ROLLBACK_COMPLETE' == stack.stack_status or '%s_ROLLBACK_COMPLETE' % operation == stack.stack_status: result = dict(changed=True, failed=True, - events = map(str, list(stack.describe_events())), + events = list(map(str, list(stack.describe_events()))), output = 'Problem with %s. Rollback complete' % operation) break elif '%s_FAILED' % operation == stack.stack_status: result = dict(changed=True, failed=True, - events = map(str, list(stack.describe_events())), + events = list(map(str, list(stack.describe_events()))), output = 'Stack %s failed' % operation) break elif '%s_ROLLBACK_FAILED' % operation == stack.stack_status: result = dict(changed=True, failed=True, - events = map(str, list(stack.describe_events())), + events = list(map(str, list(stack.describe_events()))), output = 'Stack %s rollback failed' % operation) break else: From ddd224fd0b85121437de365b0ff14a901aa983fe Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 19:00:16 +0200 Subject: [PATCH 324/770] Adding 2.1 and 2.2 support to nxos_switchport --- network/nxos/nxos_switchport.py | 251 +++++++++++++++++++++++++------- 1 file changed, 202 insertions(+), 49 deletions(-) diff --git a/network/nxos/nxos_switchport.py b/network/nxos/nxos_switchport.py index 1f7f730a0d7..49fce57aee8 100644 --- a/network/nxos/nxos_switchport.py +++ b/network/nxos/nxos_switchport.py @@ -20,7 +20,7 @@ --- module: nxos_switchport version_added: "2.1" -short_description: Manages Layer 2 switchport interfaces +short_description: Manages Layer 2 switchport interfaces. extends_documentation_fragment: nxos description: - Manages Layer 2 interfaces @@ -28,12 +28,12 @@ notes: - When C(state=absent), VLANs can be added/removed from trunk links and the existing access VLAN can be 'unconfigured' to just having VLAN 1 - on that interface + on that interface. - When working with trunks VLANs the keywords add/remove are always sent in the `switchport trunk allowed vlan` command. Use verbose mode to see commands sent. - When C(state=unconfigured), the interface will result with having a default - Layer 2 interface, i.e. vlan 1 in access mode + Layer 2 interface, i.e. vlan 1 in access mode. options: interface: description: @@ -77,28 +77,21 @@ required: false version_added: 2.2 default: null - ''' EXAMPLES = ''' # ENSURE Eth1/5 is in its default switchport state - nxos_switchport: interface=eth1/5 state=unconfigured host={{ inventory_hostname }} - # ENSURE Eth1/5 is configured for access vlan 20 - nxos_switchport: interface=eth1/5 mode=access access_vlan=20 host={{ inventory_hostname }} - # ENSURE Eth1/5 only has vlans 5-10 as trunk vlans - nxos_switchport: interface=eth1/5 mode=trunk native_vlan=10 trunk_vlans=5-10 host={{ inventory_hostname }} - # Ensure eth1/5 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) - nxos_switchport: interface=eth1/5 mode=trunk native_vlan=10 trunk_vlans=2-50 host={{ inventory_hostname }} - # Ensure these VLANs are not being tagged on the trunk - nxos_switchport: interface=eth1/5 mode=trunk trunk_vlans=51-4094 host={{ inventory_hostname }} state=absent - ''' RETURN = ''' - proposed: description: k/v pairs of parameters passed into module returned: always @@ -119,11 +112,6 @@ "interface": "Ethernet1/5", "mode": "access", "native_vlan": "1", "native_vlan_name": "default", "switchport": "Enabled", "trunk_vlans": "1-4094"} -state: - description: state as sent in from the playbook - returned: always - type: string - sample: "present" updates: description: command string sent to the device returned: always @@ -134,21 +122,172 @@ returned: always type: boolean sample: true - ''' +import json + +# 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 get_interface_type(interface): """Gets the type of interface - Args: interface (str): full name of interface, i.e. Ethernet1/1, loopback10, port-channel20, vlan20 - Returns: type of interface: ethernet, svi, loopback, management, portchannel, or unknown - """ if interface.upper().startswith('ET'): return 'ethernet' @@ -168,16 +307,13 @@ def get_interface_type(interface): def get_interface_mode(interface, module): """Gets current mode of interface: layer2 or layer3 - Args: device (Device): This is the device object of an NX-API enabled device using the Device class within device.py interface (string): full name of interface, i.e. Ethernet1/1, loopback10, port-channel20, vlan20 - Returns: str: 'layer2' or 'layer3' - """ command = 'show interface ' + interface intf_type = get_interface_type(interface) @@ -205,13 +341,10 @@ def get_interface_mode(interface, module): def interface_is_portchannel(interface, module): """Checks to see if an interface is part of portchannel bundle - Args: interface (str): full name of interface, i.e. Ethernet1/1 - Returns: True/False based on if interface is a member of a portchannel bundle - """ intf_type = get_interface_type(interface) if intf_type == 'ethernet': @@ -234,15 +367,12 @@ def interface_is_portchannel(interface, module): def get_switchport(port, module): """Gets current config of L2 switchport - Args: device (Device): This is the device object of an NX-API enabled device using the Device class within device.py port (str): full name of interface, i.e. Ethernet1/1 - Returns: dictionary with k/v pairs for L2 vlan config - """ command = 'show interface {0} switchport'.format(port) @@ -356,14 +486,11 @@ def get_switchport_config_commands(interface, existing, proposed, module): def is_switchport_default(existing): """Determines if switchport has a default config based on mode - Args: existing (dict): existing switcport configuration from Ansible mod - Returns: boolean: True if switchport has OOB Layer 2 config, i.e. vlan 1 and trunk all and mode is access - """ c1 = existing['access_vlan'] == '1' @@ -455,6 +582,15 @@ def execute_config_command(commands, module): 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): @@ -462,13 +598,19 @@ def get_cli_body_ssh(command, response, module): 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. + resource doesn't exist yet. Instead, the output will be a raw string + when issuing commands containing 'show run'. """ - if 'xml' in response[0]: + if 'xml' in response[0] or response[0] == '\n': body = [] + elif 'status' in command: + body = response else: try: - body = [json.loads(response[0])] + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -476,6 +618,11 @@ def get_cli_body_ssh(command, response, module): 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) @@ -483,15 +630,28 @@ def execute_show(cmds, module, command_type=None): response = module.execute(cmds) except ShellError: clie = get_exception() - module.fail_json(msg='Error sending {0}'.format(command), + 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) + 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' + if 'status' not in command: + command += ' | json' cmds = [command] response = execute_show(cmds, module) body = get_cli_body_ssh(command, response, module) @@ -524,11 +684,11 @@ def main(): state=dict(choices=['absent', 'present', 'unconfigured'], default='present') ) - module = get_module(argument_spec=argument_spec, - mutually_exclusive=[['access_vlan', 'trunk_vlans'], - ['access_vlan', 'native_vlan'], - ['access_vlan', 'trunk_allowed_vlans']], - supports_check_mode=True) + module = get_network_module(argument_spec=argument_spec, + mutually_exclusive=[['access_vlan', 'trunk_vlans'], + ['access_vlan', 'native_vlan'], + ['access_vlan', 'trunk_allowed_vlans']], + supports_check_mode=True) interface = module.params['interface'] mode = module.params['mode'] @@ -637,17 +797,10 @@ def main(): results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['state'] = state results['updates'] = cmds results['changed'] = changed module.exit_json(**results) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -from ansible.module_utils.shell import * -from ansible.module_utils.netcfg import * -from ansible.module_utils.nxos import * - if __name__ == '__main__': main() \ No newline at end of file From d21ad8f39cd999851beeb327532fbffe3a7a876c Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 19:51:20 +0200 Subject: [PATCH 325/770] Adding nxos_udld_interface --- network/nxos/nxos_udld_interface.py | 514 ++++++++++++++++++++++++++++ 1 file changed, 514 insertions(+) create mode 100644 network/nxos/nxos_udld_interface.py diff --git a/network/nxos/nxos_udld_interface.py b/network/nxos/nxos_udld_interface.py new file mode 100644 index 00000000000..28c3a3ece7a --- /dev/null +++ b/network/nxos/nxos_udld_interface.py @@ -0,0 +1,514 @@ +#!/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_udld_interface +version_added: "2.2" +short_description: Manages UDLD interface configuration params. +description: + - Manages UDLD interface configuration params. +extends_documentation_fragment: nxos +author: + - Jason Edelman (@jedelman8) +notes: + - Feature UDLD must be enabled on the device to use this module. +options: + mode: + description: + - Manages UDLD mode for an interface. + required: true + choices: ['enabled','disabled','aggressive'] + interface: + description: + - FULL name of the interface, i.e. Ethernet1/1- + required: true + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +# ensure Ethernet1/1 is configured to be in aggressive mode +- nxos_udld_interface: + interface=Ethernet1/1 + mode=aggressive + state=present + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Remove the aggressive config only if it's currently in aggressive mode and then disable udld (switch default) +- nxos_udld_interface: + interface=Ethernet1/1 + mode=aggressive + state=absent + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# ensure Ethernet1/1 has aggressive mode enabled +- nxos_udld_interface: + interface=Ethernet1/1 + mode=enabled + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"mode": "enabled"} +existing: + description: + - k/v pairs of existing configuration + type: dict + sample: {"mode": "aggressive"} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"mode": "enabled"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["interface ethernet1/33", + "no udld aggressive ; no udld disable"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 get_udld_interface(module, interface): + command = 'show udld {0}'.format(interface) + interface_udld = {} + mode = None + try: + body = execute_show_command(command, module)[0] + table = body['TABLE_interface']['ROW_interface'] + + status = str(table.get('mib-port-status', None)) + agg = str(table.get('mib-aggresive-mode', 'disabled')) + + if agg == 'enabled': + mode = 'aggressive' + else: + mode = status + + interface_udld['mode'] = mode + + except (KeyError, AttributeError, IndexError): + interface_udld = {} + + return interface_udld + + +def is_interface_copper(module, interface): + command = 'show interface status' + copper = [] + try: + body = execute_show_command(command, module)[0] + table = body['TABLE_interface']['ROW_interface'] + for each in table: + itype = each.get('type', 'DNE') + if 'CU' in itype or '1000' in itype or '10GBaseT' in itype: + copper.append(str(each['interface'].lower())) + except (KeyError, AttributeError): + pass + + if interface in copper: + found = True + else: + found = False + + return found + + +def get_commands_config_udld_interface(delta, interface, module, existing): + commands = [] + copper = is_interface_copper(module, interface) + if delta: + mode = delta['mode'] + if mode == 'aggressive': + command = 'udld aggressive' + elif copper: + if mode == 'enabled': + if existing['mode'] == 'aggressive': + command = 'no udld aggressive ; udld enable' + else: + command = 'udld enable' + elif mode == 'disabled': + command = 'no udld enable' + elif not copper: + if mode == 'enabled': + if existing['mode'] == 'aggressive': + command = 'no udld aggressive ; no udld disable' + else: + command = 'no udld disable' + elif mode == 'disabled': + command = 'udld disable' + if command: + commands.append(command) + commands.insert(0, 'interface {0}'.format(interface)) + + return commands + + +def get_commands_remove_udld_interface(delta, interface, module, existing): + commands = [] + copper = is_interface_copper(module, interface) + + if delta: + mode = delta['mode'] + if mode == 'aggressive': + command = 'no udld aggressive' + elif copper: + if mode == 'enabled': + command = 'no udld enable' + elif mode == 'disabled': + command = 'udld enable' + elif not copper: + if mode == 'enabled': + command = 'udld disable' + elif mode == 'disabled': + command = 'no udld disable' + if command: + commands.append(command) + commands.insert(0, 'interface {0}'.format(interface)) + + return commands + + +def main(): + argument_spec = dict( + mode=dict(choices=['enabled', 'disabled', 'aggressive'], + required=True), + interface=dict(type='str', required=True), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + interface = module.params['interface'].lower() + mode = module.params['mode'] + state = module.params['state'] + + proposed = dict(mode=mode) + existing = get_udld_interface(module, interface) + end_state = existing + + delta = dict(set(proposed.iteritems()).difference(existing.iteritems())) + + changed = False + commands = [] + if state == 'present': + if delta: + command = get_commands_config_udld_interface(delta, interface, + module, existing) + commands.append(command) + elif state == 'absent': + common = set(proposed.iteritems()).intersection(existing.iteritems()) + if common: + command = get_commands_remove_udld_interface( + dict(common), interface, module, existing + ) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_udld_interface(module, interface) + + results = {} + results['proposed'] = proposed + results['existing'] = existing + results['end_state'] = end_state + results['updates'] = cmds + results['changed'] = changed + + module.exit_json(**results) + +if __name__ == '__main__': + main() \ No newline at end of file From 4d85a0eade5da16d43bf44aef3ca1130699d7b06 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 21:21:03 +0200 Subject: [PATCH 326/770] Adding nxos_aaa_server_host --- network/nxos/nxos_aaa_server_host.py | 578 +++++++++++++++++++++++++++ 1 file changed, 578 insertions(+) create mode 100644 network/nxos/nxos_aaa_server_host.py diff --git a/network/nxos/nxos_aaa_server_host.py b/network/nxos/nxos_aaa_server_host.py new file mode 100644 index 00000000000..ed11ddd1f5f --- /dev/null +++ b/network/nxos/nxos_aaa_server_host.py @@ -0,0 +1,578 @@ +#!/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_aaa_server_host +version_added: "2.2" +short_description: Manages AAA server host-specific configuration. +description: + - Manages AAA server host-specific configuration. +extends_documentation_fragment: nxos +author: Jason Edelman (@jedelman8) +notes: + - Changes to the AAA server host key (shared secret) are not idempotent. + - If C(state=absent) removes the whole host configuration. +options: + server_type: + description: + - The server type is either radius or tacacs. + required: true + choices: ['radius', 'tacacs'] + address: + description: + - Address or name of the radius or tacacs host. + required: true + key: + description: + - Shared secret for the specified host. + required: false + default: null + encrypt_type: + description: + - The state of encryption applied to the entered key. + O for clear text, 7 for encrypted. Type-6 encryption is + not supported. + required: false + default: null + choices: ['0', '7'] + host_timeout: + description: + - Timeout period for specified host, in seconds. Range is 1-60. + required: false + default: null + auth_port: + description: + - Alternate UDP port for RADIUS authentication. + required: false + default: null + acct_port: + description: + - Alternate UDP port for RADIUS accounting. + required: false + default: null + tacacs_port: + description: + - Alternate TCP port TACACS Server. + required: false + default: null + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','absent'] +''' +EXAMPLES = ''' +# Radius Server Host Basic settings + - name: "Radius Server Host Basic settings" + nxos_aaa_server_host: + state=present + server_type=radius + address=1.2.3.4 + acct_port=2084 + host_timeout=10 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# Radius Server Host Key Configuration + - name: "Radius Server Host Key Configuration" + nxos_aaa_server_host: + state=present + server_type=radius + address=1.2.3.4 + key=hello + encrypt_type=7 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# TACACS Server Host Configuration + - name: "Tacacs Server Host Configuration" + nxos_aaa_server_host: + state=present + server_type=tacacs + tacacs_port=89 + host_timeout=10 + address=5.6.7.8 + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"address": "1.2.3.4", "auth_port": "2084", + "host_timeout": "10", "server_type": "radius"} +existing: + description: + - k/v pairs of existing configuration + type: dict + sample: {} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"address": "1.2.3.4", "auth_port": "2084", + "host_timeout": "10", "server_type": "radius"} +updates: + description: command sent to the device + returned: always + type: list + sample: ["radius-server host 1.2.3.4 auth-port 2084 timeout 10"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + + +import json + +# 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0] or response[0] == '\n': + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 _match_dict(match_list, key_map): + no_blanks = [] + match_dict = {} + + for match_set in match_list: + match_set = tuple(v for v in match_set if v) + no_blanks.append(match_set) + + for info in no_blanks: + words = info[0].strip().split() + length = len(words) + alt_key = key_map.get(words[0]) + first = alt_key or words[0] + last = words[length - 1] + match_dict[first] = last.replace('\"', '') + + return match_dict + + +def get_aaa_host_info(module, server_type, address): + aaa_host_info = {} + command = 'show run | inc {0}-server.host.{1}'.format(server_type, address) + + body = execute_show_command(command, module, command_type='cli_show_ascii') + + if body: + try: + pattern = ('(acct-port \d+)|(timeout \d+)|(auth-port \d+)|' + '(key 7 "\w+")|( port \d+)') + raw_match = re.findall(pattern, body[0]) + aaa_host_info = _match_dict(raw_match, {'acct-port': 'acct_port', + 'auth-port': 'auth_port', + 'port': 'tacacs_port', + 'timeout': 'host_timeout'}) + if aaa_host_info: + aaa_host_info['server_type'] = server_type + aaa_host_info['address'] = address + except TypeError: + return {} + else: + return {} + + return aaa_host_info + + +def config_aaa_host(server_type, address, params, clear=False): + cmds = [] + + if clear: + cmds.append('no {0}-server host {1}'.format(server_type, address)) + + cmd_str = '{0}-server host {1}'.format(server_type, address) + + key = params.get('key') + enc_type = params.get('encrypt_type', '') + host_timeout = params.get('host_timeout') + auth_port = params.get('auth_port') + acct_port = params.get('acct_port') + port = params.get('tacacs_port') + + if auth_port: + cmd_str += ' auth-port {0}'.format(auth_port) + if acct_port: + cmd_str += ' acct-port {0}'.format(acct_port) + if port: + cmd_str += ' port {0}'.format(port) + if host_timeout: + cmd_str += ' timeout {0}'.format(host_timeout) + if key: + cmds.append('{0}-server host {1} key {2} {3}'.format(server_type, + address, + enc_type, key)) + + cmds.append(cmd_str) + return cmds + + +def main(): + argument_spec = dict( + server_type=dict(choices=['radius', 'tacacs'], required=True), + address=dict(type='str', required=True), + key=dict(type='str'), + encrypt_type=dict(type='str', choices=['0', '7']), + host_timeout=dict(type='str'), + auth_port=dict(type='str'), + acct_port=dict(type='str'), + tacacs_port=dict(type='str'), + state=dict(choices=['absent', 'present'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + server_type = module.params['server_type'] + address = module.params['address'] + key = module.params['key'] + encrypt_type = module.params['encrypt_type'] + host_timeout = module.params['host_timeout'] + auth_port = module.params['auth_port'] + acct_port = module.params['acct_port'] + tacacs_port = module.params['tacacs_port'] + state = module.params['state'] + + args = dict(server_type=server_type, address=address, key=key, + encrypt_type=encrypt_type, host_timeout=host_timeout, + auth_port=auth_port, acct_port=acct_port, + tacacs_port=tacacs_port) + + proposed = dict((k, v) for k, v in args.iteritems() if v is not None) + changed = False + + if encrypt_type and not key: + module.fail_json(msg='encrypt_type must be used with key') + + if tacacs_port and server_type != 'tacacs': + module.fail_json( + msg='tacacs_port can only be used with server_type=tacacs') + + if (auth_port or acct_port) and server_type != 'radius': + module.fail_json(msg='auth_port and acct_port can only be used' + 'when server_type=radius') + + + existing = get_aaa_host_info(module, server_type, address) + end_state = existing + + commands = [] + if state == 'present': + host_timeout = proposed.get('host_timeout') + if host_timeout: + try: + if int(host_timeout) < 1 or int(host_timeout) > 60: + raise ValueError + except ValueError: + module.fail_json( + msg='host_timeout must be an integer between 1 and 60') + + delta = dict( + set(proposed.iteritems()).difference(existing.iteritems())) + if delta: + union = existing.copy() + union.update(delta) + command = config_aaa_host(server_type, address, union) + if command: + commands.append(command) + + elif state == 'absent': + intersect = dict( + set(proposed.iteritems()).intersection(existing.iteritems())) + if intersect.get('address') and intersect.get('server_type'): + command = 'no {0}-server host {1}'.format( + intersect.get('server_type'), intersect.get('address')) + commands.append(command) + + cmds = flatten_list(commands) + if cmds: + if module.check_mode: + module.exit_json(changed=True, commands=cmds) + else: + changed = True + execute_config_command(cmds, module) + end_state = get_aaa_host_info(module, server_type, address) + + results = {} + 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() \ No newline at end of file From 4840d305892e3f466fc12370bbfc22237a8674d6 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Thu, 15 Sep 2016 15:07:34 -0500 Subject: [PATCH 327/770] Expose the reuse_fips flag on os_server (#4849) * Expose the reuse_fips flag on os_server * Remove useless line --- cloud/openstack/os_server.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index dd2155c4e1a..e6c986486e7 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -185,6 +185,18 @@ required: false default: false version_added: "2.2" + reuse_fips: + description: + - When I(auto_ip) is true and this option is true, the I(auto_ip) code + will attempt to re-use unassigned floating ips in the project before + creating a new one. It is important to note that it is impossible + to safely do this concurrently, so if your use case involves + concurrent server creation, it is highly recommended to set this to + false and to delete the floating ip associated with a server when + the server is deleted using I(delete_fip). + required: false + default: true + version_added: "2.2" requirements: - "python >= 2.6" - "shade" @@ -475,6 +487,7 @@ def _create_server(module, cloud): boot_volume=module.params['boot_volume'], boot_from_volume=module.params['boot_from_volume'], terminate_volume=module.params['terminate_volume'], + reuse_fips=module.params['reuse_fips'], wait=module.params['wait'], timeout=module.params['timeout'], **bootkwargs ) @@ -575,6 +588,7 @@ def main(): scheduler_hints = dict(default=None, type='dict'), state = dict(default='present', choices=['absent', 'present']), delete_fip = dict(default=False, type='bool'), + reuse_fips = dict(default=True, type='bool'), ) module_kwargs = openstack_module_kwargs( mutually_exclusive=[ From 068ccb4c7a18dc95e99c6776c5cb26d6a82dfd60 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Thu, 15 Sep 2016 22:57:12 +0200 Subject: [PATCH 328/770] Adding nxos_igmp_snooping --- network/nxos/nxos_igmp_snooping.py | 550 +++++++++++++++++++++++++++++ 1 file changed, 550 insertions(+) create mode 100644 network/nxos/nxos_igmp_snooping.py diff --git a/network/nxos/nxos_igmp_snooping.py b/network/nxos/nxos_igmp_snooping.py new file mode 100644 index 00000000000..f8c6c4af7d8 --- /dev/null +++ b/network/nxos/nxos_igmp_snooping.py @@ -0,0 +1,550 @@ +#!/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_igmp_snooping +version_added: "2.2" +short_description: Manages IGMP snooping global configuration. +description: + - Manages IGMP snooping global configuration. +author: + - Jason Edelman (@jedelman8) + - Gabriele Gerbino (@GGabriele) +extends_documentation_fragment: nxos +notes: + - When C(state=default), params will be reset to a default state. + - C(group_timeout) also accepts I(never) as an input. +options: + snooping: + description: + - Enables/disables IGMP snooping on the switch. + required: false + default: null + choices: ['true', 'false'] + group_timeout: + description: + - Group membership timeout value for all VLANs on the device. + Accepted values are integer in range 1-10080, I(never) and + I(default). + required: false + default: null + link_local_grp_supp: + description: + - Global link-local groups suppression. + required: false + default: null + choices: ['true', 'false'] + report_supp: + description: + - Global IGMPv1/IGMPv2 Report Suppression. + required: false + default: null + v3_report_supp: + description: + - Global IGMPv3 Report Suppression and Proxy Reporting. + required: false + default: null + choices: ['true', 'false'] + state: + description: + - Manage the state of the resource. + required: false + default: present + choices: ['present','default'] +''' + +EXAMPLES = ''' +# ensure igmp snooping params supported in this module are in there default state +- nxos_igmp_snooping: + state=default + host={{ inventory_hostname }} + username={{ un }} + password={{ pwd }} + +# ensure following igmp snooping params are in the desired state +- nxos_igmp_snooping: + group_timeout: never + snooping: true + link_local_grp_supp: false + optimize_mcast_flood: false + report_supp: true + v3_report_supp: true + host: "{{ inventory_hostname }}" + username={{ un }} + password={{ pwd }} +''' + +RETURN = ''' +proposed: + description: k/v pairs of parameters passed into module + returned: always + type: dict + sample: {"group_timeout": "50", "link_local_grp_supp": true, + "report_supp": false, "snooping": false, "v3_report_supp": false} +existing: + description: + - k/v pairs of existing configuration + type: dict + sample: {"group_timeout": "never", "link_local_grp_supp": false, + "report_supp": true, "snooping": true, "v3_report_supp": true} +end_state: + description: k/v pairs of configuration after module execution + returned: always + type: dict + sample: {"group_timeout": "50", "link_local_grp_supp": true, + "report_supp": false, "snooping": false, "v3_report_supp": false} +updates: + description: command sent to the device + returned: always + type: list + sample: ["ip igmp snooping link-local-groups-suppression", + "ip igmp snooping group-timeout 50", + "no ip igmp snooping report-suppression", + "no ip igmp snooping v3-report-suppression", + "no ip igmp snooping"] +changed: + description: check to see if a change was made on the device + returned: always + type: boolean + sample: true +''' + +import json + +# 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 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. Instead, the output will be a raw string + when issuing commands containing 'show run'. + """ + if 'xml' in response[0]: + body = [] + elif 'show run' in command: + body = response + else: + try: + if isinstance(response[0], str): + response = response[0].replace(command + '\n\n', '').strip() + body = [json.loads(response[0])] + else: + body = response + 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) + 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': + if 'show run' not in command: + 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 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 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_group_timeout(config): + command = 'ip igmp snooping group-timeout' + REGEX = re.compile(r'(?:{0}\s)(?P.*)$'.format(command), re.M) + value = '' + if command in config: + value = REGEX.search(config).group('value') + return value + + +def get_snooping(config): + REGEX = re.compile(r'{0}$'.format('no ip igmp snooping'), re.M) + value = False + try: + if REGEX.search(config): + value = False + except TypeError: + value = True + return value + + +def get_igmp_snooping(module): + command = 'show run all | include igmp.snooping' + existing = {} + body = execute_show_command( + command, module, command_type='cli_show_ascii')[0] + + if body: + split_body = body.splitlines() + + if 'no ip igmp snooping' in split_body: + existing['snooping'] = False + else: + existing['snooping'] = True + + if 'no ip igmp snooping report-suppression' in split_body: + existing['report_supp'] = False + elif 'ip igmp snooping report-suppression' in split_body: + existing['report_supp'] = True + + if 'no ip igmp snooping link-local-groups-suppression' in split_body: + existing['link_local_grp_supp'] = False + elif 'ip igmp snooping link-local-groups-suppression' in split_body: + existing['link_local_grp_supp'] = True + + if 'ip igmp snooping v3-report-suppression' in split_body: + existing['v3_report_supp'] = True + else: + existing['v3_report_supp'] = False + + existing['group_timeout'] = get_group_timeout(body) + + return existing + + +def config_igmp_snooping(delta, existing, default=False): + CMDS = { + 'snooping': 'ip igmp snooping', + 'group_timeout': 'ip igmp snooping group-timeout {}', + 'link_local_grp_supp': 'ip igmp snooping link-local-groups-suppression', + 'v3_report_supp': 'ip igmp snooping v3-report-suppression', + 'report_supp': 'ip igmp snooping report-suppression' + } + + commands = [] + command = None + for key, value in delta.iteritems(): + if value: + if default and key == 'group_timeout': + if existing.get(key): + command = 'no ' + CMDS.get(key).format(existing.get(key)) + else: + command = CMDS.get(key).format(value) + else: + command = 'no ' + CMDS.get(key).format(value) + + if command: + commands.append(command) + command = None + + return commands + + +def get_igmp_snooping_defaults(): + group_timeout = 'dummy' + report_supp = True + link_local_grp_supp = True + v3_report_supp = False + snooping = True + + args = dict(snooping=snooping, link_local_grp_supp=link_local_grp_supp, + report_supp=report_supp, v3_report_supp=v3_report_supp, + group_timeout=group_timeout) + + default = dict((param, value) for (param, value) in args.iteritems() + if value is not None) + + return default + + +def main(): + argument_spec = dict( + snooping=dict(required=False, type='bool'), + group_timeout=dict(required=False, type='str'), + link_local_grp_supp=dict(required=False, type='bool'), + report_supp=dict(required=False, type='bool'), + v3_report_supp=dict(required=False, type='bool'), + state=dict(choices=['present', 'default'], default='present'), + ) + module = get_network_module(argument_spec=argument_spec, + supports_check_mode=True) + + snooping = module.params['snooping'] + link_local_grp_supp = module.params['link_local_grp_supp'] + report_supp = module.params['report_supp'] + v3_report_supp = module.params['v3_report_supp'] + group_timeout = module.params['group_timeout'] + state = module.params['state'] + + args = dict(snooping=snooping, link_local_grp_supp=link_local_grp_supp, + report_supp=report_supp, v3_report_supp=v3_report_supp, + group_timeout=group_timeout) + + proposed = dict((param, value) for (param, value) in args.iteritems() + if value is not None) + + existing = get_igmp_snooping(module) + end_state = existing + changed = False + + commands = [] + if state == 'present': + delta = dict( + set(proposed.iteritems()).difference(existing.iteritems()) + ) + if delta: + command = config_igmp_snooping(delta, existing) + if command: + commands.append(command) + elif state == 'default': + proposed = get_igmp_snooping_defaults() + delta = dict( + set(proposed.iteritems()).difference(existing.iteritems()) + ) + if delta: + command = config_igmp_snooping(delta, existing, default=True) + if command: + commands.append(command) + + 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) + end_state = get_igmp_snooping(module) + + 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() From e92db1dc12b7afafaaea81ffb111633db65f108f Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Thu, 15 Sep 2016 14:29:25 -0700 Subject: [PATCH 329/770] Document ability to disable external IP with 'none' setting. Closes #2562. (#4878) --- cloud/google/gce.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index 064a79e2f84..96f058df6d1 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -142,7 +142,7 @@ external_ip: version_added: "1.9" description: - - type of external ip, ephemeral by default; alternatively, a list of fixed gce ips or ip names can be given (if there is not enough specified ip, 'ephemeral' will be used) + - type of external ip, ephemeral by default; alternatively, a list of fixed gce ips or ip names can be given (if there is not enough specified ip, 'ephemeral' will be used). Specify 'none' if no external ip is desired. required: false default: "ephemeral" disk_auto_delete: From 81ad5c2c97b403324cb26532741e9fd9741f082c Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 03:26:49 -0700 Subject: [PATCH 330/770] Empty Init Module as required (#4740) Add network/netvisor/__init__.py --- network/netvisor/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 network/netvisor/__init__.py diff --git a/network/netvisor/__init__.py b/network/netvisor/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 3dcc06c663ce41bbd48b751b18c3075f371b3043 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Fri, 16 Sep 2016 04:54:12 -0700 Subject: [PATCH 331/770] Added support dnos9_facts module (#4873) * Added support dnos9_facts module * Addressed @gundalow review comments * Removing the addtional command, which is already in the default --- network/dnos9/dnos9_facts.py | 552 +++++++++++++++++++++++++++++++++++ 1 file changed, 552 insertions(+) create mode 100644 network/dnos9/dnos9_facts.py diff --git a/network/dnos9/dnos9_facts.py b/network/dnos9/dnos9_facts.py new file mode 100644 index 00000000000..be64159ebbd --- /dev/null +++ b/network/dnos9/dnos9_facts.py @@ -0,0 +1,552 @@ +#!/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: dnos9_facts +version_added: "2.2" +author: "Dhivya P (@dhivyap)" +short_description: Collect facts from remote devices running Dell OS9 +description: + - Collects a base set of device facts from a remote device that + is running Dell OS9. This module prepends all of the + base network fact keys with C(ansible_net_). The facts + module will always collect a base set of facts from the device + and can enable or disable collection of additional facts. +extends_documentation_fragment: dnos9 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument include + all, hardware, config, and interfaces. Can specify a list of + values to include a larger subset. Values can also be used + with an initial M(!) to specify that a specific subset should + not be collected. + required: false + default: '!config' +""" + +EXAMPLES = """ +# Collect all facts from the device +- dnos9_facts: + gather_subset: all + +# Collect only the config and default facts +- dnos9_facts: + gather_subset: + - config + +# Do not collect hardware facts +- dnos9_facts: + gather_subset: + - "!hardware" +""" + +RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +# default +ansible_net_model: + description: The model name returned from the device + returned: always + type: str +ansible_net_serialnum: + description: The serial number of the remote device + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: string +ansible_net_image: + description: The image file the device is running + returned: always + type: string + +# hardware +ansible_net_filesystems: + description: All file system names available on the device + returned: when hardware is configured + type: list +ansible_net_memfree_mb: + description: The available free memory on the remote device in Mb + returned: when hardware is configured + type: int +ansible_net_memtotal_mb: + description: The total memory on the remote device in Mb + returned: when hardware is configured + type: int + +# config +ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str + +# interfaces +ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +ansible_net_neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict +""" +import re +import itertools + +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.dnos9 + + +class FactsBase(object): + + def __init__(self, runner): + self.runner = runner + self.facts = dict() + + self.commands() + + +class Default(FactsBase): + + def commands(self): + self.runner.add_command('show version') + self.runner.add_command('show inventory') + self.runner.add_command('show running-config | grep hostname') + + def populate(self): + data = self.runner.get_command('show version') + self.facts['version'] = self.parse_version(data) + self.facts['model'] = self.parse_model(data) + self.facts['image'] = self.parse_image(data) + + data = self.runner.get_command('show inventory') + self.facts['serialnum'] = self.parse_serialnum(data) + + data = self.runner.get_command('show running-config | grep hostname') + self.facts['hostname'] = self.parse_hostname(data) + + def parse_version(self, data): + match = re.search(r'Software Version:\s*(.+)', data) + if match: + return match.group(1) + + def parse_hostname(self, data): + match = re.search(r'^hostname (.+)', data, re.M) + if match: + return match.group(1) + + def parse_model(self, data): + match = re.search(r'^System Type:\s*(.+)', data, re.M) + if match: + return match.group(1) + + def parse_image(self, data): + match = re.search(r'image file is "(.+)"', data) + if match: + return match.group(1) + + def parse_serialnum(self, data): + for line in data.split('\n'): + if line.startswith('*'): + match = re.search( + r'\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)', line, re.M) + if match: + return match.group(3) + + +class Hardware(FactsBase): + + def commands(self): + self.runner.add_command('show file-systems') + self.runner.add_command('show memory | except Processor') + + def populate(self): + data = self.runner.get_command('show file-systems') + self.facts['filesystems'] = self.parse_filesystems(data) + + data = self.runner.get_command('show memory | except Processor') + match = re.findall('\s(\d+)\s', data) + if match: + self.facts['memtotal_mb'] = int(match[0]) / 1024 + self.facts['memfree_mb'] = int(match[2]) / 1024 + + def parse_filesystems(self, data): + return re.findall(r'\s(\S+):$', data, re.M) + + +class Config(FactsBase): + + def commands(self): + self.runner.add_command('show running-config') + + def populate(self): + self.facts['config'] = self.runner.get_command('show running-config') + + +class Interfaces(FactsBase): + + def commands(self): + self.runner.add_command('show interfaces') + self.runner.add_command('show ipv6 interface') + self.runner.add_command('show lldp neighbors detail') + + def populate(self): + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + data = self.runner.get_command('show interfaces') + interfaces = self.parse_interfaces(data) + + for key in interfaces.keys(): + if "ManagementEthernet" in key: + temp_parsed = interfaces[key] + del interfaces[key] + interfaces.update(self.parse_mgmt_interfaces(temp_parsed)) + + for key in interfaces.keys(): + if "Vlan" in key: + temp_parsed = interfaces[key] + del interfaces[key] + interfaces.update(self.parse_vlan_interfaces(temp_parsed)) + + self.facts['interfaces'] = self.populate_interfaces(interfaces) + + data = self.runner.get_command('show ipv6 interface') + if len(data) > 0: + data = self.parse_ipv6_interfaces(data) + self.populate_ipv6_interfaces(data) + + data = self.runner.get_command('show inventory') + if 'LLDP' in self.get_protocol_list(data): + neighbors = self.runner.get_command('show lldp neighbors detail') + self.facts['neighbors'] = self.parse_neighbors(neighbors) + + def get_protocol_list(self, data): + start = False + protocol_list = list() + for line in data.split('\n'): + match = re.search(r'Software Protocol Configured\s*', line) + if match: + start = True + continue + if start: + line = line.strip() + if line.isalnum(): + protocol_list.append(line) + return protocol_list + + def populate_interfaces(self, interfaces): + facts = dict() + for key, value in interfaces.iteritems(): + intf = dict() + intf['description'] = self.parse_description(value) + intf['macaddress'] = self.parse_macaddress(value) + ipv4 = self.parse_ipv4(value) + intf['ipv4'] = self.parse_ipv4(value) + if ipv4: + self.add_ip_address(ipv4['address'], 'ipv4') + + intf['mtu'] = self.parse_mtu(value) + intf['bandwidth'] = self.parse_bandwidth(value) + intf['mediatype'] = self.parse_mediatype(value) + intf['duplex'] = self.parse_duplex(value) + intf['lineprotocol'] = self.parse_lineprotocol(value) + intf['operstatus'] = self.parse_operstatus(value) + intf['type'] = self.parse_type(value) + + facts[key] = intf + return facts + + def populate_ipv6_interfaces(self, data): + for key, value in data.iteritems(): + self.facts['interfaces'][key]['ipv6'] = list() + addresses = re.findall(r'\s+(.+), subnet', value, re.M) + subnets = re.findall(r', subnet is (\S+)', value, re.M) + for addr, subnet in itertools.izip(addresses, subnets): + ipv6 = dict(address=addr.strip(), subnet=subnet.strip()) + self.add_ip_address(addr.strip(), 'ipv6') + self.facts['interfaces'][key]['ipv6'].append(ipv6) + + def add_ip_address(self, address, family): + if family == 'ipv4': + self.facts['all_ipv4_addresses'].append(address) + else: + self.facts['all_ipv6_addresses'].append(address) + + def parse_neighbors(self, neighbors): + facts = dict() + + for entry in neighbors.split( + '========================================================================'): + if entry == '': + continue + + intf = self.parse_lldp_intf(entry) + if intf not in facts: + facts[intf] = list() + fact = dict() + fact['host'] = self.parse_lldp_host(entry) + fact['port'] = self.parse_lldp_port(entry) + facts[intf].append(fact) + return facts + + def parse_interfaces(self, data): + parsed = dict() + newline_count = 0 + interface_start = True + + for line in data.split('\n'): + if interface_start: + newline_count = 0 + if len(line) == 0: + newline_count += 1 + if newline_count == 2: + interface_start = True + continue + else: + match = re.match(r'^(\S+) (\S+)', line) + if match and interface_start: + interface_start = False + key = match.group(0) + parsed[key] = line + else: + parsed[key] += '\n%s' % line + return parsed + + def parse_mgmt_interfaces(self, data): + parsed = dict() + interface_start = True + for line in data.split('\n'): + match = re.match(r'^(\S+) (\S+)', line) + if "Time since" in line: + interface_start = True + parsed[key] += '\n%s' % line + continue + elif match and interface_start: + interface_start = False + key = match.group(0) + parsed[key] = line + else: + parsed[key] += '\n%s' % line + return parsed + + def parse_vlan_interfaces(self, data): + parsed = dict() + interface_start = True + line_before_end = False + for line in data.split('\n'): + match = re.match(r'^(\S+) (\S+)', line) + match_endline = re.match(r'^\s*\d+ packets, \d+ bytes$', line) + + if "Output Statistics" in line: + line_before_end = True + parsed[key] += '\n%s' % line + elif match_endline and line_before_end: + line_before_end = False + interface_start = True + parsed[key] += '\n%s' % line + elif match and interface_start: + interface_start = False + key = match.group(0) + parsed[key] = line + else: + parsed[key] += '\n%s' % line + return parsed + + def parse_ipv6_interfaces(self, data): + parsed = dict() + for line in data.split('\n'): + if len(line) == 0: + continue + elif line[0] == ' ': + parsed[key] += '\n%s' % line + else: + match = re.match(r'^(\S+) (\S+)', line) + if match: + key = match.group(0) + parsed[key] = line + return parsed + + def parse_description(self, data): + match = re.search(r'Description: (.+)$', data, re.M) + if match: + return match.group(1) + + def parse_macaddress(self, data): + match = re.search(r'address is (\S+)', data) + if match: + if match.group(1) != "not": + return match.group(1) + + def parse_ipv4(self, data): + match = re.search(r'Internet address is (\S+)', data) + if match: + if match.group(1) != "not": + addr, masklen = match.group(1).split('/') + return dict(address=addr, masklen=int(masklen)) + + def parse_mtu(self, data): + match = re.search(r'MTU (\d+)', data) + if match: + return int(match.group(1)) + + def parse_bandwidth(self, data): + match = re.search(r'LineSpeed (\d+)', data) + if match: + return int(match.group(1)) + + def parse_duplex(self, data): + match = re.search(r'(\w+) duplex', data, re.M) + if match: + return match.group(1) + + def parse_mediatype(self, data): + media = re.search(r'(.+) media present, (.+)', data, re.M) + if media: + match = re.search(r'type is (.+)$', media.group(0), re.M) + return match.group(1) + + def parse_type(self, data): + match = re.search(r'Hardware is (.+),', data, re.M) + if match: + return match.group(1) + + def parse_lineprotocol(self, data): + match = re.search(r'line protocol is (\w+[ ]?\w*)\(?.*\)?$', data, re.M) + if match: + return match.group(1) + + def parse_operstatus(self, data): + match = re.search(r'^(?:.+) is (.+),', data, re.M) + if match: + return match.group(1) + + def parse_lldp_intf(self, data): + match = re.search(r'^\sLocal Interface (\S+\s\S+)', data, re.M) + if match: + return match.group(1) + + def parse_lldp_host(self, data): + match = re.search(r'Remote System Name: (.+)$', data, re.M) + if match: + return match.group(1) + + def parse_lldp_port(self, data): + match = re.search(r'Remote Port ID: (.+)$', data, re.M) + if match: + return match.group(1) + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + module = NetworkModule(argument_spec=spec, supports_check_mode=True) + + gather_subset = module.params['gather_subset'] + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Bad subset') + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + + facts = dict() + facts['gather_subset'] = list(runable_subsets) + + runner = CommandRunner(module) + + instances = list() + for key in runable_subsets: + runs = FACT_SUBSETS[key](runner) + instances.append(runs) + + runner.run() + + try: + for inst in instances: + inst.populate() + facts.update(inst.facts) + except Exception: + module.exit_json(out=module.from_json(runner.items)) + + ansible_facts = dict() + for key, value in facts.iteritems(): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts) + + +if __name__ == '__main__': + main() From be87f4fc8abfd0cea29e45232b32b0a9871ebdee Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 04:58:51 -0700 Subject: [PATCH 332/770] pn_ospf (New Module) (#4736) * Add pn_ospf module * remove type from the options block --- network/netvisor/pn_ospf.py | 289 ++++++++++++++++++++++++++++++++++++ 1 file changed, 289 insertions(+) create mode 100644 network/netvisor/pn_ospf.py diff --git a/network/netvisor/pn_ospf.py b/network/netvisor/pn_ospf.py new file mode 100644 index 00000000000..16867aeaa1d --- /dev/null +++ b/network/netvisor/pn_ospf.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +""" PN-CLI vrouter-ospf-add/remove """ + +# +# 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 . +# + +import shlex + +DOCUMENTATION = """ +--- +module: pn_ospf +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to add/remove ospf protocol to a vRouter. +description: + - Execute vrouter-ospf-add, vrouter-ospf-remove command. + - This command adds/removes Open Shortest Path First(OSPF) routing + protocol to a virtual router(vRouter) service. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch to run the CLI on. + required: False + state: + description: + - Assert the state of the ospf. Use 'present' to add ospf + and 'absent' to remove ospf. + required: True + default: present + choices: ['present', 'absent'] + pn_vrouter_name: + description: + - Specify the name of the vRouter. + required: True + pn_network_ip: + description: + - Specify the network IP (IPv4 or IPv6) address. + required: True + pn_ospf_area: + description: + - Stub area number for the configuration. Required for vrouter-ospf-add. +""" + +EXAMPLES = """ +- name: "Add OSPF to vrouter" + pn_ospf: + state: present + pn_vrouter_name: name-string + pn_network_ip: 192.168.11.2/24 + pn_ospf_area: 1.0.0.0 + +- name: "Remove OSPF from vrouter" + pn_ospf: + state: absent + pn_vrouter_name: name-string +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the ospf command. + returned: always + type: list +stderr: + description: The set of error responses from the ospf command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + + +VROUTER_EXISTS = None +NETWORK_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks if vRouter exists on the target node. + This method also checks for idempotency using the vrouter-ospf-show command. + If the given vRouter exists, return VROUTER_EXISTS as True else False. + If an OSPF network with the given ip exists on the given vRouter, + return NETWORK_EXISTS as True else False. + + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VROUTER_EXISTS, NETWORK_EXISTS + """ + vrouter_name = module.params['pn_vrouter_name'] + network_ip = module.params['pn_network_ip'] + # Global flags + global VROUTER_EXISTS, NETWORK_EXISTS + + # Check for vRouter + check_vrouter = cli + ' vrouter-show format name no-show-headers ' + check_vrouter = shlex.split(check_vrouter) + out = module.run_command(check_vrouter)[1] + out = out.split() + + if vrouter_name in out: + VROUTER_EXISTS = True + else: + VROUTER_EXISTS = False + + # Check for OSPF networks + show = cli + ' vrouter-ospf-show vrouter-name %s ' % vrouter_name + show += 'format network no-show-headers' + show = shlex.split(show) + out = module.run_command(show)[1] + out = out.split() + + if network_ip in out: + NETWORK_EXISTS = True + else: + NETWORK_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + cmd = shlex.split(cli) + + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vrouter-ospf-add' + if state == 'absent': + command = 'vrouter-ospf-remove' + return command + + +def main(): + """ This section is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state=dict(type='str', default='present', choices=['present', + 'absent']), + pn_vrouter_name=dict(required=True, type='str'), + pn_network_ip=dict(required=True, type='str'), + pn_ospf_area=dict(type='str') + ), + required_if=( + ['state', 'present', + ['pn_network_ip', 'pn_ospf_area']], + ['state', 'absent', ['pn_network_ip']] + ) + ) + + # Accessing the arguments + state = module.params['state'] + vrouter_name = module.params['pn_vrouter_name'] + network_ip = module.params['pn_network_ip'] + ospf_area = module.params['pn_ospf_area'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + check_cli(module, cli) + + if state == 'present': + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + if NETWORK_EXISTS is True: + module.exit_json( + skipped=True, + msg=('OSPF with network ip %s already exists on %s' + % (network_ip, vrouter_name)) + ) + cli += (' %s vrouter-name %s network %s ospf-area %s' + % (command, vrouter_name, network_ip, ospf_area)) + + if state == 'absent': + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + if NETWORK_EXISTS is False: + module.exit_json( + skipped=True, + msg=('OSPF with network ip %s already exists on %s' + % (network_ip, vrouter_name)) + ) + cli += (' %s vrouter-name %s network %s' + % (command, vrouter_name, network_ip)) + + run_cli(module, cli) +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 7f4c6e547572dff13f11e60af5aa7ce317821830 Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:08:03 -0700 Subject: [PATCH 333/770] pn_show (New Module) (#4739) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block and fix typo --- network/netvisor/pn_show.py | 194 ++++++++++++++++++++++++++++++++++++ 1 file changed, 194 insertions(+) create mode 100644 network/netvisor/pn_show.py diff --git a/network/netvisor/pn_show.py b/network/netvisor/pn_show.py new file mode 100644 index 00000000000..fce0c967de7 --- /dev/null +++ b/network/netvisor/pn_show.py @@ -0,0 +1,194 @@ +#!/usr/bin/python +""" PN CLI show commands """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_show +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: Run show commands on nvOS device. +description: + - Execute show command in the nodes and returns the results + read from the device. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run the cli on. + required: False + pn_command: + description: + - The C(pn_command) takes a CLI show command as value. + required: true + pn_parameters: + description: + - Display output using a specific parameter. Use 'all' to display possible + output. List of comma separated parameters. + pn_options: + description: + - Specify formatting options. +""" + +EXAMPLES = """ +- name: run the vlan-show command + pn_show: + pn_command: 'vlan-show' + pn_parameters: id,scope,ports + pn_options: 'layout vertical' + +- name: run the vlag-show command + pn_show: + pn_command: 'vlag-show' + pn_parameters: 'id,name,cluster,mode' + pn_options: 'no-show-headers' + +- name: run the cluster-show command + pn_show: + pn_command: 'cluster-show' +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the show command. + returned: always + type: list +stderr: + description: The set of error responses from the show command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused any change on the target. + returned: always(False) + type: bool +""" + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch: + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + command = module.params['pn_command'] + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + msg='%s: ' % command, + stderr=err.strip(), + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + msg='%s: ' % command, + stdout=out.strip(), + changed=False + ) + + else: + module.exit_json( + command=cli, + msg='%s: Nothing to display!!!' % command, + changed=False + ) + + +def main(): + """ This section is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=True, type='str'), + pn_clipassword=dict(required=True, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str'), + pn_command=dict(required=True, type='str'), + pn_parameters=dict(default='all', type='str'), + pn_options=dict(type='str') + ) + ) + + # Accessing the arguments + command = module.params['pn_command'] + parameters = module.params['pn_parameters'] + options = module.params['pn_options'] + + # Building the CLI command string + cli = pn_cli(module) + + cli += ' %s format %s ' % (command, parameters) + + if options: + cli += options + + run_cli(module, cli) + +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 8fba75ce365bc229133955d29ec8ec153e5ace03 Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:08:37 -0700 Subject: [PATCH 334/770] pn_vrouterlbif (New Module) (#4735) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_vrouterlbif.py | 324 +++++++++++++++++++++++++++++ 1 file changed, 324 insertions(+) create mode 100644 network/netvisor/pn_vrouterlbif.py diff --git a/network/netvisor/pn_vrouterlbif.py b/network/netvisor/pn_vrouterlbif.py new file mode 100644 index 00000000000..1b59311d22a --- /dev/null +++ b/network/netvisor/pn_vrouterlbif.py @@ -0,0 +1,324 @@ +#!/usr/bin/python +""" PN CLI vrouter-loopback-interface-add/remove """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_vrouterlbif +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to add/remove vrouter-loopback-interface. +description: + - Execute vrouter-loopback-interface-add, vrouter-loopback-interface-remove + commands. + - Each fabric, cluster, standalone switch, or virtual network (VNET) can + provide its tenants with a virtual router (vRouter) service that forwards + traffic between networks and implements Layer 3 protocols. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run the cli on. + required: False + state: + description: + - State the action to perform. Use 'present' to add vrouter loopback + interface and 'absent' to remove vrouter loopback interface. + required: True + choices: ['present', 'absent'] + pn_vrouter_name: + description: + - Specify the name of the vRouter. + required: True + pn_index: + description: + - Specify the interface index from 1 to 255. + pn_interface_ip: + description: + - Specify the IP address. + required: True +""" + +EXAMPLES = """ +- name: add vrouter-loopback-interface + pn_vrouterlbif: + state: 'present' + pn_vrouter_name: 'ansible-vrouter' + pn_interface_ip: '104.104.104.1' + +- name: remove vrouter-loopback-interface + pn_vrouterlbif: + state: 'absent' + pn_vrouter_name: 'ansible-vrouter' + pn_interface_ip: '104.104.104.1' +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the vrouterlb command. + returned: always + type: list +stderr: + description: The set of error responses from the vrouterlb command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + + +VROUTER_EXISTS = None +LB_INTERFACE_EXISTS = None +# Index range +MIN_INDEX = 1 +MAX_INDEX = 255 + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks if vRouter exists on the target node. + This method also checks for idempotency using the + vrouter-loopback-interface-show command. + If the given vRouter exists, return VROUTER_EXISTS as True else False. + If a loopback interface with the given ip exists on the given vRouter, + return LB_INTERFACE_EXISTS as True else False. + + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VROUTER_EXISTS, LB_INTERFACE_EXISTS + """ + vrouter_name = module.params['pn_vrouter_name'] + interface_ip = module.params['pn_interface_ip'] + + # Global flags + global VROUTER_EXISTS, LB_INTERFACE_EXISTS + + # Check for vRouter + check_vrouter = cli + ' vrouter-show format name no-show-headers ' + check_vrouter = shlex.split(check_vrouter) + out = module.run_command(check_vrouter)[1] + out = out.split() + + if vrouter_name in out: + VROUTER_EXISTS = True + else: + VROUTER_EXISTS = False + + # Check for loopback interface + show = (cli + ' vrouter-loopback-interface-show vrouter-name %s format ip ' + 'no-show-headers' % vrouter_name) + show = shlex.split(show) + out = module.run_command(show)[1] + out = out.split() + + if interface_ip in out: + LB_INTERFACE_EXISTS = True + else: + LB_INTERFACE_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vrouter-loopback-interface-add' + if state == 'absent': + command = 'vrouter-loopback-interface-remove' + return command + + +def main(): + """ This portion is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state =dict(required=True, type='str', + choices=['present', 'absent']), + pn_vrouter_name=dict(required=True, type='str'), + pn_interface_ip=dict(type='str'), + pn_index=dict(type='int') + ), + required_if=( + ["state", "present", + ["pn_vrouter_name", "pn_interface_ip"]], + ["state", "absent", + ["pn_vrouter_name", "pn_interface_ip"]] + ) + ) + + # Accessing the arguments + state = module.params['state'] + vrouter_name = module.params['pn_vrouter_name'] + interface_ip = module.params['pn_interface_ip'] + index = module.params['pn_index'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + if index: + if not MIN_INDEX <= index <= MAX_INDEX: + module.exit_json( + msg="Index must be between 1 and 255", + changed=False + ) + index = str(index) + + if command == 'vrouter-loopback-interface-remove': + check_cli(module, cli) + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + if LB_INTERFACE_EXISTS is False: + module.exit_json( + skipped=True, + msg=('Loopback interface with IP %s does not exist on %s' + % (interface_ip, vrouter_name)) + ) + if not index: + # To remove loopback interface, we need the index. + # If index is not specified, get the Loopback interface index + # using the given interface ip. + get_index = cli + get_index += (' vrouter-loopback-interface-show vrouter-name %s ip ' + '%s ' % (vrouter_name, interface_ip)) + get_index += 'format index no-show-headers' + + get_index = shlex.split(get_index) + out = module.run_command(get_index)[1] + index = out.split()[1] + + cli += ' %s vrouter-name %s index %s' % (command, vrouter_name, index) + + if command == 'vrouter-loopback-interface-add': + check_cli(module, cli) + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg=('vRouter %s does not exist' % vrouter_name) + ) + if LB_INTERFACE_EXISTS is True: + module.exit_json( + skipped=True, + msg=('Loopback interface with IP %s already exists on %s' + % (interface_ip, vrouter_name)) + ) + cli += (' %s vrouter-name %s ip %s' + % (command, vrouter_name, interface_ip)) + if index: + cli += ' index %s ' % index + + run_cli(module, cli) + +# Ansible boiler-plate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From ae2148438fb2ab5b1792b0f4455030d468b8d41c Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:08:47 -0700 Subject: [PATCH 335/770] pn_vrouterif (New Module) (#4734) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_vrouterif.py | 479 +++++++++++++++++++++++++++++++ 1 file changed, 479 insertions(+) create mode 100644 network/netvisor/pn_vrouterif.py diff --git a/network/netvisor/pn_vrouterif.py b/network/netvisor/pn_vrouterif.py new file mode 100644 index 00000000000..dd3c60c3a67 --- /dev/null +++ b/network/netvisor/pn_vrouterif.py @@ -0,0 +1,479 @@ +#!/usr/bin/python +""" PN-CLI vrouter-interface-add/remove/modify """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_vrouterif +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to add/remove/modify vrouter-interface. +description: + - Execute vrouter-interface-add, vrouter-interface-remove, + vrouter-interface-modify command. + - You configure interfaces to vRouter services on a fabric, cluster, + standalone switch or virtula network(VNET). +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch to run the cli on. + required: False + state: + description: + - State the action to perform. Use 'present' to add vrouter interface, + 'absent' to remove vrouter interface and 'update' to modify vrouter + interface. + required: True + choices: ['present', 'absent', 'update'] + pn_vrouter_name: + description: + - Specify the name of the vRouter interface. + required: True + pn_vlan: + description: + - Specify the VLAN identifier. This is a value between 1 and 4092. + pn_interface_ip: + description: + - Specify the IP address of the interface in x.x.x.x/n format. + pn_assignment: + description: + - Specify the DHCP method for IP address assignment. + choices: ['none', 'dhcp', 'dhcpv6', 'autov6'] + pn_vxlan: + description: + - Specify the VXLAN identifier. This is a value between 1 and 16777215. + pn_interface: + description: + - Specify if the interface is management, data or span interface. + choices: ['mgmt', 'data', 'span'] + pn_alias: + description: + - Specify an alias for the interface. + pn_exclusive: + description: + - Specify if the interface is exclusive to the configuration. Exclusive + means that other configurations cannot use the interface. Exclusive is + specified when you configure the interface as span interface and allows + higher throughput through the interface. + pn_nic_enable: + description: + - Specify if the NIC is enabled or not + pn_vrrp_id: + description: + - Specify the ID for the VRRP interface. The IDs on both vRouters must be + the same IS number. + pn_vrrp_priority: + description: + - Specify the priority for the VRRP interface. This is a value between + 1 (lowest) and 255 (highest). + pn_vrrp_adv_int: + description: + - Specify a VRRP advertisement interval in milliseconds. The range is + from 30 to 40950 with a default value of 1000. + pn_l3port: + description: + - Specify a Layer 3 port for the interface. + pn_secondary_macs: + description: + - Specify a secondary MAC address for the interface. + pn_nic_str: + description: + - Specify the type of NIC. Used for vrouter-interface remove/modify. +""" + +EXAMPLES = """ +- name: Add vrouter-interface + pn_vrouterif: + pn_cliusername: admin + pn_clipassword: admin + state: 'present' + pn_vrouter_name: 'ansible-vrouter' + pn_interface_ip: 101.101.101.2/24 + pn_vlan: 101 + +- name: Add VRRP.. + pn_vrouterif: + pn_cliusername: admin + pn_clipassword: admin + state: 'present' + pn_vrouter_name: 'ansible-vrouter' + pn_interface_ip: 101.101.101.2/24 + pn_vrrp_ip: 101.101.101.1/24 + pn_vrrp_priority: 100 + pn_vlan: 101 + +- name: Remove vrouter-interface + pn_vrouterif: + pn_cliusername: admin + pn_clipassword: admin + state: 'absent' + pn_vrouter_name: 'ansible-vrouter' + pn_interface_ip: 101.101.101.2/24 +""" + +RETURN = """ +vrouterifcmd: + description: The CLI command run on the target node(s). +stdout/msg: + description: The set of responses from the vrouterif command. + returned: on success + type: list +stderr/msg: + description: The set of error responses from the vrouterif command. + returned: on error + type: str +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + + +VROUTER_EXISTS = None +INTERFACE_EXISTS = None +NIC_EXISTS = None +VRRP_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks if vRouter exists on the target node. + This method also checks for idempotency using the vrouter-interface-show + command. + If the given vRouter exists, return VROUTER_EXISTS as True else False. + + If an interface with the given ip exists on the given vRouter, + return INTERFACE_EXISTS as True else False. This is required for + vrouter-interface-add. + + If nic_str exists on the given vRouter, return NIC_EXISTS as True else + False. This is required for vrouter-interface-remove. + + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VROUTER_EXISTS, INTERFACE_EXISTS, NIC_EXISTS + """ + vrouter_name = module.params['pn_vrouter_name'] + interface_ip = module.params['pn_interface_ip'] + nic_str = module.params['pn_nic_str'] + + # Global flags + global VROUTER_EXISTS, INTERFACE_EXISTS, NIC_EXISTS + + # Check for vRouter + check_vrouter = cli + ' vrouter-show format name no-show-headers ' + check_vrouter = shlex.split(check_vrouter) + out = module.run_command(check_vrouter)[1] + out = out.split() + + if vrouter_name in out: + VROUTER_EXISTS = True + else: + VROUTER_EXISTS = False + + if interface_ip: + # Check for interface and VRRP and fetch nic for VRRP + show = cli + ' vrouter-interface-show vrouter-name %s ' % vrouter_name + show += 'ip %s format ip,nic no-show-headers' % interface_ip + show = shlex.split(show) + out = module.run_command(show)[1] + if out: + INTERFACE_EXISTS = True + else: + INTERFACE_EXISTS = False + + if nic_str: + # Check for nic + show = cli + ' vrouter-interface-show vrouter-name %s ' % vrouter_name + show += ' format nic no-show-headers' + show = shlex.split(show) + out = module.run_command(show)[1] + if nic_str in out: + NIC_EXISTS = True + else: + NIC_EXISTS = False + + +def get_nic(module, cli): + """ + This module checks if VRRP interface can be added. If No, return VRRP_EXISTS + as True. + If Yes, fetch the nic string from the primary interface and return nic and + VRRP_EXISTS as False. + :param module: + :param cli: + :return: nic, Global Boolean: VRRP_EXISTS + """ + vrouter_name = module.params['pn_vrouter_name'] + interface_ip = module.params['pn_interface_ip'] + + global VRRP_EXISTS + + # Check for interface and VRRP and fetch nic for VRRP + show = cli + ' vrouter-interface-show vrouter-name %s ' % vrouter_name + show += 'ip %s format ip,nic no-show-headers' % interface_ip + show = shlex.split(show) + out = module.run_command(show)[1] + out = out.split() + + if len(out) > 3: + VRRP_EXISTS = True + return None + else: + nic = out[2] + VRRP_EXISTS = False + return nic + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vrouter-interface-add' + if state == 'absent': + command = 'vrouter-interface-remove' + if state == 'update': + command = 'vrouter-interface-modify' + return command + + +def main(): + """ This portion is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state =dict(required=True, type='str', + choices=['present', 'absent']), + pn_vrouter_name=dict(required=True, type='str'), + pn_vlan=dict(type='int'), + pn_interface_ip=dict(required=True, type='str'), + pn_assignment=dict(type='str', + choices=['none', 'dhcp', 'dhcpv6', 'autov6']), + pn_vxlan=dict(type='int'), + pn_interface=dict(type='str', choices=['mgmt', 'data', 'span']), + pn_alias=dict(type='str'), + pn_exclusive=dict(type='bool'), + pn_nic_enable=dict(type='bool'), + pn_vrrp_id=dict(type='int'), + pn_vrrp_priority=dict(type='int'), + pn_vrrp_adv_int=dict(type='str'), + pn_l3port=dict(type='str'), + pn_secondary_macs=dict(type='str'), + pn_nic_str=dict(type='str') + ), + required_if=( + ["state", "present", + ["pn_vrouter_name", "pn_interface_ip"]], + ["state", "absent", + ["pn_vrouter_name", "pn_nic_str"]] + ), + ) + + # Accessing the arguments + state = module.params['state'] + vrouter_name = module.params['pn_vrouter_name'] + vlan = module.params['pn_vlan'] + interface_ip = module.params['pn_interface_ip'] + assignment = module.params['pn_assignment'] + vxlan = module.params['pn_vxlan'] + interface = module.params['pn_interface'] + alias = module.params['pn_alias'] + exclusive = module.params['pn_exclusive'] + nic_enable = module.params['pn_nic_enable'] + vrrp_id = module.params['pn_vrrp_id'] + vrrp_priority = module.params['pn_vrrp_priority'] + vrrp_adv_int = module.params['pn_vrrp_adv_int'] + l3port = module.params['pn_l3port'] + secondary_macs = module.params['pn_secondary_macs'] + nic_str = module.params['pn_nic_str'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + check_cli(module, cli) + if command == 'vrouter-interface-add': + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + + if vrrp_id: + vrrp_primary = get_nic(module, cli) + if VRRP_EXISTS is True: + module.exit_json( + skipped=True, + msg=('VRRP interface on %s already exists. Check ' + 'the IP addresses' % vrouter_name) + ) + cli += ' %s vrouter-name %s ' % (command, vrouter_name) + cli += (' ip %s vrrp-primary %s vrrp-id %s ' + % (interface_ip, vrrp_primary, str(vrrp_id))) + if vrrp_priority: + cli += ' vrrp-priority %s ' % str(vrrp_priority) + if vrrp_adv_int: + cli += ' vrrp-adv-int %s ' % vrrp_adv_int + + else: + if INTERFACE_EXISTS is True: + module.exit_json( + skipped=True, + msg=('vRouter interface on %s already exists. Check the ' + 'IP addresses' % vrouter_name) + ) + cli += ' %s vrouter-name %s ' % (command, vrouter_name) + cli += ' ip %s ' % interface_ip + + if vlan: + cli += ' vlan ' + str(vlan) + + if l3port: + cli += ' l3-port ' + l3port + + if assignment: + cli += ' assignment ' + assignment + + if vxlan: + cli += ' vxlan ' + str(vxlan) + + if interface: + cli += ' if ' + interface + + if alias: + cli += ' alias-on ' + alias + + if exclusive is True: + cli += ' exclusive ' + if exclusive is False: + cli += ' no-exclusive ' + + if nic_enable is True: + cli += ' nic-enable ' + if nic_enable is False: + cli += ' nic-disable ' + + if secondary_macs: + cli += ' secondary-macs ' + secondary_macs + + if command == 'vrouter-interface-remove': + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + if NIC_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter interface with nic %s does not exist' % nic_str + ) + cli += ' %s vrouter-name %s nic %s ' % (command, vrouter_name, nic_str) + + run_cli(module, cli) +# Ansible boiler-plate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From d08559fb32d06fb9eea2e26012afb8f59ce9b67d Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:09:03 -0700 Subject: [PATCH 336/770] pn_vrouter (New Module) (#4733) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_vrouter.py | 412 +++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 network/netvisor/pn_vrouter.py diff --git a/network/netvisor/pn_vrouter.py b/network/netvisor/pn_vrouter.py new file mode 100644 index 00000000000..0e1a6d37fba --- /dev/null +++ b/network/netvisor/pn_vrouter.py @@ -0,0 +1,412 @@ +#!/usr/bin/python +""" PN CLI vrouter-create/vrouter-delete/vrouter-modify """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_vrouter +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1 +short_description: CLI command to create/delete/modify a vrouter. +description: + - Execute vrouter-create, vrouter-delete, vrouter-modify command. + - Each fabric, cluster, standalone switch, or virtual network (VNET) can + provide its tenants with a virtual router (vRouter) service that forwards + traffic between networks and implements Layer 3 protocols. + - C(vrouter-create) creates a new vRouter service. + - C(vrouter-delete) deletes a vRouter service. + - C(vrouter-modify) modifies a vRouter service. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run the CLI on. + required: False + state: + description: + - State the action to perform. Use 'present' to create vrouter, + 'absent' to delete vrouter and 'update' to modify vrouter. + required: True + choices: ['present', 'absent', 'update'] + pn_name: + description: + - Specify the name of the vRouter. + required: true + pn_vnet: + description: + - Specify the name of the VNET. + - Required for vrouter-create. + pn_service_type: + description: + - Specify if the vRouter is a dedicated or shared VNET service. + choices: ['dedicated', 'shared'] + pn_service_state: + description: + - Specify to enable or disable vRouter service. + choices: ['enable', 'disable'] + pn_router_type: + description: + - Specify if the vRouter uses software or hardware. + - Note that if you specify hardware as router type, you cannot assign IP + addresses using DHCP. You must specify a static IP address. + choices: ['hardware', 'software'] + pn_hw_vrrp_id: + description: + - Specifies the VRRP ID for a hardware vrouter. + pn_router_id: + description: + - Specify the vRouter IP address. + pn_bgp_as: + description: + - Specify the Autonomous System Number(ASN) if the vRouter runs Border + Gateway Protocol(BGP). + pn_bgp_redistribute: + description: + - Specify how BGP routes are redistributed. + choices: ['static', 'connected', 'rip', 'ospf'] + pn_bgp_max_paths: + description: + - Specify the maximum number of paths for BGP. This is a number between + 1 and 255 or 0 to unset. + pn_bgp_options: + description: + - Specify other BGP options as a whitespaces separted string within + single quotes ''. + pn_rip_redistribute: + description: + - Specify how RIP routes are redistributed. + choices: ['static', 'connected', 'ospf', 'bgp'] + pn_ospf_redistribute: + description: + - Specify how OSPF routes are redistributed. + choices: ['static', 'connected', 'bgp', 'rip'] + pn_ospf_options: + description: + - Specify other OSPF options as a whitespaces separated string within + single quotes ''. +""" + +EXAMPLES = """ +- name: create vrouter + pn_vrouter: + state: 'present' + pn_name: 'ansible-vrouter' + pn_vnet: 'ansible-fab-global' + pn_router_id: 208.74.182.1 + +- name: delete vrouter + pn_vrouter: + state: 'absent' + pn_name: 'ansible-vrouter' +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the vrouter command. + returned: always + type: list +stderr: + description: The set of error responses from the vrouter command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + +VROUTER_EXISTS = None +VROUTER_NAME_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks for idempotency using the vlan-show command. + A switch can have only one vRouter configuration. + If a vRouter already exists on the given switch, return VROUTER_EXISTS as + True else False. + If a vRouter with the given name exists(on a different switch), return + VROUTER_NAME_EXISTS as True else False. + + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VROUTER_EXISTS, VROUTER_NAME_EXISTS + """ + name = module.params['pn_name'] + # Global flags + global VROUTER_EXISTS, VROUTER_NAME_EXISTS + + # Get the name of the local switch + location = cli + ' switch-setup-show format switch-name' + location = shlex.split(location) + out = module.run_command(location)[1] + location = out.split()[1] + + # Check for any vRouters on the switch + check_vrouter = cli + ' vrouter-show location %s ' % location + check_vrouter += 'format name no-show-headers' + check_vrouter = shlex.split(check_vrouter) + out = module.run_command(check_vrouter)[1] + + if out: + VROUTER_EXISTS = True + else: + VROUTER_EXISTS = False + + # Check for any vRouters with the given name + show = cli + ' vrouter-show format name no-show-headers ' + show = shlex.split(show) + out = module.run_command(show)[1] + out = out.split() + + if name in out: + VROUTER_NAME_EXISTS = True + else: + VROUTER_NAME_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vrouter-create' + if state == 'absent': + command = 'vrouter-delete' + if state == 'update': + command = 'vrouter-modify' + return command + + +def main(): + """ This section is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state =dict(required=True, type='str', + choices=['present', 'absent', 'update']), + pn_name=dict(required=True, type='str'), + pn_vnet=dict(type='str'), + pn_service_type=dict(type='str', choices=['dedicated', 'shared']), + pn_service_state=dict(type='str', choices=['enable', 'disable']), + pn_router_type=dict(type='str', choices=['hardware', 'software']), + pn_hw_vrrp_id=dict(type='int'), + pn_router_id=dict(type='str'), + pn_bgp_as=dict(type='int'), + pn_bgp_redistribute=dict(type='str', choices=['static', 'connected', + 'rip', 'ospf']), + pn_bgp_max_paths=dict(type='int'), + pn_bgp_options=dict(type='str'), + pn_rip_redistribute=dict(type='str', choices=['static', 'connected', + 'bgp', 'ospf']), + pn_ospf_redistribute=dict(type='str', choices=['static', 'connected', + 'bgp', 'rip']), + pn_ospf_options=dict(type='str'), + pn_vrrp_track_port=dict(type='str') + ), + required_if=( + ["state", "present", ["pn_name", "pn_vnet"]], + ["state", "absent", ["pn_name"]], + ["state", "update", ["pn_name"]] + ) + ) + + # Accessing the arguments + state = module.params['state'] + name = module.params['pn_name'] + vnet = module.params['pn_vnet'] + service_type = module.params['pn_service_type'] + service_state = module.params['pn_service_state'] + router_type = module.params['pn_router_type'] + hw_vrrp_id = module.params['pn_hw_vrrp_id'] + router_id = module.params['pn_router_id'] + bgp_as = module.params['pn_bgp_as'] + bgp_redistribute = module.params['pn_bgp_redistribute'] + bgp_max_paths = module.params['pn_bgp_max_paths'] + bgp_options = module.params['pn_bgp_options'] + rip_redistribute = module.params['pn_rip_redistribute'] + ospf_redistribute = module.params['pn_ospf_redistribute'] + ospf_options = module.params['pn_ospf_options'] + vrrp_track_port = module.params['pn_vrrp_track_port'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + if command == 'vrouter-delete': + check_cli(module, cli) + if VROUTER_NAME_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter with name %s does not exist' % name + ) + cli += ' %s name %s ' % (command, name) + + else: + + if command == 'vrouter-create': + check_cli(module, cli) + if VROUTER_EXISTS is True: + module.exit_json( + skipped=True, + msg='Maximum number of vRouters has been reached on this ' + 'switch' + ) + if VROUTER_NAME_EXISTS is True: + module.exit_json( + skipped=True, + msg='vRouter with name %s already exists' % name + ) + cli += ' %s name %s ' % (command, name) + + if vnet: + cli += ' vnet ' + vnet + + if service_type: + cli += ' %s-vnet-service ' % service_type + + if service_state: + cli += ' ' + service_state + + if router_type: + cli += ' router-type ' + router_type + + if hw_vrrp_id: + cli += ' hw-vrrp-id ' + str(hw_vrrp_id) + + if router_id: + cli += ' router-id ' + router_id + + if bgp_as: + cli += ' bgp-as ' + str(bgp_as) + + if bgp_redistribute: + cli += ' bgp-redistribute ' + bgp_redistribute + + if bgp_max_paths: + cli += ' bgp-max-paths ' + str(bgp_max_paths) + + if bgp_options: + cli += ' %s ' % bgp_options + + if rip_redistribute: + cli += ' rip-redistribute ' + rip_redistribute + + if ospf_redistribute: + cli += ' ospf-redistribute ' + ospf_redistribute + + if ospf_options: + cli += ' %s ' % ospf_options + + if vrrp_track_port: + cli += ' vrrp-track-port ' + vrrp_track_port + + run_cli(module, cli) + +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From ad94b51492eb2f8fda52a0d403d8c25313379ad8 Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:09:13 -0700 Subject: [PATCH 337/770] pn_vlan (New Module) (#4732) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_vlan.py | 309 ++++++++++++++++++++++++++++++++++++ 1 file changed, 309 insertions(+) create mode 100644 network/netvisor/pn_vlan.py diff --git a/network/netvisor/pn_vlan.py b/network/netvisor/pn_vlan.py new file mode 100644 index 00000000000..05438aa3eda --- /dev/null +++ b/network/netvisor/pn_vlan.py @@ -0,0 +1,309 @@ +#!/usr/bin/python +""" PN CLI vlan-create/vlan-delete """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_vlan +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to create/delete a VLAN. +description: + - Execute vlan-create or vlan-delete command. + - VLANs are used to isolate network traffic at Layer 2.The VLAN identifiers + 0 and 4095 are reserved and cannot be used per the IEEE 802.1Q standard. + The range of configurable VLAN identifiers is 2 through 4092. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run the cli on. + required: False + state: + description: + - State the action to perform. Use 'present' to create vlan and + 'absent' to delete vlan. + required: True + choices: ['present', 'absent'] + pn_vlanid: + description: + - Specify a VLAN identifier for the VLAN. This is a value between + 2 and 4092. + required: True + pn_scope: + description: + - Specify a scope for the VLAN. + - Required for vlan-create. + choices: ['fabric', 'local'] + pn_description: + description: + - Specify a description for the VLAN. + pn_stats: + description: + - Specify if you want to collect statistics for a VLAN. Statistic + collection is enabled by default. + pn_ports: + description: + - Specifies the switch network data port number, list of ports, or range + of ports. Port numbers must ne in the range of 1 to 64. + pn_untagged_ports: + description: + - Specifies the ports that should have untagged packets mapped to the + VLAN. Untagged packets are packets that do not contain IEEE 802.1Q VLAN + tags. +""" + +EXAMPLES = """ +- name: create a VLAN + pn_vlan: + state: 'present' + pn_vlanid: 1854 + pn_scope: fabric + +- name: delete VLANs + pn_vlan: + state: 'absent' + pn_vlanid: 1854 +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the vlan command. + returned: always + type: list +stderr: + description: The set of error responses from the vlan command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + +VLAN_EXISTS = None +MAX_VLAN_ID = 4092 +MIN_VLAN_ID = 2 + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks for idempotency using the vlan-show command. + If a vlan with given vlan id exists, return VLAN_EXISTS as True else False. + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VLAN_EXISTS + """ + vlanid = module.params['pn_vlanid'] + + show = cli + \ + ' vlan-show id %s format id,scope no-show-headers' % str(vlanid) + show = shlex.split(show) + out = module.run_command(show)[1] + + out = out.split() + # Global flags + global VLAN_EXISTS + if str(vlanid) in out: + VLAN_EXISTS = True + else: + VLAN_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state= module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vlan-create' + if state == 'absent': + command = 'vlan-delete' + return command + + +def main(): + """ This section is for arguments parsing """ + arguement_spec = pn_arguement_spec + + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state =dict(required=True, type='str', + choices=['present', 'absent']), + pn_vlanid=dict(required=True, type='int'), + pn_scope=dict(type='str', choices=['fabric', 'local']), + pn_description=dict(type='str'), + pn_stats=dict(type='bool'), + pn_ports=dict(type='str'), + pn_untagged_ports=dict(type='str') + ), + required_if=( + ["state", "present", ["pn_vlanid", "pn_scope"]], + ["state", "absent", ["pn_vlanid"]] + ) + ) + + # Accessing the arguments + state = module.params['state'] + vlanid = module.params['pn_vlanid'] + scope = module.params['pn_scope'] + description = module.params['pn_description'] + stats = module.params['pn_stats'] + ports = module.params['pn_ports'] + untagged_ports = module.params['pn_untagged_ports'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + if not MIN_VLAN_ID <= vlanid <= MAX_VLAN_ID: + module.exit_json( + msg="VLAN id must be between 2 and 4092", + changed=False + ) + + if command == 'vlan-create': + + check_cli(module, cli) + if VLAN_EXISTS is True: + module.exit_json( + skipped=True, + msg='VLAN with id %s already exists' % str(vlanid) + ) + + cli += ' %s id %s scope %s ' % (command, str(vlanid), scope) + + if description: + cli += ' description ' + description + + if stats is True: + cli += ' stats ' + if stats is False: + cli += ' no-stats ' + + if ports: + cli += ' ports ' + ports + + if untagged_ports: + cli += ' untagged-ports ' + untagged_ports + + if command == 'vlan-delete': + + check_cli(module, cli) + if VLAN_EXISTS is False: + module.exit_json( + skipped=True, + msg='VLAN with id %s does not exist' % str(vlanid) + ) + + cli += ' %s id %s ' % (command, str(vlanid)) + + run_cli(module, cli) + +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From b5a0e5a83cab7e0f428342d3c1f8f1a29b6cb66d Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:09:23 -0700 Subject: [PATCH 338/770] pn_vlag (New Module) (#4731) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_vlag.py | 342 ++++++++++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 network/netvisor/pn_vlag.py diff --git a/network/netvisor/pn_vlag.py b/network/netvisor/pn_vlag.py new file mode 100644 index 00000000000..0387d80b6d1 --- /dev/null +++ b/network/netvisor/pn_vlag.py @@ -0,0 +1,342 @@ +#!/usr/bin/python +""" PN CLI vlag-create/vlag-delete/vlag-modify """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_vlag +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to create/delete/modify vlag. +description: + - Execute vlag-create/vlag-delete/vlag-modify command. + - A virtual link aggregation group (VLAG) allows links that are physically + connected to two different Pluribus Networks devices to appear as a single + trunk to a third device. The third device can be a switch, server, or any + Ethernet device. A VLAG can provide Layer 2 multipathing, which allows you + to create redundancy by increasing bandwidth, enabling multiple parallel + paths between nodes and loadbalancing traffic where alternative paths exist. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run this command on. + state: + description: + - State the action to perform. Use 'present' to create vlag, + 'absent' to delete vlag and 'update' to modify vlag. + required: True + choices: ['present', 'absent', 'update'] + pn_name: + description: + - The C(pn_name) takes a valid name for vlag configuration. + required: true + pn_port: + description: + - Specify the local VLAG port. + - Required for vlag-create. + pn_peer_port: + description: + - Specify the peer VLAG port. + - Required for vlag-create. + pn_mode: + description: + - Specify the mode for the VLAG. Active-standby indicates one side is + active and the other side is in standby mode. Active-active indicates + that both sides of the vlag are up by default. + choices: ['active-active', 'active-standby'] + pn_peer_switch: + description: + - Specify the fabric-name of the peer switch. + pn_failover_action: + description: + - Specify the failover action as move or ignore. + choices: ['move', 'ignore'] + pn_lacp_mode: + description: + - Specify the LACP mode. + choices: ['off', 'passive', 'active'] + pn_lacp_timeout: + description: + - Specify the LACP timeout as slow(30 seconds) or fast(4 seconds). + choices: ['slow', 'fast'] + pn_lacp_fallback: + description: + - Specify the LACP fallback mode as bundles or individual. + choices: ['bundle', 'individual'] + pn_lacp_fallback_timeout: + description: + - Specify the LACP fallback timeout in seconds. The range is between 30 + and 60 seconds with a default value of 50 seconds. +""" + +EXAMPLES = """ +- name: create a VLAG + pn_vlag: + state: 'present' + pn_name: spine-to-leaf + pn_port: 'spine01-to-leaf' + pn_peer_port: 'spine02-to-leaf' + pn_peer_switch: spine02 + pn_mode: 'active-active' + +- name: delete VLAGs + pn_vlag: + state: 'absent' + pn_name: spine-to-leaf +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the vlag command. + returned: always + type: list +stderr: + description: The set of error responses from the vlag command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + +VLAG_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks for idempotency using the vlag-show command. + If a vlag with given vlag exists, return VLAG_EXISTS as True else False. + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VLAG_EXISTS + """ + name = module.params['pn_name'] + + show = cli + ' vlag-show format name no-show-headers' + show = shlex.split(show) + out = module.run_command(show)[1] + + out = out.split() + # Global flags + global VLAG_EXISTS + if name in out: + VLAG_EXISTS = True + else: + VLAG_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vlag-create' + if state == 'absent': + command = 'vlag-delete' + if state == 'update': + command = 'vlag-modify' + return command + + +def main(): + """ This section is for argument parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state =dict(required=True, type='str', + choices=['present', 'absent', 'update']), + pn_name=dict(required=True, type='str'), + pn_port=dict(type='str'), + pn_peer_port=dict(type='str'), + pn_mode=dict(type='str', choices=[ + 'active-standby', 'active-active']), + pn_peer_switch=dict(type='str'), + pn_failover_action=dict(type='str', choices=['move', 'ignore']), + pn_lacp_mode=dict(type='str', choices=[ + 'off', 'passive', 'active']), + pn_lacp_timeout=dict(type='str', choices=['slow', 'fast']), + pn_lacp_fallback=dict(type='str', choices=[ + 'individual', 'bundled']), + pn_lacp_fallback_timeout=dict(type='str') + ), + required_if=( + ["state", "present", ["pn_name", "pn_port", "pn_peer_port", + "pn_peer_switch"]], + ["state", "absent", ["pn_name"]], + ["state", "update", ["pn_name"]] + ) + ) + + # Argument accessing + state = module.params['state'] + name = module.params['pn_name'] + port = module.params['pn_port'] + peer_port = module.params['pn_peer_port'] + mode = module.params['pn_mode'] + peer_switch = module.params['pn_peer_switch'] + failover_action = module.params['pn_failover_action'] + lacp_mode = module.params['pn_lacp_mode'] + lacp_timeout = module.params['pn_lacp_timeout'] + lacp_fallback = module.params['pn_lacp_fallback'] + lacp_fallback_timeout = module.params['pn_lacp_fallback_timeout'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + if command == 'vlag-delete': + + check_cli(module, cli) + if VLAG_EXISTS is False: + module.exit_json( + skipped=True, + msg='VLAG with name %s does not exist' % name + ) + cli += ' %s name %s ' % (command, name) + + else: + + if command == 'vlag-create': + check_cli(module, cli) + if VLAG_EXISTS is True: + module.exit_json( + skipped=True, + msg='VLAG with name %s already exists' % name + ) + cli += ' %s name %s ' % (command, name) + + if port: + cli += ' port %s peer-port %s ' % (port, peer_port) + + if mode: + cli += ' mode ' + mode + + if peer_switch: + cli += ' peer-switch ' + peer_switch + + if failover_action: + cli += ' failover-' + failover_action + '-L2 ' + + if lacp_mode: + cli += ' lacp-mode ' + lacp_mode + + if lacp_timeout: + cli += ' lacp-timeout ' + lacp_timeout + + if lacp_fallback: + cli += ' lacp-fallback ' + lacp_fallback + + if lacp_fallback_timeout: + cli += ' lacp-fallback-timeout ' + lacp_fallback_timeout + + run_cli(module, cli) + +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 312cb3f8e9ee366ba833d1aca618dcf8fde9cd22 Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:09:46 -0700 Subject: [PATCH 339/770] pn_trunk (New Module) (#4728) * Cluster Module * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_trunk.py | 446 +++++++++++++++++++++++++++++++++++ 1 file changed, 446 insertions(+) create mode 100644 network/netvisor/pn_trunk.py diff --git a/network/netvisor/pn_trunk.py b/network/netvisor/pn_trunk.py new file mode 100644 index 00000000000..9d06cfc668e --- /dev/null +++ b/network/netvisor/pn_trunk.py @@ -0,0 +1,446 @@ +#!/usr/bin/python +""" PN CLI trunk-create/trunk-delete/trunk-modify """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_trunk +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to create/delete/modify a trunk. +description: + - Execute trunk-create or trunk-delete command. + - Trunks can be used to aggregate network links at Layer 2 on the local + switch. Use this command to create a new trunk. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run the cli on. + required: False + state: + description: + - State the action to perform. Use 'present' to create trunk, + 'absent' to delete trunk and 'update' to modify trunk. + required: True + choices: ['present', 'absent', 'update'] + pn_name: + description: + - Specify the name for the trunk configuration. + required: true + pn_ports: + description: + - Specify the port number(s) for the link(s) to aggregate into the trunk. + - Required for trunk-create. + pn_speed: + description: + - Specify the port speed or disable the port. + choices: ['disable', '10m', '100m', '1g', '2.5g', '10g', '40g'] + pn_egress_rate_limit: + description: + - Specify an egress port data rate limit for the configuration. + pn_jumbo: + description: + - Specify if the port can receive jumbo frames. + pn_lacp_mode: + description: + - Specify the LACP mode for the configuration. + choices: ['off', 'passive', 'active'] + pn_lacp_priority: + description: + - Specify the LACP priority. This is a number between 1 and 65535 with a + default value of 32768. + pn_lacp_timeout: + description: + - Specify the LACP time out as slow (30 seconds) or fast (4seconds). + The default value is slow. + choices: ['slow', 'fast'] + pn_lacp_fallback: + description: + - Specify the LACP fallback mode as bundles or individual. + choices: ['bundle', 'individual'] + pn_lacp_fallback_timeout: + description: + - Specify the LACP fallback timeout in seconds. The range is between 30 + and 60 seconds with a default value of 50 seconds. + pn_edge_switch: + description: + - Specify if the switch is an edge switch. + pn_pause: + description: + - Specify if pause frames are sent. + pn_description: + description: + - Specify a description for the trunk configuration. + pn_loopback: + description: + - Specify loopback if you want to use loopback. + pn_mirror_receive: + description: + - Specify if the configuration receives mirrored traffic. + pn_unkown_ucast_level: + description: + - Specify an unkown unicast level in percent. The default value is 100%. + pn_unkown_mcast_level: + description: + - Specify an unkown multicast level in percent. The default value is 100%. + pn_broadcast_level: + description: + - Specify a broadcast level in percent. The default value is 100%. + pn_port_macaddr: + description: + - Specify the MAC address of the port. + pn_loopvlans: + description: + - Specify a list of looping vlans. + pn_routing: + description: + - Specify if the port participates in routing on the network. + pn_host: + description: + - Host facing port control setting. +""" + +EXAMPLES = """ +- name: create trunk + pn_trunk: + state: 'present' + pn_name: 'spine-to-leaf' + pn_ports: '11,12,13,14' + +- name: delete trunk + pn_trunk: + state: 'absent' + pn_name: 'spine-to-leaf' +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the trunk command. + returned: always + type: list +stderr: + description: The set of error responses from the trunk command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" +TRUNK_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks for idempotency using the trunk-show command. + If a trunk with given name exists, return TRUNK_EXISTS as True else False. + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: TRUNK_EXISTS + """ + name = module.params['pn_name'] + + show = cli + ' trunk-show format switch,name no-show-headers' + show = shlex.split(show) + out = module.run_command(show)[1] + + out = out.split() + # Global flags + global TRUNK_EXISTS + if name in out: + TRUNK_EXISTS = True + else: + TRUNK_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'trunk-create' + if state == 'absent': + command = 'trunk-delete' + if state == 'update': + command = 'trunk-modify' + return command + + +def main(): + """ This portion is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state=dict(required=True, type='str', + choices=['present', 'absent', 'update']), + pn_name=dict(required=True, type='str'), + pn_ports=dict(type='str'), + pn_speed=dict(type='str', + choices=['disable', '10m', '100m', '1g', '2.5g', + '10g', '40g']), + pn_egress_rate_limit=dict(type='str'), + pn_jumbo=dict(type='bool'), + pn_lacp_mode=dict(type='str', choices=[ + 'off', 'passive', 'active']), + pn_lacp_priority=dict(type='int'), + pn_lacp_timeout=dict(type='str'), + pn_lacp_fallback=dict(type='str', choices=[ + 'bundle', 'individual']), + pn_lacp_fallback_timeout=dict(type='str'), + pn_edge_switch=dict(type='bool'), + pn_pause=dict(type='bool'), + pn_description=dict(type='str'), + pn_loopback=dict(type='bool'), + pn_mirror_receive=dict(type='bool'), + pn_unknown_ucast_level=dict(type='str'), + pn_unknown_mcast_level=dict(type='str'), + pn_broadcast_level=dict(type='str'), + pn_port_macaddr=dict(type='str'), + pn_loopvlans=dict(type='str'), + pn_routing=dict(type='bool'), + pn_host=dict(type='bool') + ), + required_if=( + ["state", "present", ["pn_name", "pn_ports"]], + ["state", "absent", ["pn_name"]], + ["state", "update", ["pn_name"]] + ) + ) + + # Accessing the arguments + state = module.params['state'] + name = module.params['pn_name'] + ports = module.params['pn_ports'] + speed = module.params['pn_speed'] + egress_rate_limit = module.params['pn_egress_rate_limit'] + jumbo = module.params['pn_jumbo'] + lacp_mode = module.params['pn_lacp_mode'] + lacp_priority = module.params['pn_lacp_priority'] + lacp_timeout = module.params['pn_lacp_timeout'] + lacp_fallback = module.params['pn_lacp_fallback'] + lacp_fallback_timeout = module.params['pn_lacp_fallback_timeout'] + edge_switch = module.params['pn_edge_switch'] + pause = module.params['pn_pause'] + description = module.params['pn_description'] + loopback = module.params['pn_loopback'] + mirror_receive = module.params['pn_mirror_receive'] + unknown_ucast_level = module.params['pn_unknown_ucast_level'] + unknown_mcast_level = module.params['pn_unknown_mcast_level'] + broadcast_level = module.params['pn_broadcast_level'] + port_macaddr = module.params['pn_port_macaddr'] + loopvlans = module.params['pn_loopvlans'] + routing = module.params['pn_routing'] + host = module.params['pn_host'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + if command == 'trunk-delete': + + check_cli(module, cli) + if TRUNK_EXISTS is False: + module.exit_json( + skipped=True, + msg='Trunk with name %s does not exist' % name + ) + cli += ' %s name %s ' % (command, name) + + else: + if command == 'trunk-create': + check_cli(module, cli) + if TRUNK_EXISTS is True: + module.exit_json( + skipped=True, + msg='Trunk with name %s already exists' % name + ) + cli += ' %s name %s ' % (command, name) + + # Appending options + if ports: + cli += ' ports ' + ports + + if speed: + cli += ' speed ' + speed + + if egress_rate_limit: + cli += ' egress-rate-limit ' + egress_rate_limit + + if jumbo is True: + cli += ' jumbo ' + if jumbo is False: + cli += ' no-jumbo ' + + if lacp_mode: + cli += ' lacp-mode ' + lacp_mode + + if lacp_priority: + cli += ' lacp-priority ' + lacp_priority + + if lacp_timeout: + cli += ' lacp-timeout ' + lacp_timeout + + if lacp_fallback: + cli += ' lacp-fallback ' + lacp_fallback + + if lacp_fallback_timeout: + cli += ' lacp-fallback-timeout ' + lacp_fallback_timeout + + if edge_switch is True: + cli += ' edge-switch ' + if edge_switch is False: + cli += ' no-edge-switch ' + + if pause is True: + cli += ' pause ' + if pause is False: + cli += ' no-pause ' + + if description: + cli += ' description ' + description + + if loopback is True: + cli += ' loopback ' + if loopback is False: + cli += ' no-loopback ' + + if mirror_receive is True: + cli += ' mirror-receive-only ' + if mirror_receive is False: + cli += ' no-mirror-receive-only ' + + if unknown_ucast_level: + cli += ' unknown-ucast-level ' + unknown_ucast_level + + if unknown_mcast_level: + cli += ' unknown-mcast-level ' + unknown_mcast_level + + if broadcast_level: + cli += ' broadcast-level ' + broadcast_level + + if port_macaddr: + cli += ' port-mac-address ' + port_macaddr + + if loopvlans: + cli += ' loopvlans ' + loopvlans + + if routing is True: + cli += ' routing ' + if routing is False: + cli += ' no-routing ' + + if host is True: + cli += ' host-enable ' + if host is False: + cli += ' host-disable ' + + run_cli(module, cli) + +# Ansible boiler-plate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 0f1039936aec3085d1d8038faf5fd2fe5cd9cfe5 Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:09:56 -0700 Subject: [PATCH 340/770] pn_ospfarea (New Module) (#4737) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_ospfarea.py | 216 ++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 network/netvisor/pn_ospfarea.py diff --git a/network/netvisor/pn_ospfarea.py b/network/netvisor/pn_ospfarea.py new file mode 100644 index 00000000000..e9fd404ddd9 --- /dev/null +++ b/network/netvisor/pn_ospfarea.py @@ -0,0 +1,216 @@ +#!/usr/bin/python +""" PN-CLI vrouter-ospf-add/remove """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_ospfarea +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to add/remove ospf area to/from a vrouter. +description: + - Execute vrouter-ospf-add, vrouter-ospf-remove command. + - This command adds/removes Open Shortest Path First(OSPF) area to/from + a virtual router(vRouter) service. +options: + pn_cliusername: + description: + - Login username. + required: true + pn_clipassword: + description: + - Login password. + required: true + pn_cliswitch: + description: + - Target switch(es) to run the CLI on. + required: False + state: + description: + - State the action to perform. Use 'present' to add ospf-area, 'absent' + to remove ospf-area and 'update' to modify ospf-area. + required: true + choices: ['present', 'absent', 'update'] + pn_vrouter_name: + description: + - Specify the name of the vRouter. + required: true + pn_ospf_area: + description: + - Specify the OSPF area number. + required: true + pn_stub_type: + description: + - Specify the OSPF stub type. + choices: ['none', 'stub', 'stub-no-summary', 'nssa', 'nssa-no-summary'] + pn_prefix_listin: + description: + - OSPF prefix list for filtering incoming packets. + pn_prefix_listout: + description: + - OSPF prefix list for filtering outgoing packets. + pn_quiet: + description: + - Enable/disable system information. + required: false + default: true +""" + +EXAMPLES = """ +- name: "Add OSPF area to vrouter" + pn_ospfarea: + state: present + pn_cliusername: admin + pn_clipassword: admin + pn_ospf_area: 1.0.0.0 + pn_stub_type: stub + +- name: "Remove OSPF from vrouter" + pn_ospf: + state: absent + pn_cliusername: admin + pn_clipassword: admin + pn_vrouter_name: name-string + pn_ospf_area: 1.0.0.0 +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the ospf command. + returned: always + type: list +stderr: + description: The set of error responses from the ospf command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vrouter-ospf-area-add' + if state == 'absent': + command = 'vrouter-ospf-area-remove' + if state == 'update': + command = 'vrouter-ospf-area-modify' + return command + + +def main(): + """ This section is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=True, type='str'), + pn_clipassword=dict(required=True, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str'), + state =dict(required=True, type='str', + choices=['present', 'absent', 'update']), + pn_vrouter_name=dict(required=True, type='str'), + pn_ospf_area=dict(required=True, type='str'), + pn_stub_type=dict(type='str', choices=['none', 'stub', 'nssa', + 'stub-no-summary', + 'nssa-no-summary']), + pn_prefix_listin=dict(type='str'), + pn_prefix_listout=dict(type='str'), + pn_quiet=dict(type='bool', default='True') + ) + ) + + # Accessing the arguments + cliusername = module.params['pn_cliusername'] + clipassword = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + vrouter_name = module.params['pn_vrouter_name'] + ospf_area = module.params['pn_ospf_area'] + stub_type = module.params['pn_stub_type'] + prefix_listin = module.params['pn_prefix_listin'] + prefix_listout = module.params['pn_prefix_listout'] + quiet = module.params['pn_quiet'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = '/usr/bin/cli' + + if quiet is True: + cli += ' --quiet ' + + cli += ' --user %s:%s ' % (cliusername, clipassword) + + if cliswitch: + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + + cli += ' %s vrouter-name %s area %s ' % (command, vrouter_name, ospf_area) + + if stub_type: + cli += ' stub-type ' + stub_type + + if prefix_listin: + cli += ' prefix-list-in ' + prefix_listin + + if prefix_listout: + cli += ' prefix-list-out ' + prefix_listout + + # Run the CLI command + ospfcommand = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(ospfcommand) + + # Response in JSON format + if result != 0: + module.exit_json( + command=cli, + stderr=err.rstrip("\r\n"), + changed=False + ) + + else: + module.exit_json( + command=cli, + stdout=out.rstrip("\r\n"), + changed=True + ) + +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 1d06c7896f1026113accc6901273e587b65e6079 Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 05:10:15 -0700 Subject: [PATCH 341/770] pn_cluster (New Module) (#4727) * made changes to make it 2.4 compatible * added no_log for password * incorporated additional review comments * remove type for options block --- network/netvisor/pn_cluster.py | 312 +++++++++++++++++++++++++++++++++ 1 file changed, 312 insertions(+) create mode 100644 network/netvisor/pn_cluster.py diff --git a/network/netvisor/pn_cluster.py b/network/netvisor/pn_cluster.py new file mode 100644 index 00000000000..76cea3c9d04 --- /dev/null +++ b/network/netvisor/pn_cluster.py @@ -0,0 +1,312 @@ +#!/usr/bin/python +""" PN CLI cluster-create/cluster-delete """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_cluster +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to create/delete a cluster. +description: + - Execute cluster-create or cluster-delete command. + - A cluster allows two switches to cooperate in high-availability (HA) + deployments. The nodes that form the cluster must be members of the same + fabric. Clusters are typically used in conjunction with a virtual link + aggregation group (VLAG) that allows links physically connected to two + separate switches appear as a single trunk to a third device. The third + device can be a switch,server, or any Ethernet device. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch to run the cli on. + required: False + state: + description: + - Specify action to perform. Use 'present' to create cluster and 'absent' + to delete cluster. + required: true + choices: ['present', 'absent'] + pn_name: + description: + - Specify the name of the cluster. + required: true + pn_cluster_node1: + description: + - Specify the name of the first switch in the cluster. + - Required for 'cluster-create'. + pn_cluster_node2: + description: + - Specify the name of the second switch in the cluster. + - Required for 'cluster-create'. + pn_validate: + description: + - Validate the inter-switch links and state of switches in the cluster. + choices: ['validate', 'no-validate'] +""" + +EXAMPLES = """ +- name: create spine cluster + pn_cluster: + state: 'present' + pn_name: 'spine-cluster' + pn_cluster_node1: 'spine01' + pn_cluster_node2: 'spine02' + pn_validate: validate + pn_quiet: True + +- name: delete spine cluster + pn_cluster: + state: 'absent' + pn_name: 'spine-cluster' + pn_quiet: True +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the cluster command. + returned: always + type: list +stderr: + description: The set of error responses from the cluster command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + +NAME_EXISTS = None +NODE1_EXISTS = None +NODE2_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks for idempotency using the cluster-show command. + If a cluster with given name exists, return NAME_EXISTS as True else False. + If the given cluster-node-1 is already a part of another cluster, return + NODE1_EXISTS as True else False. + If the given cluster-node-2 is already a part of another cluster, return + NODE2_EXISTS as True else False. + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: NAME_EXISTS, NODE1_EXISTS, NODE2_EXISTS + """ + name = module.params['pn_name'] + node1 = module.params['pn_cluster_node1'] + node2 = module.params['pn_cluster_node2'] + + show = cli + ' cluster-show format name,cluster-node-1,cluster-node-2 ' + show = shlex.split(show) + out = module.run_command(show)[1] + + out = out.split() + # Global flags + global NAME_EXISTS, NODE1_EXISTS, NODE2_EXISTS + + if name in out: + NAME_EXISTS = True + else: + NAME_EXISTS = False + if node1 in out: + NODE1_EXISTS = True + else: + NODE2_EXISTS = False + if node2 in out: + NODE2_EXISTS = True + else: + NODE2_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'cluster-create' + if state == 'absent': + command = 'cluster-delete' + return command + + +def main(): + """ This section is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state =dict(required=True, type='str', + choices=['present', 'absent']), + pn_name=dict(required=True, type='str'), + pn_cluster_node1=dict(type='str'), + pn_cluster_node2=dict(type='str'), + pn_validate=dict(type='bool') + ), + required_if=( + ["state", "present", + ["pn_name", "pn_cluster_node1", "pn_cluster_node2"]], + ["state", "absent", ["pn_name"]] + ) + ) + + # Accessing the parameters + state = module.params['state'] + name = module.params['pn_name'] + cluster_node1 = module.params['pn_cluster_node1'] + cluster_node2 = module.params['pn_cluster_node2'] + validate = module.params['pn_validate'] + + command = get_command_from_state(state) + + # Building the CLI command string + cli = pn_cli(module) + + if command == 'cluster-create': + + check_cli(module, cli) + + if NAME_EXISTS is True: + module.exit_json( + skipped=True, + msg='Cluster with name %s already exists' % name + ) + if NODE1_EXISTS is True: + module.exit_json( + skipped=True, + msg='Node %s already part of a cluster' % cluster_node1 + ) + if NODE2_EXISTS is True: + module.exit_json( + skipped=True, + msg='Node %s already part of a cluster' % cluster_node2 + ) + + cli += ' %s name %s ' % (command, name) + cli += 'cluster-node-1 %s cluster-node-2 %s ' % (cluster_node1, + cluster_node2) + if validate is True: + cli += ' validate ' + if validate is False: + cli += ' no-validate ' + + if command == 'cluster-delete': + + check_cli(module, cli) + + if NAME_EXISTS is False: + module.exit_json( + skipped=True, + msg='Cluster with name %s does not exist' % name + ) + cli += ' %s name %s ' % (command, name) + + run_cli(module, cli) + +# AnsibleModule boilerplate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 725cc699e761f2e705b44ec558892001ed0097cc Mon Sep 17 00:00:00 2001 From: amitsi Date: Fri, 16 Sep 2016 07:04:29 -0700 Subject: [PATCH 342/770] pn_bgp (New Module) (#4738) * added Py2.4 and YAML Documentation fixes * added no_log for password * incorporated additional review comments * remove type for options block * fix type for pn_multiprotocol --- network/netvisor/pn_vrouterbgp.py | 470 ++++++++++++++++++++++++++++++ 1 file changed, 470 insertions(+) create mode 100644 network/netvisor/pn_vrouterbgp.py diff --git a/network/netvisor/pn_vrouterbgp.py b/network/netvisor/pn_vrouterbgp.py new file mode 100644 index 00000000000..1a535a221f5 --- /dev/null +++ b/network/netvisor/pn_vrouterbgp.py @@ -0,0 +1,470 @@ +#!/usr/bin/python +""" PN-CLI vrouter-bgp-add/vrouter-bgp-remove/vrouter-bgp-modify """ + +# +# 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 . +# + + +import shlex + +DOCUMENTATION = """ +--- +module: pn_vrouterbgp +author: "Pluribus Networks (@amitsi)" +version_added: "2.2" +version: 1.0 +short_description: CLI command to add/remove/modify vrouter-bgp. +description: + - Execute vrouter-bgp-add, vrouter-bgp-remove, vrouter-bgp-modify command. + - Each fabric, cluster, standalone switch, or virtual network (VNET) can + provide its tenants with a vRouter service that forwards traffic between + networks and implements Layer 4 protocols. +options: + pn_cliusername: + description: + - Provide login username if user is not root. + required: False + pn_clipassword: + description: + - Provide login password if user is not root. + required: False + pn_cliswitch: + description: + - Target switch(es) to run the cli on. + required: False + state: + description: + - State the action to perform. Use 'present' to add bgp, + 'absent' to remove bgp and 'update' to modify bgp. + required: True + choices: ['present', 'absent', 'update'] + pn_vrouter_name: + description: + - Specify a name for the vRouter service. + required: True + pn_neighbor: + description: + - Specify a neighbor IP address to use for BGP. + - Required for vrouter-bgp-add. + pn_remote_as: + description: + - Specify the remote Autonomous System(AS) number. This value is between + 1 and 4294967295. + - Required for vrouter-bgp-add. + pn_next_hop_self: + description: + - Specify if the next-hop is the same router or not. + pn_password: + description: + - Specify a password, if desired. + pn_ebgp: + description: + - Specify a value for external BGP to accept or attempt BGP connections + to external peers, not directly connected, on the network. This is a + value between 1 and 255. + pn_prefix_listin: + description: + - Specify the prefix list to filter traffic inbound. + pn_prefix_listout: + description: + - Specify the prefix list to filter traffic outbound. + pn_route_reflector: + description: + - Specify if a route reflector client is used. + pn_override_capability: + description: + - Specify if you want to override capability. + pn_soft_reconfig: + description: + - Specify if you want a soft reconfiguration of inbound traffic. + pn_max_prefix: + description: + - Specify the maximum number of prefixes. + pn_max_prefix_warn: + description: + - Specify if you want a warning message when the maximum number of + prefixes is exceeded. + pn_bfd: + description: + - Specify if you want BFD protocol support for fault detection. + pn_multiprotocol: + description: + - Specify a multi-protocol for BGP. + choices: ['ipv4-unicast', 'ipv6-unicast'] + pn_weight: + description: + - Specify a default weight value between 0 and 65535 for the neighbor + routes. + pn_default_originate: + description: + - Specify if you want announce default routes to the neighbor or not. + pn_keepalive: + description: + - Specify BGP neighbor keepalive interval in seconds. + pn_holdtime: + description: + - Specify BGP neighbor holdtime in seconds. + pn_route_mapin: + description: + - Specify inbound route map for neighbor. + pn_route_mapout: + description: + - Specify outbound route map for neighbor. +""" + +EXAMPLES = """ +- name: add vrouter-bgp + pn_vrouterbgp: + state: 'present' + pn_vrouter_name: 'ansible-vrouter' + pn_neighbor: 104.104.104.1 + pn_remote_as: 1800 + +- name: remove vrouter-bgp + pn_vrouterbgp: + state: 'absent' + pn_name: 'ansible-vrouter' +""" + +RETURN = """ +command: + description: The CLI command run on the target node(s). +stdout: + description: The set of responses from the vrouterbpg command. + returned: always + type: list +stderr: + description: The set of error responses from the vrouterbgp command. + returned: on error + type: list +changed: + description: Indicates whether the CLI caused changes on the target. + returned: always + type: bool +""" + + +VROUTER_EXISTS = None +NEIGHBOR_EXISTS = None + + +def pn_cli(module): + """ + This method is to generate the cli portion to launch the Netvisor cli. + It parses the username, password, switch parameters from module. + :param module: The Ansible module to fetch username, password and switch + :return: returns the cli string for further processing + """ + username = module.params['pn_cliusername'] + password = module.params['pn_clipassword'] + cliswitch = module.params['pn_cliswitch'] + + if username and password: + cli = '/usr/bin/cli --quiet --user %s:%s ' % (username, password) + else: + cli = '/usr/bin/cli --quiet ' + + if cliswitch == 'local': + cli += ' switch-local ' + else: + cli += ' switch ' + cliswitch + return cli + + +def check_cli(module, cli): + """ + This method checks if vRouter exists on the target node. + This method also checks for idempotency using the vrouter-bgp-show command. + If the given vRouter exists, return VROUTER_EXISTS as True else False. + If a BGP neighbor with the given ip exists on the given vRouter, + return NEIGHBOR_EXISTS as True else False. + + :param module: The Ansible module to fetch input parameters + :param cli: The CLI string + :return Global Booleans: VROUTER_EXISTS, NEIGHBOR_EXISTS + """ + vrouter_name = module.params['pn_vrouter_name'] + neighbor = module.params['pn_neighbor'] + # Global flags + global VROUTER_EXISTS, NEIGHBOR_EXISTS + + # Check for vRouter + check_vrouter = cli + ' vrouter-show format name no-show-headers ' + check_vrouter = shlex.split(check_vrouter) + out = module.run_command(check_vrouter)[1] + out = out.split() + + if vrouter_name in out: + VROUTER_EXISTS = True + else: + VROUTER_EXISTS = False + + # Check for BGP neighbors + show = cli + ' vrouter-bgp-show vrouter-name %s ' % vrouter_name + show += 'format neighbor no-show-headers' + show = shlex.split(show) + out = module.run_command(show)[1] + out = out.split() + + if neighbor in out: + NEIGHBOR_EXISTS = True + else: + NEIGHBOR_EXISTS = False + + +def run_cli(module, cli): + """ + This method executes the cli command on the target node(s) and returns the + output. The module then exits based on the output. + :param cli: the complete cli string to be executed on the target node(s). + :param module: The Ansible module to fetch command + """ + cliswitch = module.params['pn_cliswitch'] + state = module.params['state'] + command = get_command_from_state(state) + + cmd = shlex.split(cli) + + # 'out' contains the output + # 'err' contains the error messages + result, out, err = module.run_command(cmd) + + print_cli = cli.split(cliswitch)[1] + + # Response in JSON format + if result != 0: + module.exit_json( + command=print_cli, + stderr=err.strip(), + msg="%s operation failed" % command, + changed=False + ) + + if out: + module.exit_json( + command=print_cli, + stdout=out.strip(), + msg="%s operation completed" % command, + changed=True + ) + + else: + module.exit_json( + command=print_cli, + msg="%s operation completed" % command, + changed=True + ) + + +def get_command_from_state(state): + """ + This method gets appropriate command name for the state specified. It + returns the command name for the specified state. + :param state: The state for which the respective command name is required. + """ + command = None + if state == 'present': + command = 'vrouter-bgp-add' + if state == 'absent': + command = 'vrouter-bgp-remove' + if state == 'update': + command = 'vrouter-bgp-modify' + return command + + +def main(): + """ This portion is for arguments parsing """ + module = AnsibleModule( + argument_spec=dict( + pn_cliusername=dict(required=False, type='str'), + pn_clipassword=dict(required=False, type='str', no_log=True), + pn_cliswitch=dict(required=False, type='str', default='local'), + state=dict(required=True, type='str', + choices=['present', 'absent', 'update']), + pn_vrouter_name=dict(required=True, type='str'), + pn_neighbor=dict(type='str'), + pn_remote_as=dict(type='str'), + pn_next_hop_self=dict(type='bool'), + pn_password=dict(type='str', no_log=True), + pn_ebgp=dict(type='int'), + pn_prefix_listin=dict(type='str'), + pn_prefix_listout=dict(type='str'), + pn_route_reflector=dict(type='bool'), + pn_override_capability=dict(type='bool'), + pn_soft_reconfig=dict(type='bool'), + pn_max_prefix=dict(type='int'), + pn_max_prefix_warn=dict(type='bool'), + pn_bfd=dict(type='bool'), + pn_multiprotocol=dict(type='str', + choices=['ipv4-unicast', 'ipv6-unicast']), + pn_weight=dict(type='int'), + pn_default_originate=dict(type='bool'), + pn_keepalive=dict(type='str'), + pn_holdtime=dict(type='str'), + pn_route_mapin=dict(type='str'), + pn_route_mapout=dict(type='str') + ), + required_if=( + ["state", "present", + ["pn_vrouter_name", "pn_neighbor", "pn_remote_as"]], + ["state", "absent", + ["pn_vrouter_name", "pn_neighbor"]], + ["state", "update", + ["pn_vrouter_name", "pn_neighbor"]] + ) + ) + + # Accessing the arguments + state= module.params['state'] + vrouter_name = module.params['pn_vrouter_name'] + neighbor = module.params['pn_neighbor'] + remote_as = module.params['pn_remote_as'] + next_hop_self = module.params['pn_next_hop_self'] + password = module.params['pn_password'] + ebgp = module.params['pn_ebgp'] + prefix_listin = module.params['pn_prefix_listin'] + prefix_listout = module.params['pn_prefix_listout'] + route_reflector = module.params['pn_route_reflector'] + override_capability = module.params['pn_override_capability'] + soft_reconfig = module.params['pn_soft_reconfig'] + max_prefix = module.params['pn_max_prefix'] + max_prefix_warn = module.params['pn_max_prefix_warn'] + bfd = module.params['pn_bfd'] + multiprotocol = module.params['pn_multiprotocol'] + weight = module.params['pn_weight'] + default_originate = module.params['pn_default_originate'] + keepalive = module.params['pn_keepalive'] + holdtime = module.params['pn_holdtime'] + route_mapin = module.params['pn_route_mapin'] + route_mapout = module.params['pn_route_mapout'] + + # Building the CLI command string + cli = pn_cli(module) + + command = get_command_from_state(state) + if command == 'vrouter-bgp-remove': + check_cli(module, cli) + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + if NEIGHBOR_EXISTS is False: + module.exit_json( + skipped=True, + msg=('BGP neighbor with IP %s does not exist on %s' + % (neighbor, vrouter_name)) + ) + cli += (' %s vrouter-name %s neighbor %s ' + % (command, vrouter_name, neighbor)) + + else: + + if command == 'vrouter-bgp-add': + check_cli(module, cli) + if VROUTER_EXISTS is False: + module.exit_json( + skipped=True, + msg='vRouter %s does not exist' % vrouter_name + ) + if NEIGHBOR_EXISTS is True: + module.exit_json( + skipped=True, + msg=('BGP neighbor with IP %s already exists on %s' + % (neighbor, vrouter_name)) + ) + + cli += (' %s vrouter-name %s neighbor %s ' + % (command, vrouter_name, neighbor)) + + if remote_as: + cli += ' remote-as ' + str(remote_as) + + if next_hop_self is True: + cli += ' next-hop-self ' + if next_hop_self is False: + cli += ' no-next-hop-self ' + + if password: + cli += ' password ' + password + + if ebgp: + cli += ' ebgp-multihop ' + str(ebgp) + + if prefix_listin: + cli += ' prefix-list-in ' + prefix_listin + + if prefix_listout: + cli += ' prefix-list-out ' + prefix_listout + + if route_reflector is True: + cli += ' route-reflector-client ' + if route_reflector is False: + cli += ' no-route-reflector-client ' + + if override_capability is True: + cli += ' override-capability ' + if override_capability is False: + cli += ' no-override-capability ' + + if soft_reconfig is True: + cli += ' soft-reconfig-inbound ' + if soft_reconfig is False: + cli += ' no-soft-reconfig-inbound ' + + if max_prefix: + cli += ' max-prefix ' + str(max_prefix) + + if max_prefix_warn is True: + cli += ' max-prefix-warn-only ' + if max_prefix_warn is False: + cli += ' no-max-prefix-warn-only ' + + if bfd is True: + cli += ' bfd ' + if bfd is False: + cli += ' no-bfd ' + + if multiprotocol: + cli += ' multi-protocol ' + multiprotocol + + if weight: + cli += ' weight ' + str(weight) + + if default_originate is True: + cli += ' default-originate ' + if default_originate is False: + cli += ' no-default-originate ' + + if keepalive: + cli += ' neighbor-keepalive-interval ' + keepalive + + if holdtime: + cli += ' neighbor-holdtime ' + holdtime + + if route_mapin: + cli += ' route-map-in ' + route_mapin + + if route_mapout: + cli += ' route-map-out ' + route_mapout + + run_cli(module, cli) +# Ansible boiler-plate +from ansible.module_utils.basic import AnsibleModule + +if __name__ == '__main__': + main() From 2e1e3562b92cdbfc72bc804ba632f0d48f6a71f6 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 16 Sep 2016 10:59:31 -0400 Subject: [PATCH 343/770] Stop sorting of termination_policies in `ec2_asg` (#4883) The AWS API requires that any termination policy list that includes `Default` must end with Default. The attribute sorting caused any list of attributes to be lexically sorted, so a list like `["OldestLaunchConfiguration", "Default"]` would be changed to `["Default", "OldestLaunchConfiguration"]` because default is earlier alphabetically. This caused calls to fail with BotoServerError per #4069 This commit also adds proper tracebacks to all botoservererror fail_json calls. Closes #4069 --- cloud/amazon/ec2_asg.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 66261fdbbd7..614e3144c8c 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -203,6 +203,7 @@ import time import logging as log +import traceback from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * @@ -441,7 +442,7 @@ def create_autoscaling_group(connection, module): changed = True return(changed, asg_properties) except BotoServerError as e: - module.fail_json(msg=str(e)) + module.fail_json(msg="Failed to create Autoscaling Group: %s" % str(e), exception=traceback.format_exc(e)) else: as_group = as_groups[0] changed = False @@ -453,14 +454,15 @@ def create_autoscaling_group(connection, module): group_attr = getattr(as_group, attr) # we do this because AWS and the module may return the same list # sorted differently - try: - module_attr.sort() - except: - pass - try: - group_attr.sort() - except: - pass + if attr != 'termination_policies': + try: + module_attr.sort() + except: + pass + try: + group_attr.sort() + except: + pass if group_attr != module_attr: changed = True setattr(as_group, attr, module_attr) @@ -496,7 +498,7 @@ def create_autoscaling_group(connection, module): try: as_group.update() except BotoServerError as e: - module.fail_json(msg=str(e)) + module.fail_json(msg="Failed to update Autoscaling Group: %s" % str(e), exception=traceback.format_exc(e)) if wait_for_instances: wait_for_new_inst(module, connection, group_name, wait_timeout, desired_capacity, 'viable_instances') @@ -505,7 +507,7 @@ def create_autoscaling_group(connection, module): as_group = connection.get_all_groups(names=[group_name])[0] asg_properties = get_properties(as_group) except BotoServerError as e: - module.fail_json(msg=str(e)) + module.fail_json(msg="Failed to read existing Autoscaling Groups: %s" % str(e), exception=traceback.format_exc(e)) return(changed, asg_properties) From 93c70ee1e2a38045a5a4be4dbecc0c8b90033969 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Fri, 16 Sep 2016 11:06:06 -0700 Subject: [PATCH 344/770] Added support for facts module for Dell Networking OS10 device. (#4879) * Added support for dnos10_facts module * Added the missing quotes * Addressed @privateip comments --- network/dnos10/dnos10_facts.py | 447 +++++++++++++++++++++++++++++++++ 1 file changed, 447 insertions(+) create mode 100644 network/dnos10/dnos10_facts.py diff --git a/network/dnos10/dnos10_facts.py b/network/dnos10/dnos10_facts.py new file mode 100644 index 00000000000..2ab0dc9d400 --- /dev/null +++ b/network/dnos10/dnos10_facts.py @@ -0,0 +1,447 @@ +#!/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: dnos10_facts +version_added: "2.2" +author: "Senthil Kumar Ganesan (@skg_net)" +short_description: Collect facts from remote devices running Dell OS10 +description: + - Collects a base set of device facts from a remote device that + is running Dell OS10. This module prepends all of the + base network fact keys with C(ansible_net_). The facts + module will always collect a base set of facts from the device + and can enable or disable collection of additional facts. +extends_documentation_fragment: dnos10 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument inlcude + all, hardware, config, and interfaces. Can specify a list of + values to include a larger subset. Values can also be used + with an initial M(!) to specify that a specific subset should + not be collected. + required: false + default: '!config' +""" + +EXAMPLES = """ +# Collect all facts from the device +- dnos10_facts: + gather_subset: all + +# Collect only the config and default facts +- dnos10_facts: + gather_subset: + - config + +# Do not collect hardware facts +- dnos10_facts: + gather_subset: + - "!hardware" +""" + +RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +# default +ansible_net_name: + description: The name of the OS which is running + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_servicetag: + description: The service tag number of the remote device + returned: always + type: str +ansible_net_model: + description: The model name returned from the device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: str + +# hardware +ansible_net_cpu_arch: + description: Cpu Architecture of the remote device + returned: when hardware is configured + type: str +ansible_net_memfree_mb: + description: The available free memory on the remote device in Mb + returned: when hardware is configured + type: int +ansible_net_memtotal_mb: + description: The total memory on the remote device in Mb + returned: when hardware is configured + type: int + +# config +ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str + +# interfaces +ansible_net_all_ipv4_addresses: + description: All IPv4 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_all_ipv6_addresses: + description: All IPv6 addresses configured on the device + returned: when interfaces is configured + type: list +ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +ansible_net_neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict +""" + +import re +import itertools + +from ansible.module_utils.basic import get_exception +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.dnos10 + +try: + from lxml import etree as ET +except ImportError: + import xml.etree.ElementTree as ET + +class FactsBase(object): + + def __init__(self, runner): + self.runner = runner + self.facts = dict() + + self.commands() + + +class Default(FactsBase): + + def commands(self): + self.runner.add_command('show version | display-xml') + self.runner.add_command('show system | display-xml') + self.runner.add_command('show running-configuration | grep hostname') + + def populate(self): + + data = self.runner.get_command('show version | display-xml') + xml_data = ET.fromstring(data) + + self.facts['name'] = self.parse_name(xml_data) + self.facts['version'] = self.parse_version(xml_data) + + data = self.runner.get_command('show system | display-xml') + xml_data = ET.fromstring(data) + + self.facts['servicetag'] = self.parse_serialnum(xml_data) + self.facts['model'] = self.parse_model(xml_data) + + data = self.runner.get_command('show running-configuration | grep hostname') + self.facts['hostname'] = self.parse_hostname(data) + + def parse_name(self, data): + sw_name = data.find('./data/system-sw-state/sw-version/sw-name') + if sw_name is not None: + return sw_name.text + else: + return "" + + def parse_version(self, data): + sw_ver = data.find('./data/system-sw-state/sw-version/sw-version') + if sw_ver is not None: + return sw_ver.text + else: + return "" + + def parse_hostname(self, data): + match = re.search(r'hostname\s+(\S+)', data, re.M) + if match: + return match.group(1) + + def parse_model(self, data): + prod_name = data.find('./data/system/node/mfg-info/product-name') + if prod_name is not None: + return prod_name.text + else: + return "" + + def parse_serialnum(self, data): + svc_tag = data.find('./data/system/node/unit/mfg-info/service-tag') + if svc_tag is not None: + return svc_tag.text + else: + return "" + + +class Hardware(FactsBase): + + def commands(self): + self.runner.add_command('show processes memory | grep Total') + + def populate(self): + + data = self.runner.get_command('show version | display-xml') + xml_data = ET.fromstring(data) + + self.facts['cpu_arch'] = self.parse_cpu_arch(xml_data) + + data = self.runner.get_command('show processes memory | grep Total') + + match = self.parse_memory(data) + if match: + self.facts['memtotal_mb'] = int(match[0]) / 1024 + self.facts['memfree_mb'] = int(match[2]) / 1024 + + def parse_cpu_arch(self, data): + cpu_arch = data.find('./data/system-sw-state/sw-version/cpu-arch') + if cpu_arch is not None: + return cpu_arch.text + else: + return "" + + def parse_memory(self, data): + return re.findall(r'\:\s*(\d+)', data, re.M) + + +class Config(FactsBase): + + def commands(self): + self.runner.add_command('show running-config') + + def populate(self): + config = self.runner.get_command('show running-config') + self.facts['config'] = config + + +class Interfaces(FactsBase): + + def commands(self): + self.runner.add_command('show interface | display-xml') + + def populate(self): + self.facts['all_ipv4_addresses'] = list() + self.facts['all_ipv6_addresses'] = list() + + data = self.runner.get_command('show interface | display-xml') + + xml_data = ET.fromstring(data) + + self.facts['interfaces'] = self.populate_interfaces(xml_data) + self.facts['neighbors'] = self.populate_neighbors(xml_data) + + def populate_interfaces(self, interfaces): + int_facts = dict() + + for interface in interfaces.findall('./data/interfaces/interface'): + intf = dict() + name = self.parse_item(interface, 'name') + + intf['description'] = self.parse_item(interface, 'description') + intf['duplex'] = self.parse_item(interface, 'duplex') + intf['primary_ipv4'] = self.parse_primary_ipv4(interface) + intf['secondary_ipv4'] = self.parse_secondary_ipv4(interface) + intf['ipv6'] = self.parse_ipv6_address(interface) + intf['mtu'] = self.parse_item(interface, 'mtu') + intf['type'] = self.parse_item(interface, 'type') + + int_facts[name] = intf + + for interface in interfaces.findall('./data/interfaces-state/interface'): + name = self.parse_item(interface, 'name') + intf = int_facts[name] + intf['bandwidth'] = self.parse_item(interface, 'speed') + intf['adminstatus'] = self.parse_item(interface, 'admin-status') + intf['operstatus'] = self.parse_item(interface, 'oper-status') + intf['macaddress'] = self.parse_item(interface, 'phys-address') + + for interface in interfaces.findall('./data/ports/ports-state/port'): + name = self.parse_item(interface, 'name') + fanout = self.parse_item(interface, 'fanout-state') + mediatype = self.parse_item(interface, 'media-type') + + typ, sname = name.split('-eth') + + if fanout == "BREAKOUT_1x1": + name = "ethernet" + sname + intf = int_facts[name] + intf['mediatype'] = mediatype + else: + #TODO: Loop for the exact subport + for subport in xrange(1, 5): + name = "ethernet" + sname + ":" + str(subport) + intf = int_facts[name] + intf['mediatype'] = mediatype + + return int_facts + + def add_ip_address(self, address, family): + if family == 'ipv4': + self.facts['all_ipv4_addresses'].append(address) + else: + self.facts['all_ipv6_addresses'].append(address) + + def parse_item(self, interface, item): + elem = interface.find(item) + if elem is not None: + return elem.text + else: + return "" + + def parse_primary_ipv4(self, interface): + ipv4 = interface.find('ipv4') + ip_address = "" + if ipv4 is not None: + prim_ipaddr = ipv4.find('./address/primary-addr') + if prim_ipaddr is not None: + ip_address = prim_ipaddr.text + self.add_ip_address(ip_address, 'ipv4') + + return ip_address + + def parse_secondary_ipv4(self, interface): + ipv4 = interface.find('ipv4') + ip_address = "" + if ipv4 is not None: + sec_ipaddr = ipv4.find('./address/secondary-addr') + if sec_ipaddr is not None: + ip_address = sec_ipaddr.text + self.add_ip_address(ip_address, 'ipv4') + + return ip_address + + def parse_ipv6_address(self, interface): + ipv6 = interface.find('ipv6') + ip_address = "" + if ipv6 is not None: + ipv6_addr = ipv6.find('./address/ipv6-address') + if ipv6_addr is not None: + ip_address = ipv6_addr.text + self.add_ip_address(ip_address, 'ipv6') + + return ip_address + + def populate_neighbors(self, interfaces): + lldp_facts = dict() + for interface in interfaces.findall('./data/interfaces-state/interface'): + name = interface.find('name').text + rem_sys_name = interface.find('./lldp-rem-neighbor-info/info/rem-system-name') + if rem_sys_name is not None: + lldp_facts[name] = list() + fact = dict() + fact['host'] = rem_sys_name.text + rem_sys_port = interface.find('./lldp-rem-neighbor-info/info/rem-lldp-port-id') + fact['port'] = rem_sys_port.text + lldp_facts[name].append(fact) + + return lldp_facts + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + + +def main(): + spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + + module = NetworkModule(argument_spec=spec, supports_check_mode=True) + + gather_subset = module.params['gather_subset'] + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Bad subset') + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + + facts = dict() + facts['gather_subset'] = list(runable_subsets) + + runner = CommandRunner(module) + + instances = list() + for key in runable_subsets: + runs = FACT_SUBSETS[key](runner) + instances.append(runs) + + runner.run() + + try: + for inst in instances: + inst.populate() + facts.update(inst.facts) + except Exception: + module.exit_json(out=module.from_json(runner.items)) + + ansible_facts = dict() + for key, value in facts.iteritems(): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts) + + +if __name__ == '__main__': + main() From a29fb59a7224385e70239ffc007bb69beabfe268 Mon Sep 17 00:00:00 2001 From: Shawn Siefkas Date: Thu, 30 Jul 2015 09:52:32 -0500 Subject: [PATCH 345/770] Adding SNS notification support to ec2_asg module Addresses #1844 --- cloud/amazon/ec2_asg.py | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 614e3144c8c..d5476e7b0cc 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -130,6 +130,18 @@ default: Default choices: ['OldestInstance', 'NewestInstance', 'OldestLaunchConfiguration', 'ClosestToNextInstanceHour', 'Default'] version_added: "2.0" + notification_topic: + description: + - A SNS topic ARN to send auto scaling notifications to. + default: None + required: false + version_added: "2.0" + notification_types: + description: + - A list of auto scaling events to trigger notifications on. + default: ['autoscaling:EC2_INSTANCE_LAUNCH', 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', 'autoscaling:EC2_INSTANCE_TERMINATE', 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'] + required: false + version_added: "2.0" extends_documentation_fragment: - aws - ec2 @@ -392,6 +404,8 @@ def create_autoscaling_group(connection, module): as_groups = connection.get_all_groups(names=[group_name]) wait_timeout = module.params.get('wait_timeout') termination_policies = module.params.get('termination_policies') + notification_topic = module.params.get('notification_topic') + notification_types = module.params.get('notification_types') if not vpc_zone_identifier and not availability_zones: region, ec2_url, aws_connect_params = get_aws_connection_info(module) @@ -437,6 +451,10 @@ def create_autoscaling_group(connection, module): if wait_for_instances: wait_for_new_inst(module, connection, group_name, wait_timeout, desired_capacity, 'viable_instances') wait_for_elb(connection, module, group_name) + + if notification_topic: + ag.put_notification_configuration(notification_topic, notification_types) + as_group = connection.get_all_groups(names=[group_name])[0] asg_properties = get_properties(as_group) changed = True @@ -500,6 +518,12 @@ def create_autoscaling_group(connection, module): except BotoServerError as e: module.fail_json(msg="Failed to update Autoscaling Group: %s" % str(e), exception=traceback.format_exc(e)) + if notification_topic: + try: + as_group.put_notification_configuration(notification_topic, notification_types) + except BotoServerError as e: + module.fail_json(msg="Failed to update Autoscaling Group notifications: %s" % str(e), exception=traceback.format_exc(e)) + if wait_for_instances: wait_for_new_inst(module, connection, group_name, wait_timeout, desired_capacity, 'viable_instances') wait_for_elb(connection, module, group_name) @@ -513,6 +537,11 @@ def create_autoscaling_group(connection, module): def delete_autoscaling_group(connection, module): group_name = module.params.get('name') + notification_topic = module.params.get('notification_topic') + + if notification_topic: + ag.delete_notification_configuration(notification_topic) + groups = connection.get_all_groups(names=[group_name]) if groups: group = groups[0] @@ -800,7 +829,14 @@ def main(): health_check_type=dict(default='EC2', choices=['EC2', 'ELB']), default_cooldown=dict(type='int', default=300), wait_for_instances=dict(type='bool', default=True), - termination_policies=dict(type='list', default='Default') + termination_policies=dict(type='list', default='Default'), + notification_topic=dict(type='str', default=None), + notification_types=dict(type='list', default=[ + 'autoscaling:EC2_INSTANCE_LAUNCH', + 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', + 'autoscaling:EC2_INSTANCE_TERMINATE', + 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' + ]) ), ) From a435dbbb2d6f0226ab3ae184f8480546226a0ba2 Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Fri, 16 Sep 2016 16:17:42 -0400 Subject: [PATCH 346/770] Fix version_added for ec2_asg feature --- cloud/amazon/ec2_asg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index d5476e7b0cc..cc52e3de1fc 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -135,13 +135,13 @@ - A SNS topic ARN to send auto scaling notifications to. default: None required: false - version_added: "2.0" + version_added: "2.2" notification_types: description: - A list of auto scaling events to trigger notifications on. default: ['autoscaling:EC2_INSTANCE_LAUNCH', 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', 'autoscaling:EC2_INSTANCE_TERMINATE', 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'] required: false - version_added: "2.0" + version_added: "2.2" extends_documentation_fragment: - aws - ec2 From 922aed148db29eeff15425a36e2904c67641d2e9 Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Fri, 16 Sep 2016 14:10:26 -0700 Subject: [PATCH 347/770] Support for GCE Managed Instance Groups. (#4541) Create, Delete, Resize and Recreate (of instances) supported. Autoscalers are also supported. --- cloud/google/gce_mig.py | 763 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 763 insertions(+) create mode 100644 cloud/google/gce_mig.py diff --git a/cloud/google/gce_mig.py b/cloud/google/gce_mig.py new file mode 100644 index 00000000000..7762eaed23f --- /dev/null +++ b/cloud/google/gce_mig.py @@ -0,0 +1,763 @@ +#!/usr/bin/python +# Copyright 2016 Google Inc. +# +# 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: gce_mig +version_added: "2.2" +short_description: Create, Update or Destroy a Managed Instance Group (MIG). +description: + - Create, Update or Destroy a Managed Instance Group (MIG). See + U(https://cloud.google.com/compute/docs/instance-groups) for an overview. + Full install/configuration instructions for the gce* modules can + be found in the comments of ansible/test/gce_tests.py. +requirements: + - "python >= 2.6" + - "apache-libcloud >= 1.2.0" +notes: + - Resizing and Recreating VM are also supported. + - An existing instance template is required in order to create a + Managed Instance Group. +author: + - "Tom Melendez (@supertom) " +options: + name: + description: + - Name of the Managed Instance Group. + required: true + template: + description: + - Instance Template to be used in creating the VMs. See + U(https://cloud.google.com/compute/docs/instance-templates) to learn more + about Instance Templates. Required for creating MIGs. + required: false + size: + description: + - Size of Managed Instance Group. If MIG already exists, it will be + resized to the number provided here. Required for creating MIGs. + required: false + service_account_email: + description: + - service account email + required: false + default: null + credentials_file: + description: + - Path to the JSON file associated with the service account email + default: null + required: false + project_id: + description: + - GCE project ID + required: false + default: null + state: + description: + - desired state of the resource + required: false + default: "present" + choices: ["absent", "present"] + zone: + description: + - The GCE zone to use for this Managed Instance Group. + required: true + autoscaling: + description: + - A dictionary of configuration for the autoscaler. 'enabled (bool)', 'name (str)' + and policy.max_instances (int) are required fields if autoscaling is used. See + U(https://cloud.google.com/compute/docs/reference/beta/autoscalers) for more information + on Autoscaling. + required: false + default: null +''' + +EXAMPLES = ''' +# Following playbook creates, rebuilds instances, resizes and then deletes a MIG. +# Notes: +# - Two valid Instance Templates must exist in your GCE project in order to run +# this playbook. Change the fields to match the templates used in your +# project. +# - The use of the 'pause' module is not required, it is just for convenience. +- name: Managed Instance Group Example + hosts: localhost + gather_facts: False + tasks: + - name: Create MIG + gce_mig: + name: ansible-mig-example + zone: us-central1-c + state: present + size: 1 + template: my-instance-template-1 + - pause: seconds=30 + - name: Recreate MIG Instances with Instance Template change. + gce_mig: + name: ansible-mig-example + zone: us-central1-c + state: present + template: my-instance-template-2-small + recreate_instances: yes + - pause: seconds=30 + - name: Resize MIG + gce_mig: + name: ansible-mig-example + zone: us-central1-c + state: present + size: 3 + - name: Update MIG with Autoscaler + gce_mig: + name: ansible-mig-example + zone: us-central1-c + state: present + size: 3 + template: my-instance-template-2-small + recreate_instances: yes + autoscaling: + enabled: yes + name: my-autoscaler + policy: + min_instances: 2 + max_instances: 5 + cool_down_period: 37 + cpu_utilization: + target: .39 + load_balancing_utilization: + target: 0.4 + - pause: seconds=30 + - name: Delete MIG + gce_mig: + name: ansible-mig-example + zone: us-central1-c + state: absent + autoscaling: + enabled: no + name: my-autoscaler +''' +RETURN = ''' +zone: + description: Zone in which to launch MIG. + returned: always + type: string + sample: "us-central1-b" + +template: + description: Instance Template to use for VMs. Must exist prior to using with MIG. + returned: changed + type: string + sample: "my-instance-template" + +name: + description: Name of the Managed Instance Group. + returned: changed + type: string + sample: "my-managed-instance-group" + +size: + description: Number of VMs in Managed Instance Group. + returned: changed + type: integer + sample: 4 + +created_instances: + description: Names of instances created. + returned: When instances are created. + type: list + sample: ["ansible-mig-new-0k4y", "ansible-mig-new-0zk5", "ansible-mig-new-kp68"] + +deleted_instances: + description: Names of instances deleted. + returned: When instances are deleted. + type: list + sample: ["ansible-mig-new-0k4y", "ansible-mig-new-0zk5", "ansible-mig-new-kp68"] + +resize_created_instances: + description: Names of instances created during resizing. + returned: When a resize results in the creation of instances. + type: list + sample: ["ansible-mig-new-0k4y", "ansible-mig-new-0zk5", "ansible-mig-new-kp68"] + +resize_deleted_instances: + description: Names of instances deleted during resizing. + returned: When a resize results in the deletion of instances. + type: list + sample: ["ansible-mig-new-0k4y", "ansible-mig-new-0zk5", "ansible-mig-new-kp68"] + +recreated_instances: + description: Names of instances recreated. + returned: When instances are recreated. + type: list + sample: ["ansible-mig-new-0k4y", "ansible-mig-new-0zk5", "ansible-mig-new-kp68"] + +created_autoscaler: + description: True if Autoscaler was attempted and created. False otherwise. + returned: When the creation of an Autoscaler was attempted. + type: bool + sample: true + +updated_autoscaler: + description: True if an Autoscaler update was attempted and succeeded. + False returned if update failed. + returned: When the update of an Autoscaler was attempted. + type: bool + sample: true + +deleted_autoscaler: + description: True if an Autoscaler delete attempted and succeeded. + False returned if delete failed. + returned: When the delete of an Autoscaler was attempted. + type: bool + sample: true +''' + +import socket + +try: + import libcloud + from libcloud.compute.types import Provider + from libcloud.compute.providers import get_driver + from libcloud.common.google import GoogleBaseError, QuotaExceededError, \ + ResourceExistsError, ResourceInUseError, ResourceNotFoundError + from libcloud.compute.drivers.gce import GCEAddress + _ = Provider.GCE + HAS_LIBCLOUD = True +except ImportError: + HAS_LIBCLOUD = False + +try: + from ast import literal_eval + HAS_PYTHON26 = True +except ImportError: + HAS_PYTHON26 = False + + +def _check_params(params, field_list): + """ + Helper to validate params. + + Use this in function definitions if they require specific fields + to be present. + + :param params: structure that contains the fields + :type params: ``dict`` + + :param field_list: list of dict representing the fields + [{'name': str, 'required': True/False', 'type': cls}] + :type field_list: ``list`` of ``dict`` + + :return True, exits otherwise + :rtype: ``bool`` + """ + for d in field_list: + if not d['name'] in params: + if d['required'] is True: + return (False, "%s is required and must be of type: %s" % + (d['name'], str(d['type']))) + else: + if not isinstance(params[d['name']], d['type']): + return (False, + "%s must be of type: %s" % (d['name'], str(d['type']))) + + return (True, '') + + +def _validate_autoscaling_params(params): + """ + Validate that the minimum configuration is present for autoscaling. + + :param params: Ansible dictionary containing autoscaling configuration + It is expected that autoscaling config will be found at the + key 'autoscaling'. + :type params: ``dict`` + + :return: Tuple containing a boolean and a string. True if autoscaler + is valid, False otherwise, plus str for message. + :rtype: ``(``bool``, ``str``)`` + """ + if not params['autoscaling']: + # It's optional, so if not set at all, it's valid. + return (True, '') + if not isinstance(params['autoscaling'], dict): + return (False, + 'autoscaling: configuration expected to be a dictionary.') + + # check first-level required fields + as_req_fields = [ + {'name': 'name', 'required': True, 'type': str}, + {'name': 'enabled', 'required': True, 'type': bool}, + {'name': 'policy', 'required': True, 'type': dict} + ] # yapf: disable + + (as_req_valid, as_req_msg) = _check_params(params['autoscaling'], + as_req_fields) + if not as_req_valid: + return (False, as_req_msg) + + # check policy configuration + as_policy_fields = [ + {'name': 'max_instances', 'required': True, 'type': int}, + {'name': 'min_instances', 'required': False, 'type': int}, + {'name': 'cool_down_period', 'required': False, 'type': int} + ] # yapf: disable + + (as_policy_valid, as_policy_msg) = _check_params( + params['autoscaling']['policy'], as_policy_fields) + if not as_policy_valid: + return (False, as_policy_msg) + + # TODO(supertom): check utilization fields + + return (True, '') + + +def _get_instance_list(mig, field='name', filter_list=['NONE']): + """ + Helper to grab field from instances response. + + :param mig: Managed Instance Group Object from libcloud. + :type mig: :class: `GCEInstanceGroupManager` + + :param field: Field name in list_managed_instances response. Defaults + to 'name'. + :type field: ``str`` + + :param filter_list: list of 'currentAction' strings to filter on. Only + items that match a currentAction in this list will + be returned. Default is "['NONE']". + :type filter_list: ``list`` of ``str`` + + :return: List of strings from list_managed_instances response. + :rtype: ``list`` + """ + return [x[field] for x in mig.list_managed_instances() + if x['currentAction'] in filter_list] + + +def _gen_gce_as_policy(as_params): + """ + Take Autoscaler params and generate GCE-compatible policy. + + :param as_params: Dictionary in Ansible-playbook format + containing policy arguments. + :type as_params: ``dict`` + + :return: GCE-compatible policy dictionary + :rtype: ``dict`` + """ + asp_data = {} + asp_data['maxNumReplicas'] = as_params['max_instances'] + if 'min_instances' in as_params: + asp_data['minNumReplicas'] = as_params['min_instances'] + if 'cool_down_period' in as_params: + asp_data['coolDownPeriodSec'] = as_params['cool_down_period'] + if 'cpu_utilization' in as_params and 'target' in as_params[ + 'cpu_utilization']: + asp_data['cpuUtilization'] = {'utilizationTarget': + as_params['cpu_utilization']['target']} + if 'load_balancing_utilization' in as_params and 'target' in as_params[ + 'load_balancing_utilization']: + asp_data['loadBalancingUtilization'] = { + 'utilizationTarget': + as_params['load_balancing_utilization']['target'] + } + + return asp_data + + +def create_autoscaler(gce, mig, params): + """ + Create a new Autoscaler for a MIG. + + :param gce: An initialized GCE driver object. + :type gce: :class: `GCENodeDriver` + + :param mig: An initialized GCEInstanceGroupManager. + :type mig: :class: `GCEInstanceGroupManager` + + :param params: Dictionary of autoscaling parameters. + :type params: ``dict`` + + :return: Tuple with changed stats. + :rtype: tuple in the format of (bool, list) + """ + changed = False + as_policy = _gen_gce_as_policy(params['policy']) + autoscaler = gce.ex_create_autoscaler(name=params['name'], zone=mig.zone, + instance_group=mig, policy=as_policy) + if autoscaler: + changed = True + return changed + + +def update_autoscaler(gce, autoscaler, params): + """ + Update an Autoscaler. + + Takes an existing Autoscaler object, and updates it with + the supplied params before calling libcloud's update method. + + :param gce: An initialized GCE driver object. + :type gce: :class: `GCENodeDriver` + + :param autoscaler: An initialized GCEAutoscaler. + :type autoscaler: :class: `GCEAutoscaler` + + :param params: Dictionary of autoscaling parameters. + :type params: ``dict`` + + :return: True if changes, False otherwise. + :rtype: ``bool`` + """ + as_policy = _gen_gce_as_policy(params['policy']) + if autoscaler.policy != as_policy: + autoscaler.policy = as_policy + autoscaler = gce.ex_update_autoscaler(autoscaler) + if autoscaler: + return True + return False + + +def delete_autoscaler(autoscaler): + """ + Delete an Autoscaler. Does not affect MIG. + + :param mig: Managed Instance Group Object from Libcloud. + :type mig: :class: `GCEInstanceGroupManager` + + :return: Tuple with changed stats and a list of affected instances. + :rtype: tuple in the format of (bool, list) + """ + changed = False + if autoscaler.destroy(): + changed = True + return changed + + +def get_autoscaler(gce, name, zone): + """ + Get an Autoscaler from GCE. + + If the Autoscaler is not found, None is found. + + :param gce: An initialized GCE driver object. + :type gce: :class: `GCENodeDriver` + + :param name: Name of the Autoscaler. + :type name: ``str`` + + :param zone: Zone that the Autoscaler is located in. + :type zone: ``str`` + + :return: A GCEAutoscaler object or None. + :rtype: :class: `GCEAutoscaler` or None + """ + try: + # Does the Autoscaler already exist? + return gce.ex_get_autoscaler(name, zone) + + except ResourceNotFoundError: + return None + + +def create_mig(gce, params): + """ + Create a new Managed Instance Group. + + :param gce: An initialized GCE driver object. + :type gce: :class: `GCENodeDriver` + + :param params: Dictionary of parameters needed by the module. + :type params: ``dict`` + + :return: Tuple with changed stats and a list of affected instances. + :rtype: tuple in the format of (bool, list) + """ + + changed = False + return_data = [] + actions_filter = ['CREATING'] + + mig = gce.ex_create_instancegroupmanager( + name=params['name'], size=params['size'], template=params['template'], + zone=params['zone']) + + if mig: + changed = True + return_data = _get_instance_list(mig, filter_list=actions_filter) + + return (changed, return_data) + + +def delete_mig(mig): + """ + Delete a Managed Instance Group. All VMs in that MIG are also deleted." + + :param mig: Managed Instance Group Object from Libcloud. + :type mig: :class: `GCEInstanceGroupManager` + + :return: Tuple with changed stats and a list of affected instances. + :rtype: tuple in the format of (bool, list) + """ + changed = False + return_data = [] + actions_filter = ['NONE', 'CREATING', 'RECREATING', 'DELETING', + 'ABANDONING', 'RESTARTING', 'REFRESHING'] + instance_names = _get_instance_list(mig, filter_list=actions_filter) + if mig.destroy(): + changed = True + return_data = instance_names + + return (changed, return_data) + + +def recreate_instances_in_mig(mig): + """ + Recreate the instances for a Managed Instance Group. + + :param mig: Managed Instance Group Object from libcloud. + :type mig: :class: `GCEInstanceGroupManager` + + :return: Tuple with changed stats and a list of affected instances. + :rtype: tuple in the format of (bool, list) + """ + changed = False + return_data = [] + actions_filter = ['RECREATING'] + + if mig.recreate_instances(): + changed = True + return_data = _get_instance_list(mig, filter_list=actions_filter) + + return (changed, return_data) + + +def resize_mig(mig, size): + """ + Resize a Managed Instance Group. + + Based on the size provided, GCE will automatically create and delete + VMs as needed. + + :param mig: Managed Instance Group Object from libcloud. + :type mig: :class: `GCEInstanceGroupManager` + + :return: Tuple with changed stats and a list of affected instances. + :rtype: tuple in the format of (bool, list) + """ + changed = False + return_data = [] + actions_filter = ['CREATING', 'DELETING'] + + if mig.resize(size): + changed = True + return_data = _get_instance_list(mig, filter_list=actions_filter) + + return (changed, return_data) + + +def get_mig(gce, name, zone): + """ + Get a Managed Instance Group from GCE. + + If the MIG is not found, None is found. + + :param gce: An initialized GCE driver object. + :type gce: :class: `GCENodeDriver` + + :param name: Name of the Managed Instance Group. + :type name: ``str`` + + :param zone: Zone that the Managed Instance Group is located in. + :type zone: ``str`` + + :return: A GCEInstanceGroupManager object or None. + :rtype: :class: `GCEInstanceGroupManager` or None + """ + try: + # Does the MIG already exist? + return gce.ex_get_instancegroupmanager(name=name, zone=zone) + + except ResourceNotFoundError: + return None + + +def main(): + + module = AnsibleModule(argument_spec=dict( + name=dict(required=True), + template=dict(), + recreate_instances=dict(type='bool', default=False), + # Do not set a default size here. For Create and some update + # operations, it is required and should be explicitly set. + # Below, we set it to the existing value if it has not been set. + size=dict(type='int'), + state=dict(choices=['absent', 'present'], default='present'), + zone=dict(required=True), + autoscaling=dict(type='dict', default=None), + service_account_email=dict(), + service_account_permissions=dict(type='list'), + pem_file=dict(), + credentials_file=dict(), + project_id=dict(), ), ) + + if not HAS_PYTHON26: + module.fail_json( + msg="GCE module requires python's 'ast' module, python v2.6+") + if not HAS_LIBCLOUD: + module.fail_json( + msg='libcloud with GCE Managed Instance Group support (1.1+) required for this module.') + + params = {} + params['state'] = module.params.get('state') + params['zone'] = module.params.get('zone') + params['name'] = module.params.get('name') + params['size'] = module.params.get('size') + params['template'] = module.params.get('template') + params['recreate_instances'] = module.params.get('recreate_instances') + params['autoscaling'] = module.params.get('autoscaling', None) + + (valid_autoscaling, as_msg) = _validate_autoscaling_params(params) + if not valid_autoscaling: + module.fail_json(msg=as_msg, changed=False) + + gce = gce_connect(module) + changed = False + json_output = {'state': params['state'], 'zone': params['zone']} + mig = get_mig(gce, params['name'], params['zone']) + + if not mig: + if params['state'] == 'absent': + # Doesn't exist in GCE, and state==absent. + changed = False + module.fail_json( + msg="Cannot delete unknown managed instance group: %s" % + (params['name'])) + else: + # Create MIG + req_create_fields = [ + {'name': 'template', 'required': True, 'type': str}, + {'name': 'size', 'required': True, 'type': int} + ] # yapf: disable + + (valid_create_fields, valid_create_msg) = _check_params( + params, req_create_fields) + if not valid_create_fields: + module.fail_json(msg=valid_create_msg, changed=False) + + (changed, json_output['created_instances']) = create_mig(gce, + params) + if params['autoscaling'] and params['autoscaling'][ + 'enabled'] is True: + # Fetch newly-created MIG and create Autoscaler for it. + mig = get_mig(gce, params['name'], params['zone']) + if not mig: + module.fail_json( + msg='Unable to fetch created MIG %s to create \ + autoscaler in zone: %s' % ( + params['name'], params['zone']), changed=False) + + if not create_autoscaler(gce, mig, params['autoscaling']): + module.fail_json( + msg='Unable to fetch MIG %s to create autoscaler \ + in zone: %s' % (params['name'], params['zone']), + changed=False) + + json_output['created_autoscaler'] = True + + elif params['state'] == 'absent': + # Delete MIG + + # First, check and remove the autoscaler, if present. + # Note: multiple autoscalers can be associated to a single MIG. We + # only handle the one that is named, but we might want to think about this. + if params['autoscaling']: + autoscaler = get_autoscaler(gce, params['autoscaling']['name'], + params['zone']) + if not autoscaler: + module.fail_json(msg='Unable to fetch autoscaler %s to delete \ + in zone: %s' % (params['autoscaling']['name'], params['zone']), + changed=False) + + changed = delete_autoscaler(autoscaler) + json_output['deleted_autoscaler'] = changed + + # Now, delete the MIG. + (changed, json_output['deleted_instances']) = delete_mig(mig) + + else: + # Update MIG + # If we're going to update a MIG, we need a size and template values. + # If not specified, we use the values from the existing MIG. + if not params['size']: + params['size'] = mig.size + + if not params['template']: + params['template'] = mig.template.name + + if params['template'] != mig.template.name: + # Update Instance Template. + new_template = gce.ex_get_instancetemplate(params['template']) + mig.set_instancetemplate(new_template) + json_output['updated_instancetemplate'] = True + changed = True + if params['recreate_instances'] is True: + # Recreate Instances. + (changed, json_output['recreated_instances'] + ) = recreate_instances_in_mig(mig) + + if params['size'] != mig.size: + # Resize MIG. + keystr = 'created' if params['size'] > mig.size else 'deleted' + (changed, json_output['resize_%s_instances' % + (keystr)]) = resize_mig(mig, params['size']) + + # Update Autoscaler + if params['autoscaling']: + autoscaler = get_autoscaler(gce, params['autoscaling']['name'], + params['zone']) + if not autoscaler: + # Try to create autoscaler. + # Note: this isn't perfect, if the autoscaler name has changed + # we wouldn't know that here. + if not create_autoscaler(gce, mig, params['autoscaling']): + module.fail_json( + msg='Unable to create autoscaler %s for existing MIG %s\ + in zone: %s' % (params['autoscaling']['name'], + params['name'], params['zone']), + changed=False) + json_output['created_autoscaler'] = True + changed = True + else: + if params['autoscaling']['enabled'] is False: + # Delete autoscaler + changed = delete_autoscaler(autoscaler) + json_output['delete_autoscaler'] = changed + else: + # Update policy, etc. + changed = update_autoscaler(gce, autoscaler, + params['autoscaling']) + json_output['updated_autoscaler'] = changed + + json_output['changed'] = changed + json_output.update(params) + module.exit_json(**json_output) + +# import module snippets +from ansible.module_utils.basic import * +from ansible.module_utils.gce import * +if __name__ == '__main__': + main() From 3b0bc3e79eb5f27e18b39af4f54f4a415767d433 Mon Sep 17 00:00:00 2001 From: Brian Maddy Date: Fri, 16 Sep 2016 17:48:21 -0500 Subject: [PATCH 348/770] typo in stat.executable (was stat.excutable) (#4886) I didn't actually run this because it's so simple, but it seems correct. --- files/stat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/stat.py b/files/stat.py index 38ede211643..48979d73791 100644 --- a/files/stat.py +++ b/files/stat.py @@ -369,7 +369,7 @@ def format_output(module, path, st, follow, get_md5, get_checksum, isgid=bool(mode & stat.S_ISGID), readable=os.access(path, os.R_OK), writeable=os.access(path, os.W_OK), - excutable=os.access(path, os.X_OK), + executable=os.access(path, os.X_OK), ) if stat.S_ISLNK(mode): From 488f0827616cd5e9ef45b64b15662b9c70ed56e6 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 16:23:27 -0700 Subject: [PATCH 349/770] Fix documentation fragment references. (#4890) --- network/dnos10/dnos10_command.py | 2 +- network/dnos10/dnos10_config.py | 2 +- network/dnos10/dnos10_facts.py | 2 +- network/dnos10/dnos10_template.py | 2 +- network/dnos6/dnos6_command.py | 2 +- network/dnos6/dnos6_config.py | 2 +- network/dnos9/dnos9_command.py | 2 +- network/dnos9/dnos9_config.py | 2 +- network/dnos9/dnos9_facts.py | 2 +- network/dnos9/dnos9_template.py | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/network/dnos10/dnos10_command.py b/network/dnos10/dnos10_command.py index 3f880985c62..7acdc837c2a 100644 --- a/network/dnos10/dnos10_command.py +++ b/network/dnos10/dnos10_command.py @@ -29,7 +29,7 @@ before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. Please use M(dnos10_config) to configure Dell OS10 devices. -extends_documentation_fragment: dnos10 +extends_documentation_fragment: dellos10 options: commands: description: diff --git a/network/dnos10/dnos10_config.py b/network/dnos10/dnos10_config.py index 9e630ce33b2..0c1ddf8bc21 100644 --- a/network/dnos10/dnos10_config.py +++ b/network/dnos10/dnos10_config.py @@ -27,7 +27,7 @@ for segmenting configuration into sections. This module provides an implementation for working with Dell OS10 configuration sections in a deterministic way. -extends_documentation_fragment: dnos10 +extends_documentation_fragment: dellos10 options: lines: description: diff --git a/network/dnos10/dnos10_facts.py b/network/dnos10/dnos10_facts.py index 2ab0dc9d400..6e2d0c1b6e9 100644 --- a/network/dnos10/dnos10_facts.py +++ b/network/dnos10/dnos10_facts.py @@ -27,7 +27,7 @@ base network fact keys with C(ansible_net_). The facts module will always collect a base set of facts from the device and can enable or disable collection of additional facts. -extends_documentation_fragment: dnos10 +extends_documentation_fragment: dellos10 options: gather_subset: description: diff --git a/network/dnos10/dnos10_template.py b/network/dnos10/dnos10_template.py index 10f5c806436..c957a2f0785 100644 --- a/network/dnos10/dnos10_template.py +++ b/network/dnos10/dnos10_template.py @@ -28,7 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. -extends_documentation_fragment: dnos10 +extends_documentation_fragment: dellos10 options: src: description: diff --git a/network/dnos6/dnos6_command.py b/network/dnos6/dnos6_command.py index f1250086121..173d2b7dc8a 100644 --- a/network/dnos6/dnos6_command.py +++ b/network/dnos6/dnos6_command.py @@ -28,7 +28,7 @@ before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. Please use M(dnos6_config) to configure Dell OS6 devices. -extends_documentation_fragment: dnos6 +extends_documentation_fragment: dellos6 options: commands: description: diff --git a/network/dnos6/dnos6_config.py b/network/dnos6/dnos6_config.py index 2293a2deb31..527dac8c413 100644 --- a/network/dnos6/dnos6_config.py +++ b/network/dnos6/dnos6_config.py @@ -27,7 +27,7 @@ for segmenting configuration into sections. This module provides an implementation for working with Dell OS6 configuration sections in a deterministic way. -extends_documentation_fragment: dnos6 +extends_documentation_fragment: dellos6 options: lines: description: diff --git a/network/dnos9/dnos9_command.py b/network/dnos9/dnos9_command.py index 1fa2ba4276f..f73055cd6d3 100755 --- a/network/dnos9/dnos9_command.py +++ b/network/dnos9/dnos9_command.py @@ -28,7 +28,7 @@ before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. Please use M(dnos9_config) to configure Dell OS9 devices. -extends_documentation_fragment: dnos9 +extends_documentation_fragment: dellos9 options: commands: description: diff --git a/network/dnos9/dnos9_config.py b/network/dnos9/dnos9_config.py index 762e7b3ac86..0c336b0d108 100755 --- a/network/dnos9/dnos9_config.py +++ b/network/dnos9/dnos9_config.py @@ -27,7 +27,7 @@ for segmenting configuration into sections. This module provides an implementation for working with Dell OS9 configuration sections in a deterministic way. -extends_documentation_fragment: dnos9 +extends_documentation_fragment: dellos9 options: lines: description: diff --git a/network/dnos9/dnos9_facts.py b/network/dnos9/dnos9_facts.py index be64159ebbd..d475e2b5cd9 100644 --- a/network/dnos9/dnos9_facts.py +++ b/network/dnos9/dnos9_facts.py @@ -27,7 +27,7 @@ base network fact keys with C(ansible_net_). The facts module will always collect a base set of facts from the device and can enable or disable collection of additional facts. -extends_documentation_fragment: dnos9 +extends_documentation_fragment: dellos9 options: gather_subset: description: diff --git a/network/dnos9/dnos9_template.py b/network/dnos9/dnos9_template.py index 0e9bad427f5..cc0c04e5d5d 100755 --- a/network/dnos9/dnos9_template.py +++ b/network/dnos9/dnos9_template.py @@ -28,7 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. -extends_documentation_fragment: dnos9 +extends_documentation_fragment: dellos9 options: src: description: From 0f8f47dcf164a0bd56c5dd2bf290ba70839f6359 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 16 Sep 2016 22:09:58 -0400 Subject: [PATCH 350/770] fix up eos_eapi module * fixes TypeError: load_config() got an unexpected keyword argument 'session'\n" * removes qos argument ref #4869 --- network/eos/eos_eapi.py | 30 +++++------------------------- 1 file changed, 5 insertions(+), 25 deletions(-) diff --git a/network/eos/eos_eapi.py b/network/eos/eos_eapi.py index 7edbe3fcda6..79d1c541eec 100644 --- a/network/eos/eos_eapi.py +++ b/network/eos/eos_eapi.py @@ -113,14 +113,6 @@ required: false default: default version_added: "2.2" - qos: - description: - - The C(qos) argument configures the IP DSCP value to assign to - eAPI response packets. This argument accepts integer values - in the valid IP DSCP range of 0 to 63. - required: false - default: 0 - version_added: "2.2" config: description: - The module, by default, will connect to the remote device and @@ -256,11 +248,6 @@ def set_vrf(module, commands): module.fail_json(msg="vrf '%s' is not configured" % vrf) commands.append('vrf %s' % vrf) -def set_qos(module, commands): - if not 0 <= module.params['qos'] <= 63: - module.fail_json(msg='qos must be between 0 and 63') - commands.append('qos dscp %s' % module.params['qos']) - def get_config(module): contents = module.params['config'] if not contents: @@ -270,17 +257,11 @@ def get_config(module): return config def load_config(module, commands, result): - session = 'ansible_%s' % int(time.time()) commit = not module.check_mode - - diff = module.config.load_config(commands, session=session, commit=commit) - - # once the configuration is done, remove the config session and - # remove the session name from the result - module.cli(['no configure session %s' % session]) - - result['diff'] = dict(prepared=diff) - result['changed'] = diff is not None + diff = module.config.load_config(commands, commit=commit) + if diff: + result['diff'] = dict(prepared=diff) + result['changed'] = True def load(module, commands, result): candidate = NetworkConfig(indent=3) @@ -330,12 +311,11 @@ def main(): socket=dict(aliases=['enable_socket'], default=False, type='bool'), vrf=dict(default='default'), - qos=dict(default=0, type='int'), config=dict(), # Only allow use of transport cli when configuring eAPI - transport=dict(required=True, choices=['cli']), + transport=dict(default='cli', choices=['cli']), state=dict(default='started', choices=['stopped', 'started']), ) From a709991c48e5ec0fe9e06b3aa20abe2dabe2044b Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 22:27:17 -0700 Subject: [PATCH 351/770] Run same tests on Shippable as on Travis. (#4889) Run the same tests as used on Travis. --- shippable.yml | 2 ++ test/utils/shippable/integration.sh | 2 +- test/utils/shippable/sanity-skip-python24.txt | 1 + test/utils/shippable/sanity-skip-python3.txt | 0 test/utils/shippable/sanity-test-python24.txt | 2 ++ test/utils/shippable/sanity.sh | 22 +++++++++++++++++++ 6 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 test/utils/shippable/sanity-skip-python24.txt create mode 100644 test/utils/shippable/sanity-skip-python3.txt create mode 100644 test/utils/shippable/sanity-test-python24.txt create mode 100755 test/utils/shippable/sanity.sh diff --git a/shippable.yml b/shippable.yml index c392bfa675f..a2c4e33585c 100644 --- a/shippable.yml +++ b/shippable.yml @@ -27,6 +27,8 @@ matrix: - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE - env: TEST=integration PLATFORM=osx VERSION=10.11 + + - env: TEST=sanity INSTALL_DEPS=1 build: pre_ci_boot: options: "--privileged=false --net=bridge" diff --git a/test/utils/shippable/integration.sh b/test/utils/shippable/integration.sh index ee16e765c15..cf10e681bfb 100755 --- a/test/utils/shippable/integration.sh +++ b/test/utils/shippable/integration.sh @@ -10,7 +10,7 @@ repo="${REPO_NAME}" if [ "${is_pr}" != "true" ]; then echo "Module integration tests are only supported on pull requests." - exit 1 + exit 0 fi case "${repo}" in diff --git a/test/utils/shippable/sanity-skip-python24.txt b/test/utils/shippable/sanity-skip-python24.txt new file mode 100644 index 00000000000..1434a04094b --- /dev/null +++ b/test/utils/shippable/sanity-skip-python24.txt @@ -0,0 +1 @@ +/cloud/ diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt new file mode 100644 index 00000000000..e69de29bb2d diff --git a/test/utils/shippable/sanity-test-python24.txt b/test/utils/shippable/sanity-test-python24.txt new file mode 100644 index 00000000000..5ad993ee773 --- /dev/null +++ b/test/utils/shippable/sanity-test-python24.txt @@ -0,0 +1,2 @@ +cloud/amazon/_ec2_ami_search.py +cloud/amazon/ec2_facts.py diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh new file mode 100755 index 00000000000..5ff826efbae --- /dev/null +++ b/test/utils/shippable/sanity.sh @@ -0,0 +1,22 @@ +#!/bin/bash -eux + +source_root=$(python -c "from os import path; print(path.abspath(path.join(path.dirname('$0'), '../../..')))") + +install_deps="${INSTALL_DEPS:-}" + +cd "${source_root}" + +if [ "${install_deps}" != "" ]; then + add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq + + pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible + pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing +fi + +python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" +python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . +python2.6 -m compileall -fq . +python2.7 -m compileall -fq . +python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" + +ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . From 4fd906e5a613c4f1d2819b0e91e4ce46d2eb2b3b Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 22:54:59 -0700 Subject: [PATCH 352/770] Cosmetic fix to test PR and merge hooks. (#4893) --- test/utils/shippable/sanity.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 5ff826efbae..1c0e2451a43 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -13,7 +13,7 @@ if [ "${install_deps}" != "" ]; then pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing fi -python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" +python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . python2.6 -m compileall -fq . python2.7 -m compileall -fq . From fb366e3766849a38a07f3c45b1cfd812e49da1d5 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 16 Sep 2016 23:12:33 -0700 Subject: [PATCH 353/770] Update CI badge. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ebcf0c7b941..fb71dafe030 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/ansible/ansible-modules-core.svg?branch=devel)](https://travis-ci.org/ansible/ansible-modules-core) +[![Build Status](https://api.shippable.com/projects/573f79d02a8192902e20e34e/badge?branch=devel)](https://app.shippable.com/projects/573f79d02a8192902e20e34e) ansible-modules-core ==================== From 7f55c9fc8eb05c525adeea69043620a7b3023ac4 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 17 Sep 2016 03:16:48 -0700 Subject: [PATCH 354/770] dnos* -> dellos* (Rename module & updated copyright) (#4888) * Renamed the Modules from dnos* -> dellos*, updated copyright, corrected doc issues * Removed the unwanted module import --- network/{dnos10 => dellos10}/__init__.py | 0 .../dellos10_command.py} | 26 +++++++++++-------- .../dellos10_config.py} | 16 +++++++----- .../dellos10_facts.py} | 17 +++++++----- .../dellos10_template.py} | 22 +++++++++------- network/{dnos6 => dellos6}/__init__.py | 0 .../dellos6_command.py} | 26 +++++++++++-------- .../dellos6_config.py} | 14 ++++++---- network/{dnos9 => dellos9}/__init__.py | 0 .../dellos9_command.py} | 24 ++++++++++------- .../dellos9_config.py} | 14 ++++++---- .../dellos9_facts.py} | 14 ++++++---- .../dellos9_template.py} | 14 ++++++---- 13 files changed, 113 insertions(+), 74 deletions(-) rename network/{dnos10 => dellos10}/__init__.py (100%) rename network/{dnos10/dnos10_command.py => dellos10/dellos10_command.py} (91%) rename network/{dnos10/dnos10_config.py => dellos10/dellos10_config.py} (97%) rename network/{dnos10/dnos10_facts.py => dellos10/dellos10_facts.py} (98%) rename network/{dnos10/dnos10_template.py => dellos10/dellos10_template.py} (93%) rename network/{dnos6 => dellos6}/__init__.py (100%) rename network/{dnos6/dnos6_command.py => dellos6/dellos6_command.py} (91%) rename network/{dnos6/dnos6_config.py => dellos6/dellos6_config.py} (97%) rename network/{dnos9 => dellos9}/__init__.py (100%) rename network/{dnos9/dnos9_command.py => dellos9/dellos9_command.py} (92%) rename network/{dnos9/dnos9_config.py => dellos9/dellos9_config.py} (97%) rename network/{dnos9/dnos9_facts.py => dellos9/dellos9_facts.py} (98%) rename network/{dnos9/dnos9_template.py => dellos9/dellos9_template.py} (96%) diff --git a/network/dnos10/__init__.py b/network/dellos10/__init__.py similarity index 100% rename from network/dnos10/__init__.py rename to network/dellos10/__init__.py diff --git a/network/dnos10/dnos10_command.py b/network/dellos10/dellos10_command.py similarity index 91% rename from network/dnos10/dnos10_command.py rename to network/dellos10/dellos10_command.py index 7acdc837c2a..5c8e5ea35f2 100644 --- a/network/dnos10/dnos10_command.py +++ b/network/dellos10/dellos10_command.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -18,9 +22,9 @@ DOCUMENTATION = """ --- -module: dnos10_command +module: dellos10_command version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg_net)" +author: "Senthil Kumar Ganesan (@skg-net)" short_description: Run commands on remote devices running Dell OS10 description: - Sends arbitrary commands to a Dell OS10 node and returns the results @@ -28,12 +32,12 @@ argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. - Please use M(dnos10_config) to configure Dell OS10 devices. + Please use M(dellos10_config) to configure Dell OS10 devices. extends_documentation_fragment: dellos10 options: commands: description: - - List of commands to send to the remote dnos10 device over the + - List of commands to send to the remote dellos10 device over the configured provider. The resulting output from the command is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or @@ -78,25 +82,25 @@ tasks: - name: run show version on remote devices - dnos10_command: + dellos10_command: commands: show version provider: "{{ cli }}" - name: run show version and check to see if output contains OS10 - dnos10_command: + dellos10_command: commands: show version wait_for: result[0] contains OS10 provider: "{{ cli }}" - name: run multiple commands on remote nodes - dnos10_command: + dellos10_command: commands: - show version - show interface provider: "{{ cli }}" - name: run multiple commands and evaluate the output - dnos10_command: + dellos10_command: commands: - show version - show interface @@ -135,7 +139,7 @@ from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, FailedConditionsError from ansible.module_utils.network import NetworkModule, NetworkError -import ansible.module_utils.dnos10 +import ansible.module_utils.dellos10 def to_lines(stdout): for item in stdout: @@ -168,9 +172,9 @@ def main(): 'check mode, not executing `%s`' % cmd) else: if cmd.startswith('conf'): - module.fail_json(msg='dnos10_command does not support running ' + module.fail_json(msg='dellos10_command does not support running ' 'config mode commands. Please use ' - 'dnos10_config instead') + 'dellos10_config instead') runner.add_command(cmd) for item in conditionals: diff --git a/network/dnos10/dnos10_config.py b/network/dellos10/dellos10_config.py similarity index 97% rename from network/dnos10/dnos10_config.py rename to network/dellos10/dellos10_config.py index 0c1ddf8bc21..e505799e0e8 100644 --- a/network/dnos10/dnos10_config.py +++ b/network/dellos10/dellos10_config.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -18,9 +22,9 @@ DOCUMENTATION = """ --- -module: dnos10_config +module: dellos10_config version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg_net)" +author: "Senthil Kumar Ganesan (@skg-net)" short_description: Manage Dell OS10 configuration sections description: - Dell OS10 configurations use a simple block indent file syntax @@ -138,11 +142,11 @@ """ EXAMPLES = """ -- dnos10_config: +- dellos10_config: lines: ['hostname {{ inventory_hostname }}'] provider: "{{ cli }}" -- dnos10_config: +- dellos10_config: lines: - 10 permit ip host 1.1.1.1 any log - 20 permit ip host 2.2.2.2 any log @@ -154,7 +158,7 @@ match: exact provider: "{{ cli }}" -- dnos10_config: +- dellos10_config: lines: - 10 permit ip host 1.1.1.1 any log - 20 permit ip host 2.2.2.2 any log @@ -190,7 +194,7 @@ """ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dnos10 import get_config, get_sublevel_config +from ansible.module_utils.dellos10 import get_config, get_sublevel_config def get_candidate(module): candidate = NetworkConfig(indent=1) diff --git a/network/dnos10/dnos10_facts.py b/network/dellos10/dellos10_facts.py similarity index 98% rename from network/dnos10/dnos10_facts.py rename to network/dellos10/dellos10_facts.py index 6e2d0c1b6e9..bbccd20a34c 100644 --- a/network/dnos10/dnos10_facts.py +++ b/network/dellos10/dellos10_facts.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -17,9 +21,9 @@ # DOCUMENTATION = """ --- -module: dnos10_facts +module: dellos10_facts version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg_net)" +author: "Senthil Kumar Ganesan (@skg-net)" short_description: Collect facts from remote devices running Dell OS10 description: - Collects a base set of device facts from a remote device that @@ -43,16 +47,16 @@ EXAMPLES = """ # Collect all facts from the device -- dnos10_facts: +- dellos10_facts: gather_subset: all # Collect only the config and default facts -- dnos10_facts: +- dellos10_facts: gather_subset: - config # Do not collect hardware facts -- dnos10_facts: +- dellos10_facts: gather_subset: - "!hardware" """ @@ -125,12 +129,11 @@ """ import re -import itertools from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dnos10 +import ansible.module_utils.dellos10 try: from lxml import etree as ET diff --git a/network/dnos10/dnos10_template.py b/network/dellos10/dellos10_template.py similarity index 93% rename from network/dnos10/dnos10_template.py rename to network/dellos10/dellos10_template.py index c957a2f0785..b9bec6a3311 100644 --- a/network/dnos10/dnos10_template.py +++ b/network/dellos10/dellos10_template.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -17,9 +21,9 @@ # DOCUMENTATION = """ --- -module: dnos10_template +module: dellos10_template version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg_net)" +author: "Senthil Kumar Ganesan (@skg-net)" short_description: Manage Dell OS10 device configurations over SSH. description: - Manages Dell OS10 network device configurations over SSH. This module @@ -44,7 +48,7 @@ current device running-config. When set to true, this will cause the module to push the contents of I(src) into the device without first checking if already configured. This argument is - mutually exclusive with O(config). + mutually exclusive with I(config). required: false default: false choices: [ "true", "false" ] @@ -54,7 +58,7 @@ the running-config from the node prior to making any changes. The backup file will be written to backup_{{ hostname }} in the root of the playbook directory. This argument is - mutually exclusive with O(config). + mutually exclusive with I(config). required: false default: false @@ -68,7 +72,7 @@ every task. The I(config) argument allows the implementer to pass in the configuration to use as the base config for comparison. This argument is mutually exclusive with - O(force) and O(backup). + I(force) and I(backup). required: false default: null @@ -76,20 +80,20 @@ EXAMPLES = """ - name: push a configuration onto the device - dnos10_template: + dellos10_template: host: hostname username: foo src: config.j2 - name: forceable push a configuration onto the device - dnos10_template: + dellos10_template: host: hostname username: foo src: config.j2 force: yes - name: provide the base configuration for comparison - dnos10_template: + dellos10_template: host: hostname username: foo src: candidate_config.txt @@ -117,7 +121,7 @@ """ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dnos10 +import ansible.module_utils.dellos10 def get_config(module): diff --git a/network/dnos6/__init__.py b/network/dellos6/__init__.py similarity index 100% rename from network/dnos6/__init__.py rename to network/dellos6/__init__.py diff --git a/network/dnos6/dnos6_command.py b/network/dellos6/dellos6_command.py similarity index 91% rename from network/dnos6/dnos6_command.py rename to network/dellos6/dellos6_command.py index 173d2b7dc8a..3cc8137043c 100644 --- a/network/dnos6/dnos6_command.py +++ b/network/dellos6/dellos6_command.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -18,21 +22,21 @@ DOCUMENTATION = """ --- -module: dnos6_command +module: dellos6_command version_added: "2.2" short_description: Run commands on remote devices running Dell OS6 description: - Sends arbitrary commands to a Dell OS6 node and returns the results - read from the device. The M(dnos6_command) module includes an + read from the device. The M(dellos6_command) module includes an argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. - Please use M(dnos6_config) to configure Dell OS6 devices. + Please use M(dellos6_config) to configure Dell OS6 devices. extends_documentation_fragment: dellos6 options: commands: description: - - List of commands to send to the remote dnos6 device over the + - List of commands to send to the remote dellos6 device over the configured provider. The resulting output from the command is returned. If the I(waitfor) argument is provided, the module is not returned until the condition is satisfied or @@ -77,25 +81,25 @@ tasks: - name: run show verion on remote devices - dnos6_command: + dellos6_command: commands: show version provider "{{ cli }}" - name: run show version and check to see if output contains Dell - dnos6_command: + dellos6_command: commands: show version wait_for: result[0] contains Dell provider "{{ cli }}" - name: run multiple commands on remote nodes - dnos6_command: + dellos6_command: commands: - show version - show interfaces provider "{{ cli }}" - name: run multiple commands and evaluate the output - dnos6_command: + dellos6_command: commands: - show version - show interfaces @@ -134,7 +138,7 @@ from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, FailedConditionsError from ansible.module_utils.network import NetworkModule, NetworkError -import ansible.module_utils.dnos6 +import ansible.module_utils.dellos6 def to_lines(stdout): for item in stdout: @@ -168,9 +172,9 @@ def main(): 'check mode, not executing `%s`' % cmd) else: if cmd.startswith('conf'): - module.fail_json(msg='dnos6_command does not support running ' + module.fail_json(msg='dellos6_command does not support running ' 'config mode commands. Please use ' - 'dnos6_config instead') + 'dellos6_config instead') runner.add_command(cmd) for item in conditionals: diff --git a/network/dnos6/dnos6_config.py b/network/dellos6/dellos6_config.py similarity index 97% rename from network/dnos6/dnos6_config.py rename to network/dellos6/dellos6_config.py index 527dac8c413..008cb0122e8 100644 --- a/network/dnos6/dnos6_config.py +++ b/network/dellos6/dellos6_config.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -18,7 +22,7 @@ DOCUMENTATION = """ --- -module: dnos6_config +module: dellos6_config version_added: "2.2" author: "Abirami N(@abirami-n)" short_description: Manage Dell OS6 configuration sections @@ -138,11 +142,11 @@ """ EXAMPLES = """ -- dnos6_config: +- dellos6_config: lines: ['hostname {{ inventory_hostname }}'] provider: "{{ cli }}" -- dnos6_config: +- dellos6_config: lines: - 10 permit ip 1.1.1.1 any log - 20 permit ip 2.2.2.2 any log @@ -154,7 +158,7 @@ match: exact provider: "{{ cli }}" -- dnos6_config: +- dellos6_config: lines: - 10 permit ip 1.1.1.1 any log - 20 permit ip 2.2.2.2 any log @@ -190,7 +194,7 @@ """ from ansible.module_utils.netcfg import NetworkConfig, dumps, ConfigLine from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dnos6 import get_config +from ansible.module_utils.dellos6 import get_config def get_candidate(module): candidate = NetworkConfig(indent=1) diff --git a/network/dnos9/__init__.py b/network/dellos9/__init__.py similarity index 100% rename from network/dnos9/__init__.py rename to network/dellos9/__init__.py diff --git a/network/dnos9/dnos9_command.py b/network/dellos9/dellos9_command.py similarity index 92% rename from network/dnos9/dnos9_command.py rename to network/dellos9/dellos9_command.py index f73055cd6d3..b6bc6ed9d70 100755 --- a/network/dnos9/dnos9_command.py +++ b/network/dellos9/dellos9_command.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -18,7 +22,7 @@ DOCUMENTATION = """ --- -module: dnos9_command +module: dellos9_command version_added: "2.2" short_description: Run commands on remote devices running Dell OS9 description: @@ -27,12 +31,12 @@ argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. - Please use M(dnos9_config) to configure Dell OS9 devices. + Please use M(dellos9_config) to configure Dell OS9 devices. extends_documentation_fragment: dellos9 options: commands: description: - - List of commands to send to the remote dnos9 device over the + - List of commands to send to the remote dellos9 device over the configured provider. The resulting output from the command is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or @@ -77,25 +81,25 @@ tasks: - name: run show version on remote devices - dnos9_command: + dellos9_command: commands: show version provider "{{ cli }}" - name: run show version and check to see if output contains OS9 - dnos9_command: + dellos9_command: commands: show version wait_for: result[0] contains OS9 provider "{{ cli }}" - name: run multiple commands on remote nodes - dnos9_command: + dellos9_command: commands: - show version - show interfaces provider "{{ cli }}" - name: run multiple commands and evalute the output - dnos9_command: + dellos9_command: commands: - show version - show interfaces @@ -134,7 +138,7 @@ from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, FailedConditionsError from ansible.module_utils.network import NetworkModule, NetworkError -import ansible.module_utils.dnos9 +import ansible.module_utils.dellos9 def to_lines(stdout): @@ -169,9 +173,9 @@ def main(): 'check mode, not executing `%s`' % cmd) else: if cmd.startswith('conf'): - module.fail_json(msg='dnos9_command does not support running ' + module.fail_json(msg='dellos9_command does not support running ' 'config mode commands. Please use ' - 'dnos9_config instead') + 'dellos9_config instead') runner.add_command(cmd) for item in conditionals: diff --git a/network/dnos9/dnos9_config.py b/network/dellos9/dellos9_config.py similarity index 97% rename from network/dnos9/dnos9_config.py rename to network/dellos9/dellos9_config.py index 0c336b0d108..a90531b0f6c 100755 --- a/network/dnos9/dnos9_config.py +++ b/network/dellos9/dellos9_config.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -18,7 +22,7 @@ DOCUMENTATION = """ --- -module: dnos9_config +module: dellos9_config version_added: "2.2" author: "Dhivya P (@dhivyap)" short_description: Manage Dell OS9 configuration sections @@ -138,11 +142,11 @@ """ EXAMPLES = """ -- dnos9_config: +- dellos9_config: lines: ['hostname {{ inventory_hostname }}'] provider: "{{ cli }}" -- dnos9_config: +- dellos9_config: lines: - 10 permit ip host 1.1.1.1 any log - 20 permit ip host 2.2.2.2 any log @@ -154,7 +158,7 @@ match: exact provider: "{{ cli }}" -- dnos9_config: +- dellos9_config: lines: - 10 permit ip host 1.1.1.1 any log - 20 permit ip host 2.2.2.2 any log @@ -190,7 +194,7 @@ """ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dnos9 import get_config, get_sublevel_config +from ansible.module_utils.dellos9 import get_config, get_sublevel_config def get_candidate(module): diff --git a/network/dnos9/dnos9_facts.py b/network/dellos9/dellos9_facts.py similarity index 98% rename from network/dnos9/dnos9_facts.py rename to network/dellos9/dellos9_facts.py index d475e2b5cd9..4b8b2a83a44 100644 --- a/network/dnos9/dnos9_facts.py +++ b/network/dellos9/dellos9_facts.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -17,7 +21,7 @@ # DOCUMENTATION = """ --- -module: dnos9_facts +module: dellos9_facts version_added: "2.2" author: "Dhivya P (@dhivyap)" short_description: Collect facts from remote devices running Dell OS9 @@ -43,16 +47,16 @@ EXAMPLES = """ # Collect all facts from the device -- dnos9_facts: +- dellos9_facts: gather_subset: all # Collect only the config and default facts -- dnos9_facts: +- dellos9_facts: gather_subset: - config # Do not collect hardware facts -- dnos9_facts: +- dellos9_facts: gather_subset: - "!hardware" """ @@ -128,7 +132,7 @@ from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dnos9 +import ansible.module_utils.dellos9 class FactsBase(object): diff --git a/network/dnos9/dnos9_template.py b/network/dellos9/dellos9_template.py similarity index 96% rename from network/dnos9/dnos9_template.py rename to network/dellos9/dellos9_template.py index cc0c04e5d5d..e4ec7364549 100755 --- a/network/dnos9/dnos9_template.py +++ b/network/dellos9/dellos9_template.py @@ -1,5 +1,9 @@ #!/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify @@ -17,7 +21,7 @@ # DOCUMENTATION = """ --- -module: dnos9_template +module: dellos9_template version_added: "2.2" author: "Dhivya P (@dhivyap)" short_description: Manage Dell OS9 device configurations over SSH. @@ -76,20 +80,20 @@ EXAMPLES = """ - name: push a configuration onto the device - dnos9_template: + dellos9_template: host: hostname username: foo src: config.j2 - name: forceable push a configuration onto the device - dnos9_template: + dellos9_template: host: hostname username: foo src: config.j2 force: yes - name: provide the base configuration for comparison - dnos9_template: + dellos9_template: host: hostname username: foo src: candidate_config.txt @@ -117,7 +121,7 @@ """ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dnos9 +import ansible.module_utils.dellos9 def get_config(module): From d629b3ccc90936135bd1cbdacf4acb4f32c396d8 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 17 Sep 2016 10:19:16 -0700 Subject: [PATCH 355/770] Added support for dellos6_facts module --- network/dellos6/dellos6_facts.py | 437 +++++++++++++++++++++++++++++++ 1 file changed, 437 insertions(+) create mode 100644 network/dellos6/dellos6_facts.py diff --git a/network/dellos6/dellos6_facts.py b/network/dellos6/dellos6_facts.py new file mode 100644 index 00000000000..922b14366e4 --- /dev/null +++ b/network/dellos6/dellos6_facts.py @@ -0,0 +1,437 @@ +# !/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: dellos6_facts +version_added: "2.2" +author: "Abirami N(@abirami-n)" +short_description: Collect facts from remote devices running Dell OS6 +description: + - Collects a base set of device facts from a remote device that + is running OS6. This module prepends all of the + base network fact keys with C(ansible_net_). The facts + module will always collect a base set of facts from the device + and can enable or disable collection of additional facts. +extends_documentation_fragment: dellos6 +options: + gather_subset: + description: + - When supplied, this argument will restrict the facts collected + to a given subset. Possible values for this argument inlcude + all, hardware, config, and interfaces. Can specify a list of + values to include a larger subset. Values can also be used + with an initial M(!) to specify that a specific subset should + not be collected. + required: false + default: '!config' +""" + +EXAMPLES = """ +# Collect all facts from the device +- dellos6_facts: + gather_subset: all + +# Collect only the config and default facts +- dellos6_facts: + gather_subset: + - config + +# Do not collect hardware facts +- dellos6_facts: + gather_subset: + - "!interfaces" +""" + +RETURN = """ +ansible_net_gather_subset: + description: The list of fact subsets collected from the device + returned: always + type: list + +# default +ansible_net_model: + description: The model name returned from the device + returned: always + type: str +ansible_net_serialnum: + description: The serial number of the remote device + returned: always + type: str +ansible_net_version: + description: The operating system version running on the remote device + returned: always + type: str +ansible_net_hostname: + description: The configured hostname of the device + returned: always + type: string +ansible_net_image: + description: The image file the device is running + returned: always + type: string + +# hardware +ansible_net_memfree_mb: + description: The available free memory on the remote device in Mb + returned: when hardware is configured + type: int +ansible_net_memtotal_mb: + description: The total memory on the remote device in Mb + returned: when hardware is configured + type: int + +# config +ansible_net_config: + description: The current active config from the device + returned: when config is configured + type: str + +# interfaces +ansible_net_interfaces: + description: A hash of all interfaces running on the system + returned: when interfaces is configured + type: dict +ansible_net_neighbors: + description: The list of LLDP neighbors from the remote device + returned: when interfaces is configured + type: dict + +""" +import re + +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.dellos6 + +class FactsBase(object): + + def __init__(self, runner): + self.runner = runner + self.facts = dict() + + self.commands() + +class Default(FactsBase): + + def commands(self): + self.runner.add_command('show version') + self.runner.add_command('show running-config | include hostname') + + def populate(self): + data = self.runner.get_command('show version') + self.facts['version'] = self.parse_version(data) + self.facts['serialnum'] = self.parse_serialnum(data) + self.facts['model'] = self.parse_model(data) + self.facts['image'] = self.parse_image(data) + hdata =self.runner.get_command('show running-config | include hostname') + self.facts['hostname'] = self.parse_hostname(hdata) + + def parse_version(self, data): + match = re.search(r'HW Version(.+)\s(\d+)', data) + if match: + return match.group(2) + + def parse_hostname(self, data): + match = re.search(r'\S+\s(\S+)', data, re.M) + if match: + return match.group(1) + + def parse_model(self, data): + match = re.search(r'System Model ID(.+)\s([A-Z0-9]*)\n', data, re.M) + if match: + return match.group(2) + + def parse_image(self, data): + match = re.search(r'Image File(.+)\s([A-Z0-9a-z_.]*)\n', data) + if match: + return match.group(2) + + def parse_serialnum(self, data): + match = re.search(r'Serial Number(.+)\s([A-Z0-9]*)\n', data) + if match: + return match.group(2) + + +class Hardware(FactsBase): + + def commands(self): + self.runner.add_command('show memory cpu') + + def populate(self): + + data = self.runner.get_command('show memory cpu') + match = re.findall('\s(\d+)\s', data) + if match: + self.facts['memtotal_mb'] = int(match[0]) / 1024 + self.facts['memfree_mb'] = int(match[1]) / 1024 + + +class Config(FactsBase): + + def commands(self): + self.runner.add_command('show running-config') + + def populate(self): + self.facts['config'] = self.runner.get_command('show running-config') + + +class Interfaces(FactsBase): + def commands(self): + self.runner.add_command('show interfaces') + self.runner.add_command('show interfaces status') + self.runner.add_command('show interfaces transceiver properties') + self.runner.add_command('show ip int') + self.runner.add_command('show lldp') + self.runner.add_command('show lldp remote-device all') + + def populate(self): + vlan_info = dict() + data = self.runner.get_command('show interfaces') + interfaces = self.parse_interfaces(data) + desc = self.runner.get_command('show interfaces status') + properties = self.runner.get_command('show interfaces transceiver properties') + vlan = self.runner.get_command('show ip int') + vlan_info = self.parse_vlan(vlan) + self.facts['interfaces'] = self.populate_interfaces(interfaces,desc,properties) + self.facts['interfaces'].update(vlan_info) + if 'LLDP is not enabled' not in self.runner.get_command('show lldp'): + neighbors = self.runner.get_command('show lldp remote-device all') + self.facts['neighbors'] = self.parse_neighbors(neighbors) + + def parse_vlan(self,vlan): + facts =dict() + vlan_info, vlan_info_next = vlan.split('---------- ----- --------------- --------------- -------') + for en in vlan_info_next.splitlines(): + if en == '': + continue + match = re.search('^(\S+)\s+(\S+)\s+(\S+)', en) + intf = match.group(1) + if intf not in facts: + facts[intf] = list() + fact = dict() + matc=re.search('^([\w+\s\d]*)\s+(\S+)\s+(\S+)',en) + fact['address'] = matc.group(2) + fact['masklen'] = matc.group(3) + facts[intf].append(fact) + return facts + + def populate_interfaces(self, interfaces, desc, properties): + facts = dict() + for key, value in interfaces.iteritems(): + intf = dict() + intf['description'] = self.parse_description(key,desc) + intf['macaddress'] = self.parse_macaddress(value) + intf['mtu'] = self.parse_mtu(value) + intf['bandwidth'] = self.parse_bandwidth(value) + intf['mediatype'] = self.parse_mediatype(key,properties) + intf['duplex'] = self.parse_duplex(value) + intf['lineprotocol'] = self.parse_lineprotocol(value) + intf['operstatus'] = self.parse_operstatus(value) + intf['type'] = self.parse_type(key,properties) + facts[key] = intf + return facts + + def parse_neighbors(self, neighbors): + facts = dict() + neighbor, neighbor_next = neighbors.split('--------- ------- ------------------- ----------------- -----------------') + for en in neighbor_next.splitlines(): + if en == '': + continue + intf = self.parse_lldp_intf(en.split()[0]) + if intf not in facts: + facts[intf] = list() + fact = dict() + fact['host'] = self.parse_lldp_host(en.split()[4]) + fact['port'] = self.parse_lldp_port(en.split()[3]) + facts[intf].append(fact) + + return facts + + def parse_interfaces(self, data): + parsed = dict() + for line in data.split('\n'): + if len(line) == 0: + continue + else: + match = re.match(r'Interface Name(.+)\s([A-Za-z0-9/]*)', line) + if match: + key = match.group(2) + parsed[key] = line + else: + parsed[key] += '\n%s' % line + return parsed + + def parse_description(self, key, desc): + desc, desc_next = desc.split('--------- --------------- ------ ------- ---- ------ ----- -- -------------------') + desc_val, desc_info = desc_next.split('Oob') + for en in desc_val.splitlines(): + if key in en: + match = re.search('^(\S+)\s+(\S+)', en) + if match.group(2) in ['Full','N/A']: + return "Null" + else: + return match.group(2) + + def parse_macaddress(self, data): + match = re.search(r'Burned MAC Address(.+)\s([A-Z0-9.]*)\n', data) + if match: + return match.group(2) + + def parse_mtu(self, data): + match = re.search(r'MTU Size(.+)\s(\d+)\n', data) + if match: + return int(match.group(2)) + + def parse_bandwidth(self, data): + match = re.search(r'Port Speed(.+)\s(\d+)\n', data) + if match: + return int(match.group(2)) + + def parse_duplex(self, data): + match = re.search(r'Port Mode\s([A-Za-z]*)(.+)\s([A-Za-z/]*)\n', data) + if match: + return match.group(3) + + def parse_mediatype(self, key, properties): + mediatype, mediatype_next = properties.split('--------- ------- --------------------- --------------------- --------------') + flag=1 + for en in mediatype_next.splitlines(): + if key in en: + flag=0 + match = re.search('^(\S+)\s+(\S+)\s+(\S+)',en) + if match: + strval = match.group(3) + return match.group(3) + if flag==1: + return "null" + + def parse_type(self, key, properties): + type_val, type_val_next = properties.split('--------- ------- --------------------- --------------------- --------------') + flag=1 + for en in type_val_next.splitlines(): + if key in en: + flag=0 + match = re.search('^(\S+)\s+(\S+)\s+(\S+)',en) + if match: + strval = match.group(2) + return match.group(2) + if flag==1: + return "null" + + def parse_lineprotocol(self, data): + match = re.search(r'Link Status.*\s(\S+)\s+(\S+)\n', data) + if match: + strval= match.group(2) + return strval.strip('/') + + def parse_operstatus(self, data): + match = re.search(r'Link Status.*\s(\S+)\s+(\S+)\n', data) + if match: + return match.group(1) + + def parse_lldp_intf(self, data): + match = re.search(r'^([A-Za-z0-9/]*)', data) + if match: + return match.group(1) + + def parse_lldp_host(self, data): + match = re.search(r'^([A-Za-z0-9]*)', data) + if match: + return match.group(1) + + def parse_lldp_port(self, data): + match = re.search(r'^([A-Za-z0-9/]*)', data) + if match: + return match.group(1) + + +FACT_SUBSETS = dict( + default=Default, + hardware=Hardware, + interfaces=Interfaces, + config=Config, +) + +VALID_SUBSETS = frozenset(FACT_SUBSETS.keys()) + +def main(): + spec = dict( + gather_subset=dict(default=['!config'], type='list') + ) + module = NetworkModule(argument_spec=spec, supports_check_mode=True) + + gather_subset = module.params['gather_subset'] + + runable_subsets = set() + exclude_subsets = set() + + for subset in gather_subset: + if subset == 'all': + runable_subsets.update(VALID_SUBSETS) + continue + + if subset.startswith('!'): + subset = subset[1:] + if subset == 'all': + exclude_subsets.update(VALID_SUBSETS) + continue + exclude = True + else: + exclude = False + + if subset not in VALID_SUBSETS: + module.fail_json(msg='Bad subset') + + if exclude: + exclude_subsets.add(subset) + else: + runable_subsets.add(subset) + + if not runable_subsets: + runable_subsets.update(VALID_SUBSETS) + + runable_subsets.difference_update(exclude_subsets) + runable_subsets.add('default') + + facts = dict() + facts['gather_subset'] = list(runable_subsets) + + runner = CommandRunner(module) + instances = list() + for key in runable_subsets: + instances.append(FACT_SUBSETS[key](runner)) + runner.run() + + try: + for inst in instances: + inst.populate() + facts.update(inst.facts) + except Exception: + module.exit_json(out=module.from_json(runner.items)) + + ansible_facts = dict() + for key, value in facts.iteritems(): + key = 'ansible_net_%s' % key + ansible_facts[key] = value + + module.exit_json(ansible_facts=ansible_facts) + + +if __name__ == '__main__': + main() + From 0957a72872487ff5d07422d6ccae453075a37de2 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 17 Sep 2016 10:23:45 -0700 Subject: [PATCH 356/770] Updated the copyright --- network/dellos6/dellos6_facts.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/network/dellos6/dellos6_facts.py b/network/dellos6/dellos6_facts.py index 922b14366e4..b3f5156d3aa 100644 --- a/network/dellos6/dellos6_facts.py +++ b/network/dellos6/dellos6_facts.py @@ -1,5 +1,9 @@ # !/usr/bin/python # +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# # This file is part of Ansible # # Ansible is free software: you can redistribute it and/or modify From c8b241c521aa47fbcae2c43b0261c732119a0f82 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 17 Sep 2016 10:35:29 -0700 Subject: [PATCH 357/770] fixing the CI issue, the python interperter line --- network/dellos6/dellos6_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/dellos6/dellos6_facts.py b/network/dellos6/dellos6_facts.py index b3f5156d3aa..4ab90f39214 100644 --- a/network/dellos6/dellos6_facts.py +++ b/network/dellos6/dellos6_facts.py @@ -1,4 +1,4 @@ -# !/usr/bin/python +#!/usr/bin/python # # (c) 2015 Peter Sprygada, # From 992eda79626ff503f718549c70527fd2c2709074 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 17 Sep 2016 11:05:31 -0700 Subject: [PATCH 358/770] Added support for template module for Dell OS6 devices --- network/dellos6/dellos6_template.py | 174 ++++++++++++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 network/dellos6/dellos6_template.py diff --git a/network/dellos6/dellos6_template.py b/network/dellos6/dellos6_template.py new file mode 100644 index 00000000000..a1209b46d98 --- /dev/null +++ b/network/dellos6/dellos6_template.py @@ -0,0 +1,174 @@ +#!/usr/bin/python +# +# (c) 2015 Peter Sprygada, +# +# Copyright (c) 2016 Dell Inc. +# +# 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: dellos6_template +version_added: "2.2" +author: "Dhivya P (@dhivyap)" +short_description: Manage Dell OS6 device configurations over SSH. +description: + - Manages Dell OS6 network device configurations over SSH. This module + allows implementors to work with the device running-config. It + provides a way to push a set of commands onto a network device + by evaluating the current running-config and only pushing configuration + commands that are not already configured. The config source can + be a set of commands or a template. +extends_documentation_fragment: dellos6 +options: + src: + description: + - The path to the config source. The source can be either a + file with config or a template that will be merged during + runtime. By default the task will first search for the source + file in role or playbook root folder in templates unless a full + path to the file is given. + required: true + force: + description: + - The force argument instructs the module not to consider the + current device running-config. When set to true, this will + cause the module to push the contents of I(src) into the device + without first checking if already configured. This argument is + mutually exclusive with I(config). + required: false + default: false + choices: [ "true", "false" ] + backup: + description: + - When this argument is configured true, the module will backup + the running-config from the node prior to making any changes. + The backup file will be written to backup_{{ hostname }} in + the root of the playbook directory. This argument is + mutually exclusive with I(config). + required: false + default: false + choices: [ "true", "false" ] + config: + description: + - The module, by default, will connect to the remote device and + retrieve the current running-config to use as a base for comparing + against the contents of source. There are times when it is not + desirable to have the task get the current running-config for + every task. The I(config) argument allows the implementer to + pass in the configuration to use as the base config for + comparison. This argument is mutually exclusive with + I(force) and I(backup). + required: false + default: null +""" + +EXAMPLES = """ +- name: push a configuration onto the device + dellos6_template: + host: hostname + username: foo + src: config.j2 + +- name: forceable push a configuration onto the device + dellos6_template: + host: hostname + username: foo + src: config.j2 + force: yes + +- name: provide the base configuration for comparison + dellos6_template: + host: hostname + username: foo + src: candidate_config.txt + config: current_config.txt +""" + +RETURN = """ +updates: + description: The set of commands that will be pushed to the remote device + returned: always + type: list + sample: ['...', '...'] + +_backup: + description: The current running config of the remote device. + returned: when running config is present in the remote device. + type: list + sample: ['...', '...'] + +responses: + description: The set of responses from issuing the commands on the device + returned: when not check_mode + type: list + sample: ['...', '...'] +""" +from ansible.module_utils.netcfg import dumps +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.dellos6 import Dellos6NetworkConfig + + +def get_config(module): + config = module.params['config'] or dict() + if not config and not module.params['force']: + config = module.config.get_config() + return config + + +def main(): + """ main entry point for module execution + """ + + argument_spec = dict( + src=dict(), + force=dict(default=False, type='bool'), + backup=dict(default=False, type='bool'), + config=dict(), + ) + mutually_exclusive = [('config', 'backup'), ('config', 'force')] + + module = NetworkModule(argument_spec=argument_spec, + mutually_exclusive=mutually_exclusive, + supports_check_mode=True) + + result = dict(changed=False) + candidate = Dellos6NetworkConfig(contents=module.params['src'], indent=0) + + contents = get_config(module) + if contents: + config = Dellos6NetworkConfig(contents=contents[0], indent=0) + result['_backup'] = contents[0] + commands = list() + + if not module.params['force']: + commands = dumps(candidate.difference(config), 'commands') + else: + commands = str(candidate) + + if commands: + commands = commands.split('\n') + if not module.check_mode: + response = module.config(commands) + result['responses'] = response + result['changed'] = True + + result['updates'] = commands + module.exit_json(**result) + + +if __name__ == '__main__': + main() From b3e69e0f34156601bb46ba3a589d411e95e7c851 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 17 Sep 2016 11:16:47 -0700 Subject: [PATCH 359/770] Updated the config module to use the new Parse method for OS6 --- network/dellos6/dellos6_config.py | 39 +++++++++++-------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/network/dellos6/dellos6_config.py b/network/dellos6/dellos6_config.py index 008cb0122e8..153e1365241 100644 --- a/network/dellos6/dellos6_config.py +++ b/network/dellos6/dellos6_config.py @@ -192,12 +192,13 @@ sample: True """ -from ansible.module_utils.netcfg import NetworkConfig, dumps, ConfigLine +from ansible.module_utils.netcfg import dumps from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dellos6 import get_config +from ansible.module_utils.dnos6 import get_config, get_sublevel_config, Dellos6NetworkConfig + def get_candidate(module): - candidate = NetworkConfig(indent=1) + candidate = Dellos6NetworkConfig(indent=0) if module.params['src']: candidate.load(module.params['src']) elif module.params['lines']: @@ -205,22 +206,9 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def get_contents(other,module): - contents =list() - - parent = ''.join(module.params['parents']) - start = False - for item in other.items: - if item.text == parent: - start = True - elif item.text != 'exit' and start: - contents.append(item.text) - elif item.text == 'exit' and start: - start = False - break - return contents def main(): + argument_spec = dict( lines=dict(aliases=['commands'], type='list'), parents=dict(type='list'), @@ -250,45 +238,44 @@ def main(): match = module.params['match'] replace = module.params['replace'] - before = module.params['before'] result = dict(changed=False, saved=False) candidate = get_candidate(module) - if module.params['match'] != 'none': + if match != 'none': config = get_config(module) if parents: - con = get_contents(config,module) - config = NetworkConfig(indent=1) - config.add(con,parents=module.params['parents']) + config = get_sublevel_config(config, module) configobjs = candidate.difference(config, match=match, replace=replace) else: configobjs = candidate.items if module.params['backup']: result['__backup__'] = module.cli('show running-config')[0] + commands = list() if configobjs: commands = dumps(configobjs, 'commands') commands = commands.split('\n') + if module.params['before']: - commands[:0] = before + commands[:0] = module.params['before'] + if module.params['after']: commands.extend(module.params['after']) + if not module.check_mode and module.params['update'] == 'merge': response = module.config.load_config(commands) result['responses'] = response - + if module.params['save']: module.config.save_config() result['saved'] = True - result['changed'] = True result['updates'] = commands module.exit_json(**result) - if __name__ == '__main__': main() From 8886afd145bfd05ebf4ee4975b34f60645d98664 Mon Sep 17 00:00:00 2001 From: nichivo Date: Mon, 19 Sep 2016 16:56:29 +1000 Subject: [PATCH 360/770] Insert missing option line before blank lines (#4747) --- files/ini_file.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 3b9cceecf60..2fc3d96f5f3 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -155,8 +155,12 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese if within_section: if state == 'present': # insert missing option line at the end of the section - ini_lines.insert(index, assignment_format % (option, value)) - changed = True + for i in range(index, 0, -1): + # search backwards for previous non-blank or non-comment line + if not re.match(r'^[ \t]*([#;].*)?$', ini_lines[i - 1]): + ini_lines.insert(i, assignment_format % (option, value)) + changed = True + break elif state == 'absent' and not option: # remove the entire section del ini_lines[section_start:index] From 7362b8d08f5750aa8bec2eaf1d91a9ce1c216a63 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Mon, 19 Sep 2016 00:50:28 -0700 Subject: [PATCH 361/770] Addressed review comments, given as part of other reviews (#4904) --- network/dellos10/dellos10_config.py | 11 +++++------ network/dellos6/dellos6_config.py | 2 +- network/dellos9/dellos9_command.py | 11 ++++++----- network/dellos9/dellos9_config.py | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/network/dellos10/dellos10_config.py b/network/dellos10/dellos10_config.py index e505799e0e8..1d976f011f1 100644 --- a/network/dellos10/dellos10_config.py +++ b/network/dellos10/dellos10_config.py @@ -180,21 +180,21 @@ responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] saved: - description: Returns whether the configuration is saved to the startup + description: Returns whether the configuration is saved to the startup configuration or not. - retured: when not check_mode + returned: when not check_mode type: bool sample: True """ from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dellos10 import get_config, get_sublevel_config +from ansible.module_utils.dellos10 import get_config, get_sublevel_config def get_candidate(module): candidate = NetworkConfig(indent=1) @@ -220,11 +220,10 @@ def main(): match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), - update=dict(choices=['merge', 'check'], default='merge'), save=dict(type='bool', default=False), config=dict(), - backup =dict(type='bool', default=False) + backup=dict(type='bool', default=False) ) mutually_exclusive = [('lines', 'src')] diff --git a/network/dellos6/dellos6_config.py b/network/dellos6/dellos6_config.py index 153e1365241..1f4c4417356 100644 --- a/network/dellos6/dellos6_config.py +++ b/network/dellos6/dellos6_config.py @@ -194,7 +194,7 @@ """ from ansible.module_utils.netcfg import dumps from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dnos6 import get_config, get_sublevel_config, Dellos6NetworkConfig +from ansible.module_utils.dellos6 import get_config, get_sublevel_config, Dellos6NetworkConfig def get_candidate(module): diff --git a/network/dellos9/dellos9_command.py b/network/dellos9/dellos9_command.py index b6bc6ed9d70..a36c83a7b72 100755 --- a/network/dellos9/dellos9_command.py +++ b/network/dellos9/dellos9_command.py @@ -24,6 +24,7 @@ --- module: dellos9_command version_added: "2.2" +author: "Dhivya P (@dhivyap)" short_description: Run commands on remote devices running Dell OS9 description: - Sends arbitrary commands to a Dell OS9 node and returns the results @@ -83,22 +84,22 @@ - name: run show version on remote devices dellos9_command: commands: show version - provider "{{ cli }}" + provider: "{{ cli }}" - name: run show version and check to see if output contains OS9 dellos9_command: commands: show version wait_for: result[0] contains OS9 - provider "{{ cli }}" + provider: "{{ cli }}" - name: run multiple commands on remote nodes dellos9_command: commands: - show version - show interfaces - provider "{{ cli }}" + provider: "{{ cli }}" - - name: run multiple commands and evalute the output + - name: run multiple commands and evaluate the output dellos9_command: commands: - show version @@ -106,7 +107,7 @@ wait_for: - result[0] contains OS9 - result[1] contains Loopback - provider "{{ cli }}" + provider: "{{ cli }}" """ RETURN = """ diff --git a/network/dellos9/dellos9_config.py b/network/dellos9/dellos9_config.py index a90531b0f6c..1d728c5319b 100755 --- a/network/dellos9/dellos9_config.py +++ b/network/dellos9/dellos9_config.py @@ -180,14 +180,14 @@ responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] saved: description: Returns whether the configuration is saved to the startup configuration or not. - retured: when not check_mode + returned: when not check_mode type: bool sample: True From 1df235f8ca5e1d52f922548265aed019ffbc330d Mon Sep 17 00:00:00 2001 From: Harnek Sidhu Date: Mon, 19 Sep 2016 03:54:14 -0400 Subject: [PATCH 362/770] Created digital_ocean_block_storage module (#4469) --- .../digital_ocean_block_storage.py | 336 ++++++++++++++++++ 1 file changed, 336 insertions(+) create mode 100644 cloud/digital_ocean/digital_ocean_block_storage.py diff --git a/cloud/digital_ocean/digital_ocean_block_storage.py b/cloud/digital_ocean/digital_ocean_block_storage.py new file mode 100644 index 00000000000..3dc04424bf5 --- /dev/null +++ b/cloud/digital_ocean/digital_ocean_block_storage.py @@ -0,0 +1,336 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# 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 . + +import json +import time + +DOCUMENTATION = ''' +--- +module: digital_ocean_block_storage +short_description: Create/destroy or attach/detach Block Storage volumes in DigitalOcean +description: + - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet. +version_added: "2.2" +author: "Harnek Sidhu" +options: + command: + description: + - Which operation do you want to perform. + choices: ['create', 'attach'] + required: true + state: + description: + - Indicate desired state of the target. + choices: ['present', 'absent'] + required: true + api_token: + description: + - DigitalOcean api token. + required: true + block_size: + description: + - The size of the Block Storage volume in gigabytes. Required when command=create and state=present. + volume_name: + description: + - The name of the Block Storage volume. + required: true + description: + description: + - Description of the Block Storage volume. + region: + description: + - The slug of the region where your Block Storage volume should be located in. + required: true + droplet_id: + description: + - The droplet id you want to operate on. Required when command=attach. + timeout: + description: + - The timeout in seconds used for polling DigitalOcean's API. + default: 10 + +notes: + - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. + They both refer to the v2 token. + +author: + - "Harnek Sidhu (github: @harneksidhu)" +''' + +EXAMPLES = ''' +# Create new Block Storage +- digital_ocean_block_storage: + state: present + command: create + api_token: + region: nyc1 + block_size: 10 + volume_name: nyc1-block-storage +# Delete Block Storage +- digital_ocean_block_storage: + state: absent + command: create + api_token: + region: nyc1 + volume_name: nyc1-block-storage +# Attach Block Storage to a Droplet +- digital_ocean_block_storage: + state: present + command: attach + api_token: + volume_name: nyc1-block-storage + region: nyc1 + droplet_id: +# Detach Block Storage from a Droplet +- digital_ocean_block_storage: + state: absent + command: attach + api_token: + volume_name: nyc1-block-storage + region: nyc1 + droplet_id: +''' + +RETURN = ''' +id: + description: Unique identifier of a Block Storage volume returned during creation. + returned: changed + type: string + sample: "69b25d9a-494c-12e6-a5af-001f53126b44" +''' + +class DOBlockStorageException(Exception): + pass + +class Response(object): + + def __init__(self, resp, info): + self.body = None + if resp: + self.body = resp.read() + self.info = info + + @property + def json(self): + if self.body: + return json.loads(self.body) + elif "body" in self.info: + return json.loads(self.info["body"]) + else: + return None + + @property + def status_code(self): + return self.info["status"] + +class Rest(object): + + def __init__(self, module, headers): + self.module = module + self.headers = headers + self.baseurl = 'https://api.digitalocean.com/v2' + + def _url_builder(self, path): + if path[0] == '/': + path = path[1:] + return '%s/%s' % (self.baseurl, path) + + def send(self, method, path, data=None, headers=None): + url = self._url_builder(path) + data = self.module.jsonify(data) + + resp, info = fetch_url(self.module, url, data=data, headers=self.headers, method=method) + + return Response(resp, info) + + def get(self, path, data=None, headers=None): + return self.send('GET', path, data, headers) + + def put(self, path, data=None, headers=None): + return self.send('PUT', path, data, headers) + + def post(self, path, data=None, headers=None): + return self.send('POST', path, data, headers) + + def delete(self, path, data=None, headers=None): + return self.send('DELETE', path, data, headers) + +class DOBlockStorage(object): + + def __init__(self, module): + api_token = module.params['api_token'] or \ + os.environ['DO_API_TOKEN'] or os.environ['DO_API_KEY'] + self.module = module + self.rest = Rest(module, {'Authorization': 'Bearer {}'.format(api_token), + 'Content-type': 'application/json'}) + + def get_key_or_fail(self, k): + v = self.module.params[k] + if v is None: + self.module.fail_json(msg='Unable to load %s' % k) + return v + + def poll_action_for_complete_status(self, action_id): + url = 'actions/{}'.format(action_id) + end_time = time.time() + self.module.params['timeout'] + while time.time() < end_time: + time.sleep(2) + response = self.rest.get(url) + status = response.status_code + json = response.json + if status == 200: + if json['action']['status'] == 'completed': + return True + elif json['action']['status'] == 'errored': + raise DOBlockStorageException(json['message']) + raise DOBlockStorageException('Unable to reach api.digitalocean.com') + + def get_attached_droplet_ID(self, volume_name, region): + url = 'volumes?name={}®ion={}'.format(volume_name, region) + response = self.rest.get(url) + status = response.status_code + json = response.json + if status == 200: + volumes = json['volumes'] + if len(volumes)>0: + droplet_ids = volumes[0]['droplet_ids'] + if len(droplet_ids)>0: + return droplet_ids[0] + return None + else: + raise DOBlockStorageException(json['message']) + + def attach_detach_block_storage(self, method, volume_name, region, droplet_id): + data = { + 'type' : method, + 'volume_name' : volume_name, + 'region' : region, + 'droplet_id' : droplet_id + } + response = self.rest.post('volumes/actions', data=data) + status = response.status_code + json = response.json + if status == 202: + return self.poll_action_for_complete_status(json['action']['id']) + elif status == 200: + return True + elif status == 422: + return False + else: + raise DOBlockStorageException(json['message']) + + def create_block_storage(self): + block_size = self.get_key_or_fail('block_size') + volume_name = self.get_key_or_fail('volume_name') + region = self.get_key_or_fail('region') + description = self.module.params['description'] + data = { + 'size_gigabytes' : block_size, + 'name' : volume_name, + 'description' : description, + 'region' : region + } + response = self.rest.post("volumes", data=data) + status = response.status_code + json = response.json + if status == 201: + self.module.exit_json(changed=True, id=json['volume']['id']) + elif status == 409 and json['id'] == 'already_exists': + self.module.exit_json(changed=False) + else: + raise DOBlockStorageException(json['message']) + + def delete_block_storage(self): + volume_name = self.get_key_or_fail('volume_name') + region = self.get_key_or_fail('region') + url = 'volumes?name={}®ion={}'.format(volume_name, region) + attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) + if attached_droplet_id != None: + self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) + response = self.rest.delete(url) + status = response.status_code + json = response.json + if status == 204: + self.module.exit_json(changed=True) + elif status == 404: + self.module.exit_json(changed=False) + else: + raise DOBlockStorageException(json['message']) + + def attach_block_storage(self): + volume_name = self.get_key_or_fail('volume_name') + region = self.get_key_or_fail('region') + droplet_id = self.get_key_or_fail('droplet_id') + attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) + if attached_droplet_id != None: + if attached_droplet_id==droplet_id: + self.module.exit_json(changed=False) + else: + self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) + changed_status = self.attach_detach_block_storage('attach', volume_name, region, droplet_id) + self.module.exit_json(changed=changed_status) + + def detach_block_storage(self): + volume_name = self.get_key_or_fail('volume_name') + region = self.get_key_or_fail('region') + droplet_id = self.get_key_or_fail('droplet_id') + changed_status = self.attach_detach_block_storage('detach', volume_name, region, droplet_id) + self.module.exit_json(changed=changed_status) + +def handle_request(module): + block_storage = DOBlockStorage(module) + command = module.params['command'] + state = module.params['state'] + if command == 'create': + if state == 'present': + block_storage.create_block_storage() + elif state == 'absent': + block_storage.delete_block_storage() + elif command == 'attach': + if state =='present': + block_storage.attach_block_storage() + elif state == 'absent': + block_storage.detach_block_storage() + +def main(): + module = AnsibleModule( + argument_spec=dict( + state = dict(choices=['present', 'absent'], required=True), + command = dict(choices=['create', 'attach'], required=True), + api_token = dict(aliases=['API_TOKEN'], no_log=True), + block_size = dict(type='int'), + volume_name = dict(type='str', required=True), + description = dict(type='str'), + region = dict(type='str', required=True), + droplet_id = dict(type='int'), + timeout = dict(type='int', default=10), + ), + ) + try: + handle_request(module) + except DOBlockStorageException: + e = get_exception() + module.fail_json(msg=e.message) + except KeyError: + e = get_exception() + module.fail_json(msg='Unable to load %s' % e.message) + +from ansible.module_utils.basic import * +from ansible.module_utils.urls import * +if __name__ == '__main__': + main() \ No newline at end of file From 5174e3f31fdaeceb438526a2f12d6bbcdd1d6574 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 19 Sep 2016 14:27:00 +0200 Subject: [PATCH 363/770] Fixing nxos_portchannel --- network/nxos/nxos_portchannel.py | 34 +++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index ab07385effc..b6654c3db64 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -18,7 +18,6 @@ DOCUMENTATION = ''' --- - module: nxos_portchannel version_added: "2.2" short_description: Manages port-channel interfaces. @@ -107,14 +106,14 @@ "Ethernet2/5": {"mode": "on", "status": "D"}, "Ethernet2/6": {"mode": "on", "status": "D"}}, "min_links": null, "mode": "on"} -commands: - description: command string sent to the device +updates: + description: command sent to the device returned: always - type: string - sample: "interface Ethernet2/6 ; no channel-group 12 ; - interface Ethernet2/5 ; no channel-group 12 ; - interface Ethernet2/6 ; channel-group 12 mode on ; - interface Ethernet2/5 ; channel-group 12 mode on ;" + type: list + sample: ["interface Ethernet2/6", "no channel-group 12", + "interface Ethernet2/5", "no channel-group 12", + "interface Ethernet2/6", "channel-group 12 mode on", + "interface Ethernet2/5", "channel-group 12 mode on" changed: description: check to see if a change was made on the device returned: always @@ -330,12 +329,24 @@ def execute_config_command(commands, module): 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') + output = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) return output def get_cli_body_ssh(command, response, module): try: - body = [json.loads(response[0])] + if isinstance(response[0], str): + body = [json.loads(response[0])] + else: + body = response except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -714,12 +725,13 @@ def main(): output = execute_config_command(cmds, module) changed = True end_state, interface_exist = get_existing(module, args) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed results['existing'] = existing results['end_state'] = end_state - results['state'] = state results['updates'] = cmds results['changed'] = changed @@ -730,4 +742,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From d632cce54603356e7d7fb978d7e3a820edc1d430 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 19 Sep 2016 14:35:51 +0200 Subject: [PATCH 364/770] Fixed docstring --- network/nxos/nxos_portchannel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index b6654c3db64..68af16c8f1d 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -113,7 +113,7 @@ sample: ["interface Ethernet2/6", "no channel-group 12", "interface Ethernet2/5", "no channel-group 12", "interface Ethernet2/6", "channel-group 12 mode on", - "interface Ethernet2/5", "channel-group 12 mode on" + "interface Ethernet2/5", "channel-group 12 mode on"] changed: description: check to see if a change was made on the device returned: always From 4ff0fd39107cb13f7c46f765eefe186402d1391e Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 10:53:43 -0400 Subject: [PATCH 365/770] eos_eapi module allows independent configuration of protocol and port The eos_eapi module would not configure the port if the protocol wasn't configured as reported in #4905. This changes the behavior to now allow the port to be configured independently fixes #4905 --- network/eos/eos_eapi.py | 63 +++++++++++++++++++++++++++-------------- 1 file changed, 41 insertions(+), 22 deletions(-) diff --git a/network/eos/eos_eapi.py b/network/eos/eos_eapi.py index 79d1c541eec..cd08cca12c8 100644 --- a/network/eos/eos_eapi.py +++ b/network/eos/eos_eapi.py @@ -182,9 +182,11 @@ import re import time -from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.eos import NetworkModule, NetworkError +import ansible.module_utils.eos + from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig, dumps PRIVATE_KEYS_RE = re.compile('__.+__') @@ -194,7 +196,24 @@ def invoke(name, *args, **kwargs): if func: return func(*args, **kwargs) -def started(module, commands): +def get_instance(module): + try: + resp = module.cli('show management api http-commands', 'json') + return dict( + http=resp[0]['httpServer']['configured'], + http_port=resp[0]['httpServer']['port'], + https=resp[0]['httpsServer']['configured'], + https_port=resp[0]['httpsServer']['port'], + local_http=resp[0]['localHttpServer']['configured'], + local_http_port=resp[0]['localHttpServer']['port'], + socket=resp[0]['unixSocketServer']['configured'], + vrf=resp[0]['vrf'] + ) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) + +def started(module, instance, commands): commands.append('no shutdown') setters = set() for key, value in module.argument_spec.iteritems(): @@ -202,45 +221,45 @@ def started(module, commands): setter = value.get('setter') or 'set_%s' % key if setter not in setters: setters.add(setter) - invoke(setter, module, commands) + invoke(setter, module, instance, commands) -def stopped(module, commands): +def stopped(module, instance, commands): commands.append('shutdown') -def set_protocol_http(module, commands): +def set_protocol_http(module, instance, commands): port = module.params['http_port'] if not 1 <= port <= 65535: module.fail_json(msg='http_port must be between 1 and 65535') - elif module.params['http'] is True: + elif any((module.params['http'], instance['http'])): commands.append('protocol http port %s' % port) elif module.params['http'] is False: commands.append('no protocol http') -def set_protocol_https(module, commands): +def set_protocol_https(module, instance, commands): port = module.params['https_port'] if not 1 <= port <= 65535: module.fail_json(msg='https_port must be between 1 and 65535') - elif module.params['https'] is True: + elif any((module.params['https'], instance['https'])): commands.append('protocol https port %s' % port) elif module.params['https'] is False: commands.append('no protocol https') -def set_local_http(module, commands): +def set_local_http(module, instance, commands): port = module.params['local_http_port'] if not 1 <= port <= 65535: module.fail_json(msg='local_http_port must be between 1 and 65535') - elif module.params['local_http'] is True: + elif any((module.params['local_http'], instance['local_http'])): commands.append('protocol http localhost port %s' % port) elif module.params['local_http'] is False: commands.append('no protocol http localhost port 8080') -def set_socket(module, commands): - if module.params['socket'] is True: +def set_socket(module, instance, commands): + if any((module.params['socket'], instance['socket'])): commands.append('protocol unix-socket') elif module.params['socket'] is False: commands.append('no protocol unix-socket') -def set_vrf(module, commands): +def set_vrf(module, instance, commands): vrf = module.params['vrf'] if vrf != 'default': resp = module.cli(['show vrf']) @@ -256,14 +275,14 @@ def get_config(module): config = NetworkConfig(indent=3, contents=contents[0]) return config -def load_config(module, commands, result): +def load_config(module, instance, commands, result): commit = not module.check_mode diff = module.config.load_config(commands, commit=commit) if diff: result['diff'] = dict(prepared=diff) result['changed'] = True -def load(module, commands, result): +def load(module, instance, commands, result): candidate = NetworkConfig(indent=3) candidate.add(commands, parents=['management api http-commands']) @@ -273,7 +292,7 @@ def load(module, commands, result): if configobjs: commands = dumps(configobjs, 'commands').split('\n') result['updates'] = commands - load_config(module, commands, result) + load_config(module, instance, commands, result) def clean_result(result): # strip out any keys that have two leading and two trailing @@ -326,15 +345,15 @@ def main(): state = module.params['state'] - warnings = list() - - result = dict(changed=False, warnings=warnings) + result = dict(changed=False) commands = list() - invoke(state, module, commands) + instance = get_instance(module) + + invoke(state, module, instance, commands) try: - load(module, commands, result) + load(module, instance, commands, result) except NetworkError: exc = get_exception() module.fail_json(msg=str(exc), **exc.kwargs) From 7173162c5126e6d9a42e26a977cb6a2104a5dcf7 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 19 Sep 2016 17:02:55 +0200 Subject: [PATCH 366/770] Fix duplicate required key error ansible-doc -vvvv -l show this warning: [WARNING]: While constructing a mapping from /home/misc/checkout/git/ansible/lib/ansible/modules/core/network/junos/junos_config.py, line 88, column 5, found a duplicate dict key (required). Using last defined value only. --- network/junos/junos_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 2a930037dc6..4bd4b360f71 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -112,7 +112,6 @@ the equivalent, set the I(update) argument to C(replace). This argument will be removed in a future release. required: false - required: true choices: ['yes', 'no'] default: false backup: From 2e357e262c566b39ebf13b1908b7d15fa22a086e Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 12:32:37 -0400 Subject: [PATCH 367/770] adds exception handling to nxos_command for FailedConditionalError This adds exception handling as per ansible/ansible#17638 to the nxos_command module. --- network/nxos/nxos_command.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/network/nxos/nxos_command.py b/network/nxos/nxos_command.py index bcd7566dce6..9dc01a913c4 100644 --- a/network/nxos/nxos_command.py +++ b/network/nxos/nxos_command.py @@ -148,10 +148,14 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.nxos + from ansible.module_utils.basic import get_exception -from ansible.module_utils.netcli import CommandRunner, FailedConditionsError +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.netcli import FailedConditionsError +from ansible.module_utils.netcli import FailedConditionalError from ansible.module_utils.netcli import AddCommandError -from ansible.module_utils.nxos import NetworkModule, NetworkError VALID_KEYS = ['command', 'output', 'prompt', 'response'] @@ -186,7 +190,6 @@ def main(): ) module = NetworkModule(argument_spec=spec, - connect_on_load=False, supports_check_mode=True) commands = list(parse_commands(module)) @@ -223,9 +226,12 @@ def main(): except FailedConditionsError: exc = get_exception() module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except FailedConditionalError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditional=exc.failed_conditional) except NetworkError: exc = get_exception() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), **exc.kwargs) result = dict(changed=False) From 3714b30a9fbb8b61a86a796e97b35772159d1987 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 12:25:32 -0400 Subject: [PATCH 368/770] bug fix that now catches FailedConditionalError when specifying conditionals This is related to ansible/ansible#17638 and updates the eos_command module to catch the exception and return a santatized error. --- network/eos/eos_command.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index 911f0919364..b0ab3286b6f 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -143,10 +143,15 @@ type: list sample: ['...', '...'] """ + +import ansible.module_utils.eos + from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner -from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.eos import NetworkModule, NetworkError +from ansible.module_utils.netcli import AddCommandError +from ansible.module_utils.netcli import FailedConditionsError +from ansible.module_utils.netcli import FailedConditionalError VALID_KEYS = ['command', 'output', 'prompt', 'response'] @@ -182,7 +187,6 @@ def main(): ) module = NetworkModule(argument_spec=spec, - connect_on_load=False, supports_check_mode=True) commands = list(parse_commands(module)) @@ -219,9 +223,12 @@ def main(): except FailedConditionsError: exc = get_exception() module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except FailedConditionalError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditional=exc.failed_conditional) except NetworkError: exc = get_exception() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), **exc.kwargs) result = dict(changed=False, stdout=list()) From 169b722d73068dbc53aa8c585706ac8ad8b62f25 Mon Sep 17 00:00:00 2001 From: GGabriele Date: Mon, 19 Sep 2016 18:50:05 +0200 Subject: [PATCH 369/770] Add feature mappings --- network/nxos/nxos_feature.py | 34 ++++++++++++++++++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index b487f6cae37..8b9b831deb1 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -247,6 +247,15 @@ def execute_config_command(commands, module): 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): @@ -381,11 +390,30 @@ def validate_feature(module, mode='show'): feature_to_be_mapped = { 'show': { 'nv overlay': 'nve', - 'vn-segment-vlan-based': 'vnseg_vlan'}, + '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'}, 'config': { 'nve': 'nv overlay', - 'vnseg_vlan': 'vn-segment-vlan-based'} + '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', + } } if feature in feature_to_be_mapped[mode]: @@ -434,6 +462,8 @@ def main(): 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 From 27d2950e2d97751d20b6d28a73884e87a9bdee1b Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 12:35:37 -0700 Subject: [PATCH 370/770] Remove Travis config since we only use Shippable. (#4916) --- .travis.yml | 28 ---------------------------- 1 file changed, 28 deletions(-) delete mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index abc512d43b2..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -sudo: false -language: python -python: - - "2.7" -addons: - apt: - sources: - - deadsnakes - packages: - - python2.4 - - python2.6 - - python3.5 -before_install: - - git config user.name "ansible" - - git config user.email "ansible@ansible.com" - - if [[ "$TRAVIS_PULL_REQUEST" != "false" ]]; then git rebase $TRAVIS_BRANCH; fi; -install: - - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing -script: - - python2.4 -m compileall -fq -x 'cloud/' . - - python2.4 -m compileall -fq cloud/amazon/_ec2_ami_search.py cloud/amazon/ec2_facts.py - - python2.6 -m compileall -fq . - - python2.7 -m compileall -fq . - - python3.5 -m compileall -fq . - - ansible-validate-modules --exclude 'utilities/' . - #- ansible-validate-modules --exclude 'cloud/amazon/ec2_lc\.py|cloud/amazon/ec2_scaling_policy\.py|cloud/amazon/ec2_scaling_policy\.py|cloud/amazon/ec2_asg\.py|cloud/azure/azure\.py|packaging/os/rhn_register\.py|network/openswitch/ops_template\.py|system/hostname\.py|utilities/' . - #- ./test-docs.sh core From f2c2dddc0197ee4a6c9ca7789d10b337a098035e Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 15:01:48 -0700 Subject: [PATCH 371/770] Remove duplicate keys from module docs. (#4920) --- cloud/digital_ocean/digital_ocean_block_storage.py | 1 - network/junos/junos_config.py | 1 - network/nxos/nxos_acl.py | 1 - 3 files changed, 3 deletions(-) diff --git a/cloud/digital_ocean/digital_ocean_block_storage.py b/cloud/digital_ocean/digital_ocean_block_storage.py index 3dc04424bf5..0087942323b 100644 --- a/cloud/digital_ocean/digital_ocean_block_storage.py +++ b/cloud/digital_ocean/digital_ocean_block_storage.py @@ -26,7 +26,6 @@ description: - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet. version_added: "2.2" -author: "Harnek Sidhu" options: command: description: diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 2a930037dc6..4bd4b360f71 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -112,7 +112,6 @@ the equivalent, set the I(update) argument to C(replace). This argument will be removed in a future release. required: false - required: true choices: ['yes', 'no'] default: false backup: diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index 11117c45d08..d2e13a3d831 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -95,7 +95,6 @@ keyword 'any'. required: false default: null - default: null dest_port_op: description: - Destination port operands such as eq, neq, gt, lt, range. From 600228ca7f82c1a1eefae18a6928b7d28bf0ce70 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 19 Sep 2016 16:59:27 -0700 Subject: [PATCH 372/770] Test module docs on Shippable. (#4921) --- shippable.yml | 2 + test/utils/shippable/docs-requirements.txt | 2 + test/utils/shippable/docs.sh | 55 ++++++++++++++++++++++ 3 files changed, 59 insertions(+) create mode 100644 test/utils/shippable/docs-requirements.txt create mode 100755 test/utils/shippable/docs.sh diff --git a/shippable.yml b/shippable.yml index a2c4e33585c..a8dd0fc5a9f 100644 --- a/shippable.yml +++ b/shippable.yml @@ -29,6 +29,8 @@ matrix: - env: TEST=integration PLATFORM=osx VERSION=10.11 - env: TEST=sanity INSTALL_DEPS=1 + + - env: TEST=docs build: pre_ci_boot: options: "--privileged=false --net=bridge" diff --git a/test/utils/shippable/docs-requirements.txt b/test/utils/shippable/docs-requirements.txt new file mode 100644 index 00000000000..4e859bb8c71 --- /dev/null +++ b/test/utils/shippable/docs-requirements.txt @@ -0,0 +1,2 @@ +jinja2 +pyyaml diff --git a/test/utils/shippable/docs.sh b/test/utils/shippable/docs.sh new file mode 100755 index 00000000000..9b5a6164f64 --- /dev/null +++ b/test/utils/shippable/docs.sh @@ -0,0 +1,55 @@ +#!/bin/bash -eux + +set -o pipefail + +ansible_repo_url="https://github.com/ansible/ansible.git" + +build_dir="${SHIPPABLE_BUILD_DIR}" +repo="${REPO_NAME}" + +case "${repo}" in + "ansible-modules-core") + this_module_group="core" + other_module_group="extras" + ;; + "ansible-modules-extras") + this_module_group="extras" + other_module_group="core" + ;; + *) + echo "Unsupported repo name: ${repo}" + exit 1 + ;; +esac + +modules_tmp_dir="${build_dir}.tmp" +this_modules_dir="${build_dir}/lib/ansible/modules/${this_module_group}" +other_modules_dir="${build_dir}/lib/ansible/modules/${other_module_group}" + +cd / +mv "${build_dir}" "${modules_tmp_dir}" +git clone "${ansible_repo_url}" "${build_dir}" +cd "${build_dir}" +rmdir "${this_modules_dir}" +mv "${modules_tmp_dir}" "${this_modules_dir}" +mv "${this_modules_dir}/shippable" "${build_dir}" +git submodule init "${other_modules_dir}" +git submodule sync "${other_modules_dir}" +git submodule update "${other_modules_dir}" + +pip install -r lib/ansible/modules/${this_module_group}/test/utils/shippable/docs-requirements.txt --upgrade +pip list + +source hacking/env-setup + +PAGER=/bin/cat \ + ANSIBLE_DEPRECATION_WARNINGS=false \ + bin/ansible-doc -l \ + 2>/tmp/ansible-doc.err + +if [ -s /tmp/ansible-doc.err ]; then + # report warnings as errors + echo "Output from 'ansible-doc -l' on stderr is considered an error:" + cat /tmp/ansible-doc.err + exit 1 +fi From a076309a0f5bb9912392db6ae77f4a8043e2d9cd Mon Sep 17 00:00:00 2001 From: Will Thames Date: Tue, 20 Sep 2016 11:13:23 +1000 Subject: [PATCH 373/770] Check whether yum file or URL install is an upgrade (#4547) Rather than just checking whether a package with the right name is installed, use `local_nvra` to check whether the version/release/arch differs too. Remove `local_name` as it is a shortcut too far. Fixes #3807 Fixes #4529 --- packaging/os/yum.py | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index cc0ad419f2f..45bfd2d5775 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -507,24 +507,11 @@ def local_nvra(module, path): finally: os.close(fd) - return '%s-%s-%s.%s' % (header[rpm.RPMTAG_NAME], + return '%s-%s-%s.%s' % (header[rpm.RPMTAG_NAME], header[rpm.RPMTAG_VERSION], header[rpm.RPMTAG_RELEASE], header[rpm.RPMTAG_ARCH]) - -def local_name(module, path): - """return package name of a local rpm passed in""" - ts = rpm.TransactionSet() - ts.setVSFlags(rpm._RPMVSF_NOSIGNATURES) - fd = os.open(path, os.O_RDONLY) - try: - header = ts.hdrFromFdno(fd) - finally: - os.close(fd) - - return header[rpm.RPMTAG_NAME] - def pkg_to_dict(pkgstr): if pkgstr.strip(): @@ -599,10 +586,10 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): res['msg'] += "No Package file matching '%s' found on system" % spec module.fail_json(**res) - pkg_name = local_name(module, spec) + nvra = local_nvra(module, spec) # look for them in the rpmdb - if is_installed(module, repoq, pkg_name, conf_file, en_repos=en_repos, dis_repos=dis_repos): + if is_installed(module, repoq, nvra, conf_file, en_repos=en_repos, dis_repos=dis_repos): # if they are there, skip it continue pkg = spec @@ -611,8 +598,8 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): elif '://' in spec: # download package so that we can check if it's already installed package = fetch_rpm_from_url(spec, module=module) - pkg_name = local_name(module, package) - if is_installed(module, repoq, pkg_name, conf_file, en_repos=en_repos, dis_repos=dis_repos): + nvra = local_nvra(module, package) + if is_installed(module, repoq, nvra, conf_file, en_repos=en_repos, dis_repos=dis_repos): # if it's there, skip it continue pkg = package From d1bdd883a25e05a9a395db6f87f3f277ce095c74 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 20 Sep 2016 03:19:23 +0200 Subject: [PATCH 374/770] Remove duplicate key 'author' (#4919) From 6950e79cded5ca33bfb9f6cf9c4663b998adddb5 Mon Sep 17 00:00:00 2001 From: Josh Lothian Date: Mon, 19 Sep 2016 21:22:12 -0500 Subject: [PATCH 375/770] Fix YAML syntax in NXOS documentation (#4922) * Fix YAML syntax in NXOS documentation Use ':' and not '=' to ensure valid YAML in the EXAMPLES * Correcting introduced syntax error --- network/nxos/nxos_aaa_server.py | 40 ++++++++++----------- network/nxos/nxos_aaa_server_host.py | 50 +++++++++++++------------- network/nxos/nxos_bgp.py | 8 ++--- network/nxos/nxos_bgp_af.py | 12 +++---- network/nxos/nxos_evpn_global.py | 2 +- network/nxos/nxos_igmp_snooping.py | 12 +++---- network/nxos/nxos_interface_ospf.py | 6 ++-- network/nxos/nxos_mtu.py | 41 +++++++++++----------- network/nxos/nxos_ntp.py | 14 ++++---- network/nxos/nxos_ntp_auth.py | 14 ++++---- network/nxos/nxos_ntp_options.py | 14 ++++---- network/nxos/nxos_ospf.py | 2 +- network/nxos/nxos_overlay_global.py | 2 +- network/nxos/nxos_pim.py | 2 +- network/nxos/nxos_pim_interface.py | 52 ++++++++++++++-------------- network/nxos/nxos_snmp_community.py | 12 +++---- network/nxos/nxos_snmp_contact.py | 10 +++--- network/nxos/nxos_snmp_host.py | 14 ++++---- network/nxos/nxos_snmp_location.py | 22 ++++++------ network/nxos/nxos_snmp_traps.py | 24 ++++++------- network/nxos/nxos_snmp_user.py | 16 ++++----- network/nxos/nxos_static_route.py | 8 ++--- network/nxos/nxos_udld.py | 22 ++++++------ network/nxos/nxos_udld_interface.py | 36 +++++++++---------- network/nxos/nxos_vpc.py | 14 ++++---- network/nxos/nxos_vpc_interface.py | 4 +-- network/nxos/nxos_vrf_af.py | 6 ++-- network/nxos/nxos_vtp_domain.py | 10 +++--- network/nxos/nxos_vtp_password.py | 22 ++++++------ network/nxos/nxos_vtp_version.py | 10 +++--- network/nxos/nxos_vxlan_vtep.py | 12 +++---- network/nxos/nxos_vxlan_vtep_vni.py | 6 ++-- 32 files changed, 260 insertions(+), 259 deletions(-) diff --git a/network/nxos/nxos_aaa_server.py b/network/nxos/nxos_aaa_server.py index 147a9137a71..43cc3425f26 100644 --- a/network/nxos/nxos_aaa_server.py +++ b/network/nxos/nxos_aaa_server.py @@ -86,33 +86,33 @@ # Radius Server Basic settings - name: "Radius Server Basic settings" nxos_aaa_server: - server_type=radius - server_timeout=9 - deadtime=20 - directed_request=enabled - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + server_type: radius + server_timeout: 9 + deadtime: 20 + directed_request: enabled + host: inventory_hostname }} + username: un }} + password: pwd }} # Tacacs Server Basic settings - name: "Tacacs Server Basic settings" nxos_aaa_server: - server_type=tacacs - server_timeout=8 - deadtime=19 - directed_request=disabled - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + server_type: tacacs + server_timeout: 8 + deadtime: 19 + directed_request: disabled + host: inventory_hostname }} + username: un }} + password: pwd }} # Setting Global Key - name: "AAA Server Global Key" nxos_aaa_server: - server_type=radius - global_key=test_key - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + server_type: radius + global_key: test_key + host: inventory_hostname }} + username: un }} + password: pwd }} ''' RETURN = ''' @@ -564,4 +564,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_aaa_server_host.py b/network/nxos/nxos_aaa_server_host.py index ed11ddd1f5f..28f24f86adf 100644 --- a/network/nxos/nxos_aaa_server_host.py +++ b/network/nxos/nxos_aaa_server_host.py @@ -83,38 +83,38 @@ # Radius Server Host Basic settings - name: "Radius Server Host Basic settings" nxos_aaa_server_host: - state=present - server_type=radius - address=1.2.3.4 - acct_port=2084 - host_timeout=10 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + state: present + server_type: radius + address: 1.2.3.4 + acct_port: 2084 + host_timeout: 10 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # Radius Server Host Key Configuration - name: "Radius Server Host Key Configuration" nxos_aaa_server_host: - state=present - server_type=radius - address=1.2.3.4 - key=hello - encrypt_type=7 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + state: present + server_type: radius + address: 1.2.3.4 + key: hello + encrypt_type: 7 + host: inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # TACACS Server Host Configuration - name: "Tacacs Server Host Configuration" nxos_aaa_server_host: - state=present - server_type=tacacs - tacacs_port=89 - host_timeout=10 - address=5.6.7.8 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + state: present + server_type: tacacs + tacacs_port: 89 + host_timeout: 10 + address: 5.6.7.8 + host: inventory_hostname }} + username: un }} + password: pwd }} ''' RETURN = ''' @@ -575,4 +575,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 35eaf04265d..594e48aa9d3 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -288,10 +288,10 @@ EXAMPLES = ''' # configure a simple asn - nxos_bgp: - asn=65535 - vrf=test - router_id=1.1.1.1 - state=present + asn: 65535 + vrf: test + router_id: 1.1.1.1 + state: present username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index cb11733dfd4..728515f70d0 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -242,12 +242,12 @@ EXAMPLES = ''' # configure a simple address-family - nxos_bgp_af: - asn=65535 - vrf=TESTING - afi=ipv4 - safi=unicast - advertise_l2vpn_evpn=true - state=present + asn: 65535 + vrf: TESTING + afi: ipv4 + safi: unicast + advertise_l2vpn_evpn: true + state: present ''' RETURN = ''' diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index 16cec8f30e0..c2bbe4fdce6 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -34,7 +34,7 @@ ''' EXAMPLES = ''' - nxos_evpn_global: - nv_overlay_evpn=true + nv_overlay_evpn: true username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_igmp_snooping.py b/network/nxos/nxos_igmp_snooping.py index f8c6c4af7d8..1e11a9246d5 100644 --- a/network/nxos/nxos_igmp_snooping.py +++ b/network/nxos/nxos_igmp_snooping.py @@ -72,10 +72,10 @@ EXAMPLES = ''' # ensure igmp snooping params supported in this module are in there default state - nxos_igmp_snooping: - state=default - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + state: default + host: inventory_hostname }} + username: un }} + password: pwd }} # ensure following igmp snooping params are in the desired state - nxos_igmp_snooping: @@ -86,8 +86,8 @@ report_supp: true v3_report_supp: true host: "{{ inventory_hostname }}" - username={{ un }} - password={{ pwd }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py index d0898d1600b..34ec566cf4d 100644 --- a/network/nxos/nxos_interface_ospf.py +++ b/network/nxos/nxos_interface_ospf.py @@ -115,9 +115,9 @@ ''' EXAMPLES = ''' - nxos_interface_ospf: - interface=ethernet1/32 - ospf=1 - area=1 + interface: ethernet1/32 + ospf: 1 + area: 1 cost=default username: "{{ un }}" password: "{{ pwd }}" diff --git a/network/nxos/nxos_mtu.py b/network/nxos/nxos_mtu.py index 379eca9cdb5..e8cd0f205ca 100644 --- a/network/nxos/nxos_mtu.py +++ b/network/nxos/nxos_mtu.py @@ -56,34 +56,35 @@ EXAMPLES = ''' # Ensure system mtu is 9126 - nxos_mtu: - sysmtu=9216 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + sysmtu: 9216 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # Config mtu on Eth1/1 (routed interface) - nxos_mtu: - interface=Ethernet1/1 - mtu=1600 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + interface: Ethernet1/1 + mtu: 1600 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # Config mtu on Eth1/3 (switched interface) - nxos_mtu: - interface=Ethernet1/3 - mtu=9216 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + interface: Ethernet1/3 + mtu: 9216 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # Unconfigure mtu on a given interface - nxos_mtu: - interface=Ethernet1/3 - mtu=9216 host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} - state=absent + interface: Ethernet1/3 + mtu: 9216 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} + state: absent ''' RETURN = ''' @@ -591,4 +592,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_ntp.py b/network/nxos/nxos_ntp.py index 7e64fccdb50..81de38bef84 100644 --- a/network/nxos/nxos_ntp.py +++ b/network/nxos/nxos_ntp.py @@ -78,12 +78,12 @@ EXAMPLES = ''' # Set NTP Server with parameters - nxos_ntp: - server=1.2.3.4 - key_id=32 - prefer=enabled - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + server: 1.2.3.4 + key_id: 32 + prefer: enabled + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -629,4 +629,4 @@ def main(): from ansible.module_utils.basic import * if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py index fc2d1c52a3f..28da3a74468 100644 --- a/network/nxos/nxos_ntp_auth.py +++ b/network/nxos/nxos_ntp_auth.py @@ -74,12 +74,12 @@ EXAMPLES = ''' # Basic NTP authentication configuration - nxos_ntp_auth: - key_id=32 - md5string=hello - auth_type=text - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + key_id: 32 + md5string: hello + auth_type: text + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -566,4 +566,4 @@ def main(): module.exit_json(**results) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py index a8a0fe2d77f..69dd954cda2 100644 --- a/network/nxos/nxos_ntp_options.py +++ b/network/nxos/nxos_ntp_options.py @@ -63,12 +63,12 @@ EXAMPLES = ''' # Basic NTP options configuration - nxos_ntp_options: - master=true - stratum=12 - logging=false - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + master: true + stratum: 12 + logging: false + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -513,4 +513,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index 2f2800a1d6e..ea566da000f 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -41,7 +41,7 @@ EXAMPLES = ''' - nxos_ospf: - ospf=1 + ospf: 1 state: present username: "{{ un }}" password: "{{ pwd }}" diff --git a/network/nxos/nxos_overlay_global.py b/network/nxos/nxos_overlay_global.py index e84541fd634..710e02e2b12 100644 --- a/network/nxos/nxos_overlay_global.py +++ b/network/nxos/nxos_overlay_global.py @@ -39,7 +39,7 @@ EXAMPLES = ''' - nxos_overlay_global: - anycast_gateway_mac="b.b.b" + anycast_gateway_mac: "b.b.b" username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_pim.py b/network/nxos/nxos_pim.py index b97785184b2..da5b49a818f 100644 --- a/network/nxos/nxos_pim.py +++ b/network/nxos/nxos_pim.py @@ -34,7 +34,7 @@ ''' EXAMPLES = ''' - nxos_pim: - ssm_range="232.0.0.0/8" + ssm_range: "232.0.0.0/8" username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_pim_interface.py b/network/nxos/nxos_pim_interface.py index 00ace4b9fb8..68f0cbb775c 100644 --- a/network/nxos/nxos_pim_interface.py +++ b/network/nxos/nxos_pim_interface.py @@ -108,40 +108,40 @@ EXAMPLES = ''' # ensure PIM is not running on the interface - nxos_pim_interface: - interface=eth1/33 - state=absent - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + 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 }} + 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 }} + 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 }} + interface: eth1/33 + state: default + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -926,4 +926,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_snmp_community.py b/network/nxos/nxos_snmp_community.py index 9e3aa1de2ec..137dd7715b0 100644 --- a/network/nxos/nxos_snmp_community.py +++ b/network/nxos/nxos_snmp_community.py @@ -59,12 +59,12 @@ EXAMPLES = ''' # ensure snmp community is configured - nxos_snmp_community: - community=TESTING7 - group=network-operator - state=present - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + community: TESTING7 + group: network-operator + state: present + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index a1adb1a6793..e6982a8a4d4 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -45,11 +45,11 @@ EXAMPLES = ''' # ensure snmp contact is configured - nxos_snmp_contact: - contact=Test - state=present - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + contact: Test + state: present + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_host.py b/network/nxos/nxos_snmp_host.py index 16c6f025469..e767f1ce43c 100644 --- a/network/nxos/nxos_snmp_host.py +++ b/network/nxos/nxos_snmp_host.py @@ -84,12 +84,12 @@ EXAMPLES = ''' # ensure snmp host is configured - nxos_snmp_host: - snmp_host=3.3.3.3 - community=TESTING - state=present - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + snmp_host: 3.3.3.3 + community: TESTING + state: present + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -636,4 +636,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py index eb9520a931a..b829b83cdad 100644 --- a/network/nxos/nxos_snmp_location.py +++ b/network/nxos/nxos_snmp_location.py @@ -44,19 +44,19 @@ EXAMPLES = ''' # ensure snmp location is configured - nxos_snmp_location: - location=Test - state=present - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + location: Test + state: present + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # ensure snmp location is not configured - nxos_snmp_location: - location=Test - state=absent - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + location: Test + state: absent + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -416,4 +416,4 @@ def main(): from ansible.module_utils.basic import * if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py index 98c779fe3eb..c48eafa0eba 100644 --- a/network/nxos/nxos_snmp_traps.py +++ b/network/nxos/nxos_snmp_traps.py @@ -49,22 +49,22 @@ choices: ['enabled','disabled'] ''' -EXAMPLES = ''' +EXAMPLES = ''' # ensure lldp trap configured - nxos_snmp_traps: - group=lldp - state=enabled - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + group: lldp + state: enabled + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # ensure lldp trap is not configured - nxos_snmp_traps: - group=lldp - state=disabled - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + group: lldp + state: disabled + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -493,4 +493,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_snmp_user.py b/network/nxos/nxos_snmp_user.py index 5e9eec29a48..9e1c051739d 100644 --- a/network/nxos/nxos_snmp_user.py +++ b/network/nxos/nxos_snmp_user.py @@ -69,13 +69,13 @@ EXAMPLES = ''' - nxos_snmp_user: - user=ntc - group=network-operator - auth=md5 - pwd=test_password - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + user: ntc + group: network-operator + auth: md5 + pwd: test_password + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -555,4 +555,4 @@ def main(): if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index 0fb5878dc58..eb0555067f1 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -67,10 +67,10 @@ EXAMPLES = ''' - nxos_static_route: - prefix="192.168.20.64/24" - next_hop="3.3.3.3" - route_name=testing - pref=100 + prefix: "192.168.20.64/24" + next_hop: "3.3.3.3" + route_name: testing + pref: 100 username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_udld.py b/network/nxos/nxos_udld.py index 04f8373e234..cd377870ee3 100644 --- a/network/nxos/nxos_udld.py +++ b/network/nxos/nxos_udld.py @@ -60,19 +60,19 @@ EXAMPLES = ''' # ensure udld aggressive mode is globally disabled and se global message interval is 20 - nxos_udld: - aggressive=disabled - msg_time=20 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + aggressive: disabled + msg_time: 20 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # Ensure agg mode is globally enabled and msg time is 15 - nxos_udld: - aggressive=enabled - msg_time=15 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + aggressive: enabled + msg_time: 15 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -499,4 +499,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_udld_interface.py b/network/nxos/nxos_udld_interface.py index 28c3a3ece7a..593aaa3fcd7 100644 --- a/network/nxos/nxos_udld_interface.py +++ b/network/nxos/nxos_udld_interface.py @@ -48,29 +48,29 @@ EXAMPLES = ''' # ensure Ethernet1/1 is configured to be in aggressive mode - nxos_udld_interface: - interface=Ethernet1/1 - mode=aggressive - state=present - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + interface: Ethernet1/1 + mode: aggressive + state: present + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # Remove the aggressive config only if it's currently in aggressive mode and then disable udld (switch default) - nxos_udld_interface: - interface=Ethernet1/1 - mode=aggressive - state=absent - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + interface: Ethernet1/1 + mode: aggressive + state: absent + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # ensure Ethernet1/1 has aggressive mode enabled - nxos_udld_interface: - interface=Ethernet1/1 - mode=enabled - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + interface: Ethernet1/1 + mode: enabled + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -511,4 +511,4 @@ def main(): module.exit_json(**results) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py index 516c4ddcd92..62635e84610 100644 --- a/network/nxos/nxos_vpc.py +++ b/network/nxos/nxos_vpc.py @@ -90,13 +90,13 @@ EXAMPLES = ''' # configure a simple asn - nxos_vpc: - domain=100 - role_priority=1000 - system_priority=2000 - pkl_dest=192.168.100.4 - pkl_src=10.1.100.20 - peer_gw=true - auto_recovery=true + domain: 100 + role_priority: 1000 + system_priority: 2000 + pkl_dest: 192.168.100.4 + pkl_src: 10.1.100.20 + peer_gw: true + auto_recovery: true username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index ed81ffe3343..4480c58727d 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -58,8 +58,8 @@ EXAMPLES = ''' - nxos_vpc_portchannel: - portchannel=10 - vpc=100 + portchannel: 10 + vpc: 100 username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_vrf_af.py b/network/nxos/nxos_vrf_af.py index 67b4ef04046..b8706d28c29 100644 --- a/network/nxos/nxos_vrf_af.py +++ b/network/nxos/nxos_vrf_af.py @@ -61,9 +61,9 @@ ''' EXAMPLES = ''' - nxos_vrf_af: - interface=nve1 - vni=6000 - ingress_replication=true + interface: nve1 + vni: 6000 + ingress_replication: true username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 68fc8e20a22..7ea1b8c8cde 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -44,10 +44,10 @@ EXAMPLES = ''' # ENSURE VTP DOMAIN IS CONFIGURED - nxos_vtp_domain: - domain=ntc - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + domain: ntc + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' @@ -417,4 +417,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index 29ae405ee78..d4528b70179 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -54,19 +54,19 @@ EXAMPLES = ''' # ENSURE VTP PASSWORD IS SET - nxos_vtp_password: - password=ntc - state=present - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + password: ntc + state: present + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} # ENSURE VTP PASSWORD IS REMOVED - nxos_vtp_password: - password=ntc - state=absent - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + password: ntc + state: absent + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -474,4 +474,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_vtp_version.py b/network/nxos/nxos_vtp_version.py index 4979d60a1fe..0d24e9c37c0 100644 --- a/network/nxos/nxos_vtp_version.py +++ b/network/nxos/nxos_vtp_version.py @@ -42,10 +42,10 @@ EXAMPLES = ''' # ENSURE VTP VERSION IS 2 - nxos_vtp_version: - version=2 - host={{ inventory_hostname }} - username={{ un }} - password={{ pwd }} + version: 2 + host: {{ inventory_hostname }} + username: {{ un }} + password: {{ pwd }} ''' RETURN = ''' @@ -412,4 +412,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_vxlan_vtep.py b/network/nxos/nxos_vxlan_vtep.py index be03739bf52..620d3e240e1 100644 --- a/network/nxos/nxos_vxlan_vtep.py +++ b/network/nxos/nxos_vxlan_vtep.py @@ -75,12 +75,12 @@ ''' EXAMPLES = ''' - nxos_vxlan_vtep: - interface=nve1 - description=default - host_reachability=default - source_interface=Loopback0 - source_interface_hold_down_time=30 - shutdown=default + interface: nve1 + description: default + host_reachability: default + source_interface: Loopback0 + source_interface_hold_down_time: 30 + shutdown: default username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py index 45232d20394..cf522f63e8f 100644 --- a/network/nxos/nxos_vxlan_vtep_vni.py +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -101,9 +101,9 @@ ''' EXAMPLES = ''' - nxos_vxlan_vtep_vni: - interface=nve1 - vni=6000 - ingress_replication=default + interface: nve1 + vni: 6000 + ingress_replication: default username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" From 446c7de2395d0f4872581e163509015cfd3cf660 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 22:26:56 -0400 Subject: [PATCH 376/770] bug fixes in junos_netconf module (#4924) * fixes exception thrown when sending commands to device * fixes exception thrown when retrieving current resource instance * fixes issue where netconf would be configured in some instances when state was set to absent * now returns the command string sent to the remote device * fixes argument name to be netconf_port with alias to listens_on --- network/junos/junos_netconf.py | 44 +++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/network/junos/junos_netconf.py b/network/junos/junos_netconf.py index fa9ca4115e6..99b918c0517 100644 --- a/network/junos/junos_netconf.py +++ b/network/junos/junos_netconf.py @@ -30,13 +30,15 @@ resources as defined in RFC 6242. extends_documentation_fragment: junos options: - listens_on: + netconf_port: description: - This argument specifies the port the netconf service should listen on for SSH connections. The default port as defined in RFC 6242 is 830. required: false default: 830 + aliases: ['listens_on'] + version_added: "2.2" state: description: - Specifies the state of the M(junos_netconf) resource on @@ -53,29 +55,37 @@ # Note: examples below use the following provider dict to handle # transport and authentication to the node. vars: - netconf: + cli: host: "{{ inventory_hostname }}" username: ansible password: Ansible - transport: netconf + transport: cli - name: enable netconf service on port 830 junos_netconf: listens_on: 830 state: present - provider: "{{ netconf }}" + provider: "{{ cli }}" - name: disable netconf service junos_netconf: state: absent - provider: "{{ netconf }}" + provider: "{{ cli }}" """ RETURN = """ +commands: + description: Returns the command sent to the remote device + returned: when changed is True + type: str + sample: 'set system services netconf ssh port 830' """ import re -from ansible.module_utils.junos import NetworkModule +import ansible.module_utils.junos + +from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError def parse_port(config): match = re.search(r'port (\d+)', config) @@ -84,7 +94,7 @@ def parse_port(config): def get_instance(module): cmd = 'show configuration system services netconf' - cfg = module.run_commands(cmd)[0] + cfg = module.cli(cmd)[0] result = dict(state='absent') if cfg: result = dict(state='present') @@ -96,7 +106,7 @@ def main(): """ argument_spec = dict( - listens_on=dict(type='int', default=830), + netconf_port=dict(type='int', default=830, aliases=['listens_on']), state=dict(default='present', choices=['present', 'absent']), transport=dict(default='cli', choices=['cli']) ) @@ -105,25 +115,31 @@ def main(): supports_check_mode=True) state = module.params['state'] - port = module.params['listens_on'] + port = module.params['netconf_port'] result = dict(changed=False) instance = get_instance(module) - commands = None if state == 'present' and instance.get('state') == 'absent': commands = 'set system services netconf ssh port %s' % port + elif state == 'present' and port != instance.get('port'): + commands = 'set system services netconf ssh port %s' % port elif state == 'absent' and instance.get('state') == 'present': commands = 'delete system services netconf' - elif port != instance.get('port'): - commands = 'set system services netconf ssh port %s' % port + else: + commands = None if commands: if not module.check_mode: - comment = 'configuration updated by junos_netconf' - module.config(commands, comment=comment) + try: + comment = 'configuration updated by junos_netconf' + module.config(commands, comment=comment) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) result['changed'] = True + result['commands'] = commands module.exit_json(**result) From 16f298138c405373e94c119b60859ad6d102499f Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 22:27:07 -0400 Subject: [PATCH 377/770] roll up of bug fixes for junos_config module (#4925) * fixed docstring referencing old arguments * changed out lxml for xml library to avoid import errors * fixed issue when trying to confirm a commit will end up a NOOP * fixed issue for passing replace argument to load_config method --- network/junos/junos_config.py | 43 +++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 4bd4b360f71..017b2b1c36c 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -31,10 +31,10 @@ options: lines: description: - - The path to the config source. The source can be either a - file with config or a template that will be merged during - runtime. By default the task will search for the source - file in role or playbook root folder in templates directory. + - This argument takes a list of C(set) or C(delete) configuration + lines to push into the remote device. Each line must start with + either C(set) or C(delete). This argument is mutually exclusive + with the I(src) argument. required: false default: null src: @@ -58,17 +58,6 @@ default: null choices: ['xml', 'set', 'text', 'json'] version_added: "2.2" - update: - description: - - The I(update) argument controls how the configuration statements - are processed on the remote device. Valid choices for the I(update) - argument are I(merge) and I(replace). When the argument is set to - I(merge), the configuration changes are merged with the current - device active configuration. - required: false - default: merge - choices: ['merge', 'replace'] - version_added: "2.2" rollback: description: - The C(rollback) argument instructs the module to rollback the @@ -156,6 +145,10 @@ junos_config: zeroize: yes provider: "{{ netconf }}" + +- name: confirm a previous commit + junos_config: + provider: "{{ netconf }}" """ RETURN = """ @@ -165,12 +158,14 @@ type: path sample: /playbooks/ansible/backup/config.2016-07-16@22:28:34 """ -import re import json -from lxml import etree +from xml.etree import ElementTree + +import ansible.module_utils.junos -from ansible.module_utils.junos import NetworkModule, NetworkError +from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError DEFAULT_COMMENT = 'configured by junos_config' @@ -184,9 +179,9 @@ def guess_format(config): pass try: - etree.fromstring(config) + ElementTree.fromstring(config) return 'xml' - except etree.XMLSyntaxError: + except ElementTree.ParseError: pass if config.startswith('set') or config.startswith('delete'): @@ -200,6 +195,7 @@ def load_config(module, result): kwargs = dict() kwargs['comment'] = module.params['comment'] kwargs['confirm'] = module.params['confirm'] + kwargs['replace'] = module.params['replace'] kwargs['commit'] = not module.check_mode if module.params['src']: @@ -231,11 +227,18 @@ def zeroize_config(module, result): module.cli.run_commands('request system zeroize') result['changed'] = True +def confirm_config(module, result): + if not module.check_mode: + module.connection.commit_config() + result['changed'] = True + def run(module, result): if module.params['rollback']: return rollback_config(module, result) elif module.params['zeroize']: return zeroize_config(module, result) + elif not any((module.params['src'], module.params['lines'])): + return confirm_config(module, result) else: return load_config(module, result) From a3807eee1029bdcf68cb898fa3c80741137d2fe6 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 23:13:53 -0400 Subject: [PATCH 378/770] fix import in junos_command module (#4927) This fixes the import statements in the junos_command module to be consistent with all junos_* modules --- network/junos/junos_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index cdbb34ce47c..3286b0614e7 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -154,10 +154,12 @@ """ import re +import ansible.module_utils.junos + from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.junos import NetworkModule, NetworkError VALID_KEYS = { 'cli': frozenset(['command', 'output', 'prompt', 'response']), From df4a9dabd53eff5b2ad8d0bfa408b426730397be Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 23:18:26 -0400 Subject: [PATCH 379/770] fix up junos_facts import statements (#4928) This fixes the junos_facts import statements and removes importing NetworkModule from the junos shared module. --- network/junos/junos_facts.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/network/junos/junos_facts.py b/network/junos/junos_facts.py index 8ff5eb4572b..f22b3bb0000 100644 --- a/network/junos/junos_facts.py +++ b/network/junos/junos_facts.py @@ -85,7 +85,9 @@ returned: always type: dict """ -from ansible.module_utils.junos import NetworkModule +import ansible.module_utils.junos + +from ansible.module_utils.network import NetworkModule from ansible.module_utils.junos import xml_to_string, xml_to_json def main(): From 4c8e32ee27db53cf9b7ab47f21c2f6eb6b7d930e Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 19 Sep 2016 23:18:35 -0400 Subject: [PATCH 380/770] fixes import statements in junos_package module (#4929) This fixes the import statement to import NetworkModule from network instead of from junos --- network/junos/junos_package.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/network/junos/junos_package.py b/network/junos/junos_package.py index fbf575b4dfc..78a0f024839 100644 --- a/network/junos/junos_package.py +++ b/network/junos/junos_package.py @@ -92,7 +92,9 @@ src: junos-vsrx-12.1X46-D10.2-domestic.tgz reboot: no """ -from ansible.module_utils.junos import NetworkModule +import ansible.module_utils.junos + +from ansible.module_utils.newtork import NetworkModule try: from jnpr.junos.utils.sw import SW From 20e08b2cdfcf3f699ec34d4b271181920450f726 Mon Sep 17 00:00:00 2001 From: indispeq Date: Tue, 20 Sep 2016 08:39:01 -0400 Subject: [PATCH 381/770] Fix openstack security group rule vrrp choice apostrophe error (#4750) Fixes #4444 that was erroneously closed and thought fixed --- cloud/openstack/os_security_group_rule.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_security_group_rule.py b/cloud/openstack/os_security_group_rule.py index 9dcda820dd7..5cb1418acce 100644 --- a/cloud/openstack/os_security_group_rule.py +++ b/cloud/openstack/os_security_group_rule.py @@ -39,7 +39,7 @@ protocol: description: - IP protocols TCP UDP ICMP 112 (VRRP) - choices: ['tcp', 'udp', 'icmp', 112, None] + choices: ['tcp', 'udp', 'icmp', '112', None] default: None port_range_min: description: @@ -257,7 +257,7 @@ def main(): # NOTE(Shrews): None is an acceptable protocol value for # Neutron, but Nova will balk at this. protocol = dict(default=None, - choices=[None, 'tcp', 'udp', 'icmp', 112]), + choices=[None, 'tcp', 'udp', 'icmp', '112']), port_range_min = dict(required=False, type='int'), port_range_max = dict(required=False, type='int'), remote_ip_prefix = dict(required=False, default=None), From 5ab1a6f88dea24eafa6e61114e5687fc7f2b7704 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 20 Sep 2016 09:45:26 -0400 Subject: [PATCH 382/770] Make project start and stop error handling more resilient, and improve message text. --- cloud/docker/docker_service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index afb2e757b68..8158de7e517 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -655,7 +655,7 @@ def cmd_up(self): detached=detached, remove_orphans=self.remove_orphans) except Exception as exc: - self.client.fail("Error bring %s up - %s" % (self.project.name, str(exc))) + self.client.fail("Error starting project - %s" % str(exc)) if self.stopped: result.update(self.cmd_stop(service_names)) @@ -804,7 +804,7 @@ def cmd_down(self): try: self.project.down(image_type, self.remove_volumes, self.remove_orphans) except Exception as exc: - self.client.fail("Error bringing %s down - %s" % (self.project.name, str(exc))) + self.client.fail("Error stopping project - %s" % str(exc)) return result From 12a7027c49f03e969f219bab816bfb928005bacf Mon Sep 17 00:00:00 2001 From: Rob Cutmore Date: Tue, 20 Sep 2016 10:05:08 -0400 Subject: [PATCH 383/770] Pip: use 'pip list' when available for package list (#4644) * Pip: handle parsing different pip commands * Pip: use 'pip list' when available * Pip: explicitly check which command is used * Pip: add error checking when fetching packages --- packaging/language/pip.py | 54 +++++++++++++++++++++++++++------------ 1 file changed, 38 insertions(+), 16 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index d3bda11d38e..f1ca9d76156 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -204,20 +204,48 @@ def _get_full_name(name, version=None): resp = name + '==' + version return resp -def _is_present(name, version, installed_pkgs): + +def _get_packages(module, pip, chdir): + '''Return results of pip command to get packages.''' + # Try 'pip list' command first. + command = '%s list' % pip + rc, out, err = module.run_command(command, cwd=chdir) + + # If there was an error (pip version too old) then use 'pip freeze'. + if rc != 0: + command = '%s freeze' % pip + rc, out, err = module.run_command(command, cwd=chdir) + if rc != 0: + _fail(module, command, out, err) + + return (command, out, err) + + +def _is_present(name, version, installed_pkgs, pkg_command): + '''Return whether or not package is installed.''' for pkg in installed_pkgs: - if '==' not in pkg: + # Package listing will be different depending on which pip + # command was used ('pip list' vs. 'pip freeze'). + if 'list' in pkg_command: + pkg = pkg.replace('(', '').replace(')', '') + if ',' in pkg: + pkg_name, pkg_version, _ = pkg.replace(',', '').split(' ') + else: + pkg_name, pkg_version = pkg.split(' ') + elif 'freeze' in pkg_command: + if '==' in pkg: + pkg_name, pkg_version = pkg.split('==') + else: + continue + else: continue - [pkg_name, pkg_version] = pkg.split('==') - if pkg_name == name and (version is None or version == pkg_version): return True return False - def _get_pip(module, env=None, executable=None): # On Debian and Ubuntu, pip is pip. # On Fedora18 and up, pip is python-pip. @@ -400,12 +428,7 @@ def main(): elif has_vcs: module.exit_json(changed=True) - freeze_cmd = '%s freeze' % pip - - rc, out_pip, err_pip = module.run_command(freeze_cmd, cwd=chdir) - - if rc != 0: - module.exit_json(changed=True) + pkg_cmd, out_pip, err_pip = _get_packages(module, pip, chdir) out += out_pip err += err_pip @@ -413,15 +436,14 @@ def main(): changed = False if name: for pkg in name: - is_present = _is_present(pkg, version, out.split()) + is_present = _is_present(pkg, version, out.split('\n'), pkg_cmd) if (state == 'present' and not is_present) or (state == 'absent' and is_present): changed = True break - module.exit_json(changed=changed, cmd=freeze_cmd, stdout=out, stderr=err) + module.exit_json(changed=changed, cmd=pkg_cmd, stdout=out, stderr=err) if requirements or has_vcs: - freeze_cmd = '%s freeze' % pip - out_freeze_before = module.run_command(freeze_cmd, cwd=chdir)[1] + _, out_freeze_before, _ = _get_packages(module, pip, chdir) else: out_freeze_before = None @@ -443,7 +465,7 @@ def main(): if out_freeze_before is None: changed = 'Successfully installed' in out_pip else: - out_freeze_after = module.run_command(freeze_cmd, cwd=chdir)[1] + _, out_freeze_after, _ = _get_packages(module, pip, chdir) changed = out_freeze_before != out_freeze_after module.exit_json(changed=changed, cmd=cmd, name=name, version=version, From 4a560d4a155fe4de590daae250b746e1b801cb67 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 20 Sep 2016 10:29:27 -0400 Subject: [PATCH 384/770] Cast scale value to int. Fixes #4592. --- cloud/docker/docker_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index 8158de7e517..db4352c9306 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -877,7 +877,7 @@ def cmd_scale(self): result['actions'][service.name]['scale'] = self.scale[service.name] - len(containers) if not self.check_mode: try: - service.scale(self.scale[service.name]) + service.scale(int(self.scale[service.name])) except Exception as exc: self.client.fail("Error scaling %s - %s" % (service.name, str(exc))) return result From a49cd08832a4a948920297b628f6c255d5c6a498 Mon Sep 17 00:00:00 2001 From: Lars Engels Date: Tue, 20 Sep 2016 16:42:27 +0200 Subject: [PATCH 385/770] Add support for password aging on Solaris (#4372) * Add support for password aging on Solaris * Fix shadow file editing when {MIN,MAX,WARN}WEEKS is not set in /etc/default/passwd * Un-break with python3 * _Really_ un-break with python3 --- system/user.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/system/user.py b/system/user.py index 505bc3e48b8..9c1c9adaa83 100644 --- a/system/user.py +++ b/system/user.py @@ -1238,6 +1238,29 @@ class SunOS(User): distribution = None SHADOWFILE = '/etc/shadow' + def get_password_defaults(self): + # Read password aging defaults + try: + minweeks = '' + maxweeks = '' + warnweeks = '' + for line in open("/etc/default/passwd", 'r'): + line = line.strip() + if (line.startswith('#') or line == ''): + continue + key, value = line.split('=') + if key == "MINWEEKS": + minweeks = value.rstrip('\n') + elif key == "MAXWEEKS": + maxweeks = value.rstrip('\n') + elif key == "WARNWEEKS": + warnweeks = value.rstrip('\n') + except Exception: + err = get_exception() + self.module.fail_json(msg="failed to read /etc/default/passwd: %s" % str(err)) + + return (minweeks, maxweeks, warnweeks) + def remove_user(self): cmd = [self.module.get_bin_path('userdel', True)] if self.remove: @@ -1295,6 +1318,7 @@ def create_user(self): if not self.module.check_mode: # we have to set the password by editing the /etc/shadow file if self.password is not None: + minweeks, maxweeks, warnweeks = self.get_password_defaults() try: lines = [] for line in open(self.SHADOWFILE, 'rb').readlines(): @@ -1304,6 +1328,12 @@ def create_user(self): continue fields[1] = self.password fields[2] = str(int(time.time() / 86400)) + if minweeks: + fields[3] = str(int(minweeks) * 7) + if maxweeks: + fields[4] = str(int(maxweeks) * 7) + if warnweeks: + fields[5] = str(int(warnweeks) * 7) line = ':'.join(fields) lines.append('%s\n' % line) open(self.SHADOWFILE, 'w+').writelines(lines) @@ -1382,6 +1412,7 @@ def modify_user_usermod(self): if self.update_password == 'always' and self.password is not None and info[1] != self.password: (rc, out, err) = (0, '', '') if not self.module.check_mode: + minweeks, maxweeks, warnweeks = self.get_password_defaults() try: lines = [] for line in open(self.SHADOWFILE, 'rb').readlines(): @@ -1391,6 +1422,12 @@ def modify_user_usermod(self): continue fields[1] = self.password fields[2] = str(int(time.time() / 86400)) + if minweeks: + fields[3] = str(int(minweeks) * 7) + if maxweeks: + fields[4] = str(int(maxweeks) * 7) + if warnweeks: + fields[5] = str(int(warnweeks) * 7) line = ':'.join(fields) lines.append('%s\n' % line) open(self.SHADOWFILE, 'w+').writelines(lines) From 6fbd72db82512479410ad578a694e62324ecc802 Mon Sep 17 00:00:00 2001 From: Toopy Date: Tue, 20 Sep 2016 16:50:42 +0200 Subject: [PATCH 386/770] Fix git get_diff TypeError when fetch (#4881) --- source_control/git.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/source_control/git.py b/source_control/git.py index 5c5618403a1..8b85df27f44 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -424,7 +424,8 @@ def get_diff(module, git_path, dest, repo, remote, depth, bare, before, after): return { 'prepared': '>> Newly checked out %s' % after } elif before != after: # Ensure we have the object we are referring to during git diff ! - fetch(git_path, module, repo, dest, after, remote, depth, bare, '') + git_version_used = git_version(git_path, module) + fetch(git_path, module, repo, dest, after, remote, depth, bare, '', git_version_used) cmd = '%s diff %s %s' % (git_path, before, after) (rc, out, err) = module.run_command(cmd, cwd=dest) if rc == 0 and out: From 70d4ff8e38c955d74fad77d5f91507c0f1478c65 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 20 Sep 2016 07:59:55 -0700 Subject: [PATCH 387/770] Fix parsing of pip output --- packaging/language/pip.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index f1ca9d76156..df41251b81c 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -19,11 +19,6 @@ # along with Ansible. If not, see . # -import tempfile -import re -import os -import sys - DOCUMENTATION = ''' --- module: pip @@ -46,8 +41,8 @@ default: null requirements: description: - - The path to a pip requirements file, which should be local to the remote system. - File can be specified as a relative path if using the chdir option. + - The path to a pip requirements file, which should be local to the remote system. + File can be specified as a relative path if using the chdir option. required: false default: null virtualenv: @@ -186,6 +181,14 @@ become: True ''' +import tempfile +import re +import os +import sys + +from ansible.module_utils.basic import AnsibleModule + + def _get_cmd_options(module, cmd): thiscmd = cmd + " --help" rc, stdout, stderr = module.run_command(thiscmd) @@ -209,7 +212,8 @@ def _get_packages(module, pip, chdir): '''Return results of pip command to get packages.''' # Try 'pip list' command first. command = '%s list' % pip - rc, out, err = module.run_command(command, cwd=chdir) + lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + rc, out, err = module.run_command(command, cwd=chdir, environ_update=lang_env) # If there was an error (pip version too old) then use 'pip freeze'. if rc != 0: @@ -334,9 +338,8 @@ def main(): module.fail_json(msg="umask must be an octal integer", details=str(sys.exc_info()[1])) - old_umask = None - if umask != None: + if umask is not None: old_umask = os.umask(umask) try: if state == 'latest' and version is not None: @@ -435,8 +438,10 @@ def main(): changed = False if name: + # Pip spits an upgrade notice to stdout that we have to get rid of + pkg_list = [p for p in out.split('\n') if not p.startswith('You are using') and not p.startswith('You should consider') and p] for pkg in name: - is_present = _is_present(pkg, version, out.split('\n'), pkg_cmd) + is_present = _is_present(pkg, version, pkg_list, pkg_cmd) if (state == 'present' and not is_present) or (state == 'absent' and is_present): changed = True break @@ -472,10 +477,8 @@ def main(): state=state, requirements=requirements, virtualenv=env, stdout=out, stderr=err) finally: - if old_umask != None: + if old_umask is not None: os.umask(old_umask) -# import module snippets -from ansible.module_utils.basic import * - -main() +if __name__ == '__main__': + main() From 19be0da3b039f2510042261d0f2faaa03236ba85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Tue, 20 Sep 2016 17:36:02 +0200 Subject: [PATCH 388/770] iam_cert: remove choice list for dup_ok type bool (#4940) See 8879931f0cd727244587b6e58a7279d9125c96a2 --- cloud/amazon/iam_cert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/iam_cert.py b/cloud/amazon/iam_cert.py index 8e56017052e..dbc4fcb476e 100644 --- a/cloud/amazon/iam_cert.py +++ b/cloud/amazon/iam_cert.py @@ -232,7 +232,7 @@ def main(): new_name=dict(default=None, required=False), path=dict(default='/', required=False), new_path=dict(default=None, required=False), - dup_ok=dict(default=False, required=False, choices=[False, True], type='bool') + dup_ok=dict(default=False, required=False, type='bool') ) ) From 3a2cfadb3195286d46d7572b33b8b1aa1815df8a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 20 Sep 2016 11:42:51 -0400 Subject: [PATCH 389/770] fixes junos_config to be idempotent when confirming a commit (#4946) The junos_config module would always return true when confirming a commit This changes the module to now check first making the feature idempontent --- network/junos/junos_config.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 017b2b1c36c..d7458d956c7 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -228,9 +228,8 @@ def zeroize_config(module, result): result['changed'] = True def confirm_config(module, result): - if not module.check_mode: - module.connection.commit_config() - result['changed'] = True + checkonly = module.check_mode + result['changed'] = module.connection.confirm_commit(checkonly) def run(module, result): if module.params['rollback']: From b87bf7c12d8117a5dcb53d7b5312f9ed20eb1901 Mon Sep 17 00:00:00 2001 From: Timothy Appnel Date: Tue, 20 Sep 2016 11:51:57 -0400 Subject: [PATCH 390/770] Adds docs for msg param in assert modules plus some other clean up (#4926) --- utilities/logic/assert.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/utilities/logic/assert.py b/utilities/logic/assert.py index e9e359f421a..9600b9d1f92 100644 --- a/utilities/logic/assert.py +++ b/utilities/logic/assert.py @@ -21,9 +21,9 @@ DOCUMENTATION = ''' --- module: assert -short_description: Fail with custom message +short_description: Asserts given expressions are true description: - - This module asserts that a given expression is true and can be a simpler alternative to the 'fail' module in some cases. + - This module asserts that given expressions are true with an optional custom message. version_added: "1.5" options: that: @@ -31,6 +31,10 @@ - "A string expression of the same form that can be passed to the 'when' statement" - "Alternatively, a list of string expressions" required: true + msg: + description: + - "The customized message used for a failing assertion" + required: false author: - "Ansible Core Team" - "Michael DeHaan" @@ -43,4 +47,10 @@ that: - "'foo' in some_command_result.stdout" - "number_of_the_counting == 3" + +- assert: + that: + - "my_param <= 100" + - "my_param >= 0" + msg: "'my_param' is must be between 0 and 100" ''' From 5b1994cb42ad52a3a3cbf5461cbe6f8af2a69262 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 20 Sep 2016 11:02:14 -0700 Subject: [PATCH 391/770] pip list isn't available on pip less than 1.3 so make a fallback (#4947) --- packaging/language/pip.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index df41251b81c..9eda293250a 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -424,7 +424,6 @@ def main(): if requirements: cmd += ' -r %s' % requirements - if module.check_mode: if extra_args or requirements or state == 'latest' or not name: module.exit_json(changed=True) @@ -438,8 +437,34 @@ def main(): changed = False if name: - # Pip spits an upgrade notice to stdout that we have to get rid of pkg_list = [p for p in out.split('\n') if not p.startswith('You are using') and not p.startswith('You should consider') and p] + if pkg_cmd.endswith(' freeze') and ('pip' in name or 'setuptools' in name): + # Older versions of pip (pre-1.3) do not have pip list. + # pip freeze does not list setuptools or pip in its output + # So we need to get those via a specialcase + if 'setuptools' in name: + try: + import setuptools + except ImportError: + # Could not import, assume that it is not installed + pass + else: + formatted_dep = 'setuptools==%s' % setuptools.__version__ + pkg_list.append(formatted_dep) + out += '%s\n' % formatted_dep + + if 'pip' in name: + try: + import pkg_resources + except ImportError: + # Could not import pkg_resources. pip requires + # pkg_resources so assume that it is not installed + pass + else: + formatted_dep = 'pip==%s' % pkg_resources.get_distribution('pip').version + pkg_list.append(formatted_dep) + out += '%s\n' % formatted_dep + for pkg in name: is_present = _is_present(pkg, version, pkg_list, pkg_cmd) if (state == 'present' and not is_present) or (state == 'absent' and is_present): From 34863959703e8d920a21bb10da618cab9b48ed44 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 20 Sep 2016 11:42:15 -0700 Subject: [PATCH 392/770] Remove script previously used by Travis. --- test-docs.sh | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100755 test-docs.sh diff --git a/test-docs.sh b/test-docs.sh deleted file mode 100755 index 76297fbada6..00000000000 --- a/test-docs.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh -set -x - -CHECKOUT_DIR=".ansible-checkout" -MOD_REPO="$1" - -# Hidden file to avoid the module_formatter recursing into the checkout -git clone https://github.com/ansible/ansible "$CHECKOUT_DIR" -cd "$CHECKOUT_DIR" -git submodule update --init -rm -rf "lib/ansible/modules/$MOD_REPO" -ln -s "$TRAVIS_BUILD_DIR/" "lib/ansible/modules/$MOD_REPO" - -pip install -U Jinja2 PyYAML setuptools six pycrypto sphinx - -. ./hacking/env-setup -PAGER=/bin/cat bin/ansible-doc -l -if [ $? -ne 0 ] ; then - exit $? -fi -make -C docsite From b4f1d0d0c2f8e580e972c75860a9e44dba89e3d8 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 20 Sep 2016 18:20:13 -0700 Subject: [PATCH 393/770] Fix pip freeze workaround with virtualenv (#4951) --- packaging/language/pip.py | 78 +++++++++++++++++++++++---------------- 1 file changed, 47 insertions(+), 31 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 9eda293250a..1fe039509eb 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -187,6 +187,14 @@ import sys from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +#: Python one-liners to be run at the command line that will determine the +# installed version for these special libraries. These are libraries that +# don't end up in the output of pip freeze. +_SPECIAL_PACKAGE_CHECKERS = {'setuptools': 'import setuptools; print(setuptools.__version__)', + 'pip': 'import pkg_resources; print(pkg_resources.get_distribution("pip").version)'} def _get_cmd_options(module, cmd): @@ -196,7 +204,7 @@ def _get_cmd_options(module, cmd): module.fail_json(msg="Could not get output from %s: %s" % (thiscmd, stdout + stderr)) words = stdout.strip().split() - cmd_options = [ x for x in words if x.startswith('--') ] + cmd_options = [x for x in words if x.startswith('--')] return cmd_options @@ -292,6 +300,31 @@ def _fail(module, cmd, out, err): module.fail_json(cmd=cmd, msg=msg) +def _get_package_info(module, package, env=None): + """This is only needed for special packages which do not show up in pip freeze + + pip and setuptools fall into this category. + + :returns: a string containing the version number if the package is + installed. None if the package is not installed. + """ + if env: + opt_dirs = ['%s/bin' % env] + else: + opt_dirs = [] + python_bin = module.get_bin_path('python', False, opt_dirs) + + if python_bin is None: + formatted_dep = None + else: + rc, out, err = module.run_command([python_bin, '-c', _SPECIAL_PACKAGE_CHECKERS[package]]) + if rc: + formatted_dep = None + else: + formatted_dep = '%s==%s' % (package, out.strip()) + return formatted_dep + + def main(): state_map = dict( present='install', @@ -306,9 +339,9 @@ def main(): name=dict(type='list'), version=dict(type='str'), requirements=dict(), - virtualenv=dict(), + virtualenv=dict(type='path'), virtualenv_site_packages=dict(default=False, type='bool'), - virtualenv_command=dict(default='virtualenv'), + virtualenv_command=dict(default='virtualenv', type='path'), virtualenv_python=dict(type='str'), use_mirrors=dict(default=True, type='bool'), extra_args=dict(), @@ -336,7 +369,7 @@ def main(): umask = int(umask, 8) except Exception: module.fail_json(msg="umask must be an octal integer", - details=str(sys.exc_info()[1])) + details=to_native(sys.exc_info()[1])) old_umask = None if umask is not None: @@ -347,23 +380,21 @@ def main(): if chdir is None: # this is done to avoid permissions issues with privilege escalation and virtualenvs - chdir = tempfile.gettempdir() + chdir = tempfile.gettempdir() err = '' out = '' env = module.params['virtualenv'] - virtualenv_command = module.params['virtualenv_command'] if env: - env = os.path.expanduser(env) if not os.path.exists(os.path.join(env, 'bin', 'activate')): if module.check_mode: module.exit_json(changed=True) - cmd = os.path.expanduser(virtualenv_command) + cmd = module.params['virtualenv_command'] if os.path.basename(cmd) == cmd: - cmd = module.get_bin_path(virtualenv_command, True) + cmd = module.get_bin_path(cmd, True) if module.params['virtualenv_site_packages']: cmd += ' --system-site-packages' @@ -438,32 +469,17 @@ def main(): changed = False if name: pkg_list = [p for p in out.split('\n') if not p.startswith('You are using') and not p.startswith('You should consider') and p] + if pkg_cmd.endswith(' freeze') and ('pip' in name or 'setuptools' in name): # Older versions of pip (pre-1.3) do not have pip list. # pip freeze does not list setuptools or pip in its output # So we need to get those via a specialcase - if 'setuptools' in name: - try: - import setuptools - except ImportError: - # Could not import, assume that it is not installed - pass - else: - formatted_dep = 'setuptools==%s' % setuptools.__version__ - pkg_list.append(formatted_dep) - out += '%s\n' % formatted_dep - - if 'pip' in name: - try: - import pkg_resources - except ImportError: - # Could not import pkg_resources. pip requires - # pkg_resources so assume that it is not installed - pass - else: - formatted_dep = 'pip==%s' % pkg_resources.get_distribution('pip').version - pkg_list.append(formatted_dep) - out += '%s\n' % formatted_dep + for pkg in ('setuptools', 'pip'): + if pkg in name: + formatted_dep = _get_package_info(module, pkg, env) + if formatted_dep is not None: + pkg_list.append(formatted_dep) + out += '%s\n' % formatted_dep for pkg in name: is_present = _is_present(pkg, version, pkg_list, pkg_cmd) From 4bd2a409e6409f703005b3ccf2603b9db19db9b0 Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Wed, 21 Sep 2016 09:57:02 -0400 Subject: [PATCH 394/770] Correct reuse_fips param to reuse_ips (#4939) This new parameter was incorrectly named. Fixing to match the expected name in the shade library. --- cloud/openstack/os_server.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index e6c986486e7..ec3b4e83ac1 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -185,7 +185,7 @@ required: false default: false version_added: "2.2" - reuse_fips: + reuse_ips: description: - When I(auto_ip) is true and this option is true, the I(auto_ip) code will attempt to re-use unassigned floating ips in the project before @@ -487,7 +487,7 @@ def _create_server(module, cloud): boot_volume=module.params['boot_volume'], boot_from_volume=module.params['boot_from_volume'], terminate_volume=module.params['terminate_volume'], - reuse_fips=module.params['reuse_fips'], + reuse_ips=module.params['reuse_ips'], wait=module.params['wait'], timeout=module.params['timeout'], **bootkwargs ) @@ -588,7 +588,7 @@ def main(): scheduler_hints = dict(default=None, type='dict'), state = dict(default='present', choices=['absent', 'present']), delete_fip = dict(default=False, type='bool'), - reuse_fips = dict(default=True, type='bool'), + reuse_ips = dict(default=True, type='bool'), ) module_kwargs = openstack_module_kwargs( mutually_exclusive=[ From 4e9e546ef00e96a3e45586f6be5b836b2ff37001 Mon Sep 17 00:00:00 2001 From: shaung <_@shaung.org> Date: Tue, 23 Aug 2016 16:03:16 +0800 Subject: [PATCH 395/770] Fix #4504 Respect timeout option when starting/stopping/restarting containers. --- cloud/docker/docker_service.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index afb2e757b68..2592086edfd 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -466,9 +466,11 @@ from compose.cli.command import project_from_options from compose.service import ConvergenceStrategy from compose.cli.main import convergence_strategy_from_opts, build_action_from_opts, image_type_from_opt + from compose.const import DEFAULT_TIMEOUT except ImportError as exc: HAS_COMPOSE = False HAS_COMPOSE_EXC = str(exc) + DEFAULT_TIMEOUT = 10 from ansible.module_utils.docker_common import * @@ -653,7 +655,8 @@ def cmd_up(self): strategy=converge, do_build=do_build, detached=detached, - remove_orphans=self.remove_orphans) + remove_orphans=self.remove_orphans, + timeout=self.timeout) except Exception as exc: self.client.fail("Error bring %s up - %s" % (self.project.name, str(exc))) @@ -828,7 +831,7 @@ def cmd_stop(self, service_names): if not self.check_mode and result['changed']: try: - self.project.stop(service_names=service_names) + self.project.stop(service_names=service_names, timeout=self.timeout) except Exception as exc: self.client.fail("Error stopping services for %s - %s" % (self.project.name, str(exc))) @@ -855,7 +858,7 @@ def cmd_restart(self, service_names): if not self.check_mode and result['changed']: try: - self.project.restart(service_names=service_names) + self.project.restart(service_names=service_names, timeout=self.timeout) except Exception as exc: self.client.fail("Error restarting services for %s - %s" % (self.project.name, str(exc))) @@ -903,7 +906,8 @@ def main(): dependencies=dict(type='bool', default=True), pull=dict(type='bool', default=False), nocache=dict(type='bool', default=False), - debug=dict(type='bool', default=False) + debug=dict(type='bool', default=False), + timeout=dict(type='int', default=DEFAULT_TIMEOUT) ) mutually_exclusive = [ From c354c974e74d79589b5d08de0365f94aaa03b309 Mon Sep 17 00:00:00 2001 From: Marius Gedminas Date: Wed, 21 Sep 2016 20:14:03 +0300 Subject: [PATCH 396/770] postgresql_db: fix inverted 'changed' logic when state=absent (#4934) Fixes #4933. --- database/postgresql/postgresql_db.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) mode change 100644 => 100755 database/postgresql/postgresql_db.py diff --git a/database/postgresql/postgresql_db.py b/database/postgresql/postgresql_db.py old mode 100644 new mode 100755 index 33e812bc560..64871ed1734 --- a/database/postgresql/postgresql_db.py +++ b/database/postgresql/postgresql_db.py @@ -288,11 +288,11 @@ def main(): try: if module.check_mode: if state == "absent": - changed = not db_exists(cursor, db) + changed = db_exists(cursor, db) elif state == "present": changed = not db_matches(cursor, db, owner, template, encoding, lc_collate, lc_ctype) - module.exit_json(changed=changed,db=db) + module.exit_json(changed=changed, db=db) if state == "absent": try: From 1f741b69f48568976a314e4ea67035074f6bf051 Mon Sep 17 00:00:00 2001 From: JesseEmond Date: Wed, 21 Sep 2016 14:38:05 -0400 Subject: [PATCH 397/770] Unarchive stop passing file mode to tar command (#4179) Fixes #4063. Tar does not use this parameter on extraction (-x) or diff (-d)(the only two cases where it is passed in unarchive). It only uses it on creation: https://www.gnu.org/software/tar/manual/html_section/tar_33.html Providing `unarchive` with a file mode of `0755` (octal) makes it pass the argument `--mode 493` (493 = 0755 in decimal) to `tar`, which then fails while verifying it (because it contains an invalid octal char '9'). Not passing the parameter to tar solves the issue. --- files/unarchive.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index 95bfe36238c..0eb8e49eab4 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -621,8 +621,6 @@ def is_unarchived(self): cmd.append('--owner=' + quote(self.file_args['owner'])) if self.file_args['group']: cmd.append('--group=' + quote(self.file_args['group'])) - if self.file_args['mode']: - cmd.append('--mode=' + quote(self.file_args['mode'])) if self.module.params['keep_newer']: cmd.append('--keep-newer-files') if self.excludes: @@ -670,8 +668,6 @@ def unarchive(self): cmd.append('--owner=' + quote(self.file_args['owner'])) if self.file_args['group']: cmd.append('--group=' + quote(self.file_args['group'])) - if self.file_args['mode']: - cmd.append('--mode=' + quote(self.file_args['mode'])) if self.module.params['keep_newer']: cmd.append('--keep-newer-files') if self.excludes: From cb9fc47cbc4b01fc08506049392e7f48ee99bafb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=B8ren=20Kr=C3=B6ger?= Date: Wed, 21 Sep 2016 20:41:56 +0200 Subject: [PATCH 398/770] Added single_transaction and quick to db_dump (#3687) It was missing before, ... --- database/mysql/mysql_db.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index 9d765e2bc74..a63b0d66ca0 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -317,7 +317,7 @@ def main(): else: rc, stdout, stderr = db_dump(module, login_host, login_user, login_password, db, target, all_databases, - login_port, config_file, socket, ssl_cert, ssl_key, ssl_ca) + login_port, config_file, socket, ssl_cert, ssl_key, ssl_ca, single_transaction, quick) if rc != 0: module.fail_json(msg="%s" % stderr) else: From 0f505378c3be908d322b89717cbab5f7625f8969 Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Wed, 21 Sep 2016 15:04:09 -0400 Subject: [PATCH 399/770] Accept JSON type as the content of policy_json parameter on `iam_policy` module --- cloud/amazon/iam_policy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/iam_policy.py b/cloud/amazon/iam_policy.py index e1cc6b305c9..559ad3c4fda 100644 --- a/cloud/amazon/iam_policy.py +++ b/cloud/amazon/iam_policy.py @@ -282,7 +282,7 @@ def main(): iam_name=dict(default=None, required=False), policy_name=dict(default=None, required=True), policy_document=dict(default=None, required=False), - policy_json=dict(default=None, required=False), + policy_json=dict(type='json', default=None, required=False), skip_duplicates=dict(type='bool', default=True, required=False) )) From b99535818572ea94043c9bd0edd3d8eb703613fc Mon Sep 17 00:00:00 2001 From: Timothy Appnel Date: Thu, 22 Sep 2016 10:49:26 -0400 Subject: [PATCH 400/770] Fixes grammatical error in assert module example (#4972) --- utilities/logic/assert.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utilities/logic/assert.py b/utilities/logic/assert.py index 9600b9d1f92..f37e6bf5da7 100644 --- a/utilities/logic/assert.py +++ b/utilities/logic/assert.py @@ -52,5 +52,5 @@ that: - "my_param <= 100" - "my_param >= 0" - msg: "'my_param' is must be between 0 and 100" + msg: "'my_param' must be between 0 and 100" ''' From ab1ac22187fc047ad49de7458e3f2ce4258dca1c Mon Sep 17 00:00:00 2001 From: Trond Hindenes Date: Fri, 23 Sep 2016 00:31:15 +0200 Subject: [PATCH 401/770] win_feature_docs_update (#4421) --- windows/win_feature.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_feature.py b/windows/win_feature.py index 04226c609a8..c16f3891238 100644 --- a/windows/win_feature.py +++ b/windows/win_feature.py @@ -25,9 +25,9 @@ --- module: win_feature version_added: "1.7" -short_description: Installs and uninstalls Windows Features +short_description: Installs and uninstalls Windows Features on Windows Server description: - - Installs or uninstalls Windows Roles or Features + - Installs or uninstalls Windows Roles or Features on Windows Server. This module uses the Add/Remove-WindowsFeature Cmdlets, which is not available on client os machines. options: name: description: From 8d3119dfe5279926dcd1ce970ad7cdeefe2829c5 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Thu, 22 Sep 2016 16:34:36 -0700 Subject: [PATCH 402/770] Add shellcheck to sanity checks. (#4976) Also disable deprecation warnings during module validation. --- test/utils/shippable/sanity.sh | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 1c0e2451a43..d9234cf0527 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -9,6 +9,10 @@ cd "${source_root}" if [ "${install_deps}" != "" ]; then add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq + apt-add-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports universe' + apt-get update -qq + apt-get install shellcheck + pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing fi @@ -19,4 +23,8 @@ python2.6 -m compileall -fq . python2.7 -m compileall -fq . python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" -ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . +ANSIBLE_DEPRECATION_WARNINGS=false \ + ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . + +shellcheck \ + test/utils/shippable/*.sh From a829ca29ee78f200792b89ffe85f1c4aef156121 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Fri, 23 Sep 2016 15:25:44 +0100 Subject: [PATCH 403/770] Force is not a 2.2 feature https://github.com/ansible/ansible-modules-core/commit/9b5e6bbfa11469fcd9228d46d9695c0b35e20c3d incorrectly chopped some text around --- network/ios/ios_config.py | 1 - 1 file changed, 1 deletion(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 4318df4fb03..bddb9e40ea6 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -110,7 +110,6 @@ required: false default: false choices: ["true", "false"] - version_added: "2.2" commit: description: - This argument specifies the update method to use when applying the From a923689182ff9bcab8a95b72a543e2f675491459 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 23 Sep 2016 12:10:03 -0400 Subject: [PATCH 404/770] fixes bug where setting state=absent in nxos_nxapi is not idempotent (#4984) When setting state=absent the nxos_nxapi module would always try to remove the configuration regardless of the current state of the device. This will fix that problem. This also updates the docstring to correctly reflect https as default=no fixes #4955 depends on ansible/ansible#17728 --- network/nxos/nxos_nxapi.py | 40 +++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/network/nxos/nxos_nxapi.py b/network/nxos/nxos_nxapi.py index feec8a6d80a..3fb2a5c6d88 100644 --- a/network/nxos/nxos_nxapi.py +++ b/network/nxos/nxos_nxapi.py @@ -65,7 +65,7 @@ enable the use of the HTTPS transport, set the value of this argument to True. required: false - default: yes + default: no choices: ['yes', 'no'] aliases: ['enable_https'] sandbox: @@ -151,7 +151,26 @@ def invoke(name, *args, **kwargs): if func: return func(*args, **kwargs) -def present(module, commands): +def get_instance(module): + instance = dict(state='absent') + try: + resp = module.cli('show nxapi', 'json') + except NetworkError: + return instance + + instance['state'] = 'present' + + instance['http'] = 'http_port' in resp[0] + instance['http_port'] = resp[0].get('http_port') or 80 + + instance['https'] = 'https_port' in resp[0] + instance['https_port'] = resp[0].get('https_port') or 443 + + instance['sandbox'] = resp[0]['sandbox_status'] + + return instance + +def present(module, instance, commands): commands.append('feature nxapi') setters = set() for key, value in module.argument_spec.iteritems(): @@ -159,12 +178,13 @@ def present(module, commands): if setter not in setters: setters.add(setter) if module.params[key] is not None: - invoke(setter, module, commands) + invoke(setter, module, instance, commands) -def absent(module, commands): - commands.append('no feature nxapi') +def absent(module, instance, commands): + if instance['state'] != 'absent': + commands.append('no feature nxapi') -def set_http(module, commands): +def set_http(module, instance, commands): port = module.params['http_port'] if not 0 <= port <= 65535: module.fail_json(msg='http_port must be between 1 and 65535') @@ -173,7 +193,7 @@ def set_http(module, commands): elif module.params['http'] is False: commands.append('no nxapi http') -def set_https(module, commands): +def set_https(module, instance, commands): port = module.params['https_port'] if not 0 <= port <= 65535: module.fail_json(msg='https_port must be between 1 and 65535') @@ -182,7 +202,7 @@ def set_https(module, commands): elif module.params['https'] is False: commands.append('no nxapi https') -def set_sandbox(module, commands): +def set_sandbox(module, instance, commands): if module.params['sandbox'] is True: commands.append('nxapi sandbox') elif module.params['sandbox'] is False: @@ -287,7 +307,9 @@ def main(): 'a future release. Please use state=absent instead') commands = list() - invoke(state, module, commands) + instance = get_instance(module) + + invoke(state, module, instance, commands) try: load(module, commands, result) From 1dd2b849dcc123e9c66833e9ab3ee98acc479859 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 23 Sep 2016 12:10:15 -0400 Subject: [PATCH 405/770] fixes default value in docstring for http (#4985) The docstring incorrectly stated the default value for http is yes when indeed its no. This fixes the docstring --- network/eos/eos_eapi.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/eos/eos_eapi.py b/network/eos/eos_eapi.py index cd08cca12c8..8087a8d0da6 100644 --- a/network/eos/eos_eapi.py +++ b/network/eos/eos_eapi.py @@ -44,7 +44,7 @@ By default, when eAPI is first configured, the HTTP protocol is disabled. required: false - default: yes + default: no choices: ['yes', 'no'] aliases: ['enable_http'] http_port: From d843204575952a57e7e866bf21f93d6eb957ce04 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 23 Sep 2016 12:36:33 -0700 Subject: [PATCH 406/770] Working subset of mount fixes (#4987) * Fixing bind mount on Linux * The latest update from jtyr doesn't pass integration tests. Manually select the changes that are necessary to fix the bug with unmounting --- system/mount.py | 477 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 317 insertions(+), 160 deletions(-) diff --git a/system/mount.py b/system/mount.py index 0178bd32d7d..38785cd9303 100644 --- a/system/mount.py +++ b/system/mount.py @@ -20,56 +20,77 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import get_platform +from ansible.module_utils.ismount import ismount +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import iteritems +import os +import re + + DOCUMENTATION = ''' --- module: mount short_description: Control active and configured mount points description: - - This module controls active and configured mount points in C(/etc/fstab). + - This module controls active and configured mount points in C(/etc/fstab). +author: + - Ansible Core Team + - Seth Vidal version_added: "0.6" options: name: description: - - "path to the mount point, eg: C(/mnt/files)" + - Path to the mount point (e.g. C(/mnt/files)) required: true src: description: - - device to be mounted on I(name). Required when C(state=present) or C(state=mounted) + - Device to be mounted on I(name). Required when I(state) set to + C(present) or C(mounted). required: false default: null fstype: description: - - file-system type. Required when C(state=present) or C(state=mounted) + - Filesystem type. Required when I(state) is C(present) or C(mounted). required: false default: null opts: description: - - mount options (see fstab(5), or vfstab(4) on Solaris) + - Mount options (see fstab(5), or vfstab(4) on Solaris). required: false default: null dump: description: - - "dump (see fstab(5)), Note that if nulled, C(state=present) will cease to work and duplicate entries will be made with subsequent runs." + - Dump (see fstab(5)). Note that if set to C(null) and I(state) set to + C(present), it will cease to work and duplicate entries will be made + with subsequent runs. - Has no effect on Solaris systems. required: false default: 0 passno: description: - - "passno (see fstab(5)), Note that if nulled, C(state=present) will cease to work and duplicate entries will be made with subsequent runs." + - Passno (see fstab(5)). Note that if set to C(null) and I(state) set to + C(present), it will cease to work and duplicate entries will be made + with subsequent runs. - Deprecated on Solaris systems. required: false default: 0 state: description: - - If C(mounted) or C(unmounted), the device will be actively mounted or unmounted as needed and appropriately configured in I(fstab). - - C(absent) and C(present) only deal with I(fstab) but will not affect current mounting. - - If specifying C(mounted) and the mount point is not present, the mount point will be created. Similarly. + - If C(mounted) or C(unmounted), the device will be actively mounted or + unmounted as needed and appropriately configured in I(fstab). + - C(absent) and C(present) only deal with I(fstab) but will not affect + current mounting. + - If specifying C(mounted) and the mount point is not present, the mount + point will be created. Similarly. - Specifying C(absent) will remove the mount point directory. required: true - choices: [ "present", "absent", "mounted", "unmounted" ] + choices: ["present", "absent", "mounted", "unmounted"] fstab: description: - - file to use instead of C(/etc/fstab). You shouldn't use that option + - File to use instead of C(/etc/fstab). You shouldn't use that option unless you really know what you are doing. This might be useful if you need to configure mountpoints in a chroot environment. required: false @@ -77,65 +98,92 @@ boot: version_added: 2.2 description: - - Determines if the filesystem should be mounted on boot. Only applies to Solaris systems. + - Determines if the filesystem should be mounted on boot. + - Only applies to Solaris systems. required: false default: yes - choices: [ "yes", "no" ] - -author: - - Ansible Core Team - - Seth Vidal + choices: ["yes", "no"] ''' -EXAMPLES = ''' -# Mount DVD read-only -- mount: name=/mnt/dvd src=/dev/sr0 fstype=iso9660 opts=ro state=present - -# Mount up device by label -- mount: name=/srv/disk src='LABEL=SOME_LABEL' fstype=ext4 state=present -# Mount up device by UUID -- mount: name=/home src='UUID=b3e48f45-f933-4c8e-a700-22a159ec9077' fstype=xfs opts=noatime state=present +EXAMPLES = ''' +- name: Mount DVD read-only + mount: + name: /mnt/dvd + src: /dev/sr0 + fstype: iso9660 + opts: ro + state: present + +- name: Mount up device by label + mount: + name: /srv/disk + src: LABEL=SOME_LABEL + fstype: ext4 + state: present + +- name: Mount up device by UUID + mount: + name: /home + src: UUID=b3e48f45-f933-4c8e-a700-22a159ec9077 + fstype: xfs + opts: noatime + state: present ''' -from ansible.module_utils.six import iteritems def write_fstab(lines, dest): - fs_w = open(dest, 'w') + for l in lines: fs_w.write(l) fs_w.flush() fs_w.close() + def _escape_fstab(v): - """ escape space (040), ampersand (046) and backslash (134) which are invalid in fstab fields """ + """Escape invalid characters in fstab fields. + + space (040) + ampersand (046) + backslash (134) + """ + if isinstance(v, int): return v else: - return v.replace('\\', '\\134').replace(' ', '\\040').replace('&', '\\046') + return( + v. + replace('\\', '\\134'). + replace(' ', '\\040'). + replace('&', '\\046')) + def set_mount(module, **kwargs): - """ set/change a mount point location in fstab """ + """Set/change a mount point location in fstab.""" - # solaris kwargs: name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab - # linux kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + # solaris kwargs: + # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux: + # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab if get_platform() == 'SunOS': args = dict( - opts = '-', - passno = '-', - fstab = '/etc/vfstab', - boot = 'yes' + opts='-', + passno='-', + fstab='/etc/vfstab', + boot='yes' ) - new_line = '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n' + new_line = ( + '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n') else: args = dict( - opts = 'defaults', - dump = '0', - passno = '0', - fstab = '/etc/fstab' + opts='defaults', + dump='0', + passno='0', + fstab='/etc/fstab' ) - new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n' + new_line = ( + '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n') args.update(kwargs) to_write = [] @@ -145,35 +193,57 @@ def set_mount(module, **kwargs): for line in open(args['fstab'], 'r').readlines(): if not line.strip(): to_write.append(line) + continue + if line.strip().startswith('#'): to_write.append(line) + continue + if len(line.split()) != 6 and get_platform() != 'SunOS': - # not sure what this is or why it is here - # but it is not our fault so leave it be + # Not sure what this is or why it is here but it is not our fault + # so leave it be to_write.append(line) + continue ld = {} if get_platform() == 'SunOS': - ld['src'], dash, ld['name'], ld['fstype'], ld['passno'], ld['boot'], ld['opts'] = line.split() + ( + ld['src'], + dash, + ld['name'], + ld['fstype'], + ld['passno'], + ld['boot'], + ld['opts'] + ) = line.split() else: - ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() + ( + ld['src'], + ld['name'], + ld['fstype'], + ld['opts'], + ld['dump'], + ld['passno'] + ) = line.split() if ld['name'] != escaped_args['name']: to_write.append(line) + continue - # it exists - now see if what we have is different + # It exists - now see if what we have is different exists = True + if get_platform() == 'SunOS': - for t in ('src', 'fstype','passno', 'boot', 'opts'): + for t in ('src', 'fstype', 'passno', 'boot', 'opts'): if ld[t] != escaped_args[t]: changed = True ld[t] = escaped_args[t] else: - for t in ('src', 'fstype','opts', 'dump', 'passno'): + for t in ('src', 'fstype', 'opts', 'dump', 'passno'): if ld[t] != escaped_args[t]: changed = True ld[t] = escaped_args[t] @@ -194,23 +264,25 @@ def set_mount(module, **kwargs): def unset_mount(module, **kwargs): - """ remove a mount point from fstab """ + """Remove a mount point from fstab.""" - # solaris kwargs: name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab - # linux kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + # solaris kwargs: + # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux kwargs: + # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab if get_platform() == 'SunOS': args = dict( - opts = '-', - passno = '-', - fstab = '/etc/vfstab', - boot = 'yes' + opts='-', + passno='-', + fstab='/etc/vfstab', + boot='yes' ) else: args = dict( - opts = 'default', - dump = '0', - passno = '0', - fstab = '/etc/fstab' + opts='default', + dump='0', + passno='0', + fstab='/etc/fstab' ) args.update(kwargs) @@ -220,27 +292,49 @@ def unset_mount(module, **kwargs): for line in open(args['fstab'], 'r').readlines(): if not line.strip(): to_write.append(line) + continue + if line.strip().startswith('#'): to_write.append(line) + continue + if len(line.split()) != 6 and get_platform() != 'SunOS': - # not sure what this is or why it is here - # but it is not our fault so leave it be + # Not sure what this is or why it is here but it is not our fault + # so leave it be to_write.append(line) + continue ld = {} + if get_platform() == 'SunOS': - ld['src'], dash, ld['name'], ld['fstype'], ld['passno'], ld['boot'], ld['opts'] = line.split() + ( + ld['src'], + dash, + ld['name'], + ld['fstype'], + ld['passno'], + ld['boot'], + ld['opts'] + ) = line.split() else: - ld['src'], ld['name'], ld['fstype'], ld['opts'], ld['dump'], ld['passno'] = line.split() + ( + ld['src'], + ld['name'], + ld['fstype'], + ld['opts'], + ld['dump'], + ld['passno'] + ) = line.split() if ld['name'] != escaped_name: to_write.append(line) + continue - # if we got here we found a match - continue and mark changed + # If we got here we found a match - continue and mark changed changed = True if changed and not module.check_mode: @@ -250,87 +344,146 @@ def unset_mount(module, **kwargs): def mount(module, **kwargs): - """ mount up a path or remount if needed """ + """Mount up a path or remount if needed.""" - # solaris kwargs: name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab - # linux kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + # solaris kwargs: + # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux kwargs: + # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab if get_platform() == 'SunOS': args = dict( - opts = '-', - passno = '-', - fstab = '/etc/vfstab', - boot = 'yes' + opts='-', + passno='-', + fstab='/etc/vfstab', + boot='yes' ) else: args = dict( - opts = 'default', - dump = '0', - passno = '0', - fstab = '/etc/fstab' + opts='default', + dump='0', + passno='0', + fstab='/etc/fstab' ) args.update(kwargs) - mount_bin = module.get_bin_path('mount') - + mount_bin = module.get_bin_path('mount', required=True) name = kwargs['name'] - - cmd = [ mount_bin, ] + cmd = [mount_bin] if ismount(name): - cmd += [ '-o', 'remount', ] + cmd += ['-o', 'remount'] if get_platform().lower() == 'freebsd': - cmd += [ '-F', args['fstab'], ] + cmd += ['-F', args['fstab']] + elif get_platform().lower() == 'linux': + cmd += ['-T', args['fstab']] - if get_platform().lower() == 'linux': - cmd += [ '-T', args['fstab'], ] - - cmd += [ name, ] + cmd += [name] rc, out, err = module.run_command(cmd) + if rc == 0: return 0, '' else: return rc, out+err + def umount(module, **kwargs): - """ unmount a path """ + """Unmount a path.""" - umount_bin = module.get_bin_path('umount') + umount_bin = module.get_bin_path('umount', required=True) name = kwargs['name'] cmd = [umount_bin, name] rc, out, err = module.run_command(cmd) + if rc == 0: return 0, '' else: return rc, out+err -def main(): +# Note if we wanted to put this into module_utils we'd have to get permission +# from @jupeter -- https://github.com/ansible/ansible-modules-core/pull/2923 +# @jtyr -- https://github.com/ansible/ansible-modules-core/issues/4439 +# and @abadger to relicense from GPLv3+ +def is_bind_mounted(module, dest, src=None, fstype=None): + """Return whether the dest is bind mounted + + :arg module: The AnsibleModule (used for helper functions) + :arg dest: The directory to be mounted under. This is the primary means + of identifying whether the destination is mounted. + :kwarg src: The source directory. If specified, this is used to help + ensure that we are detecting that the correct source is mounted there. + :kwarg fstype: The filesystem type. If specified this is also used to + help ensure that we are detecting the right mount. + :returns: True if the dest is mounted with src otherwise False. + """ + + is_mounted = False + bin_path = module.get_bin_path('mount', required=True) + cmd = '%s -l' % bin_path + + if get_platform().lower() == 'linux': + bin_path = module.get_bin_path('findmnt', required=True) + cmd = '%s -nr %s' % (bin_path, dest) + + rc, out, err = module.run_command(cmd) + mounts = [] + + if len(out): + mounts = out.strip().split('\n') + + mount_pattern = re.compile('\[(.*)\]') + + for mnt in mounts: + arguments = mnt.split() + + if get_platform().lower() == 'linux': + result = mount_pattern.search(arguments[1]) + + if len(result.groups()) == 1: + if arguments[0] == dest: + is_mounted = True + elif ( + (arguments[0] == src or src is None) and + arguments[2] == dest and + (arguments[4] == fstype or fstype is None)): + is_mounted = True + + if is_mounted: + break + + return is_mounted + + +def main(): module = AnsibleModule( - argument_spec = dict( - state = dict(required=True, choices=['present', 'absent', 'mounted', 'unmounted']), - name = dict(required=True), - opts = dict(default=None), - passno = dict(default=None, type='str'), - dump = dict(default=None), - src = dict(required=False), - fstype = dict(required=False), - boot = dict(default='yes', choices=['yes', 'no']), - fstab = dict(default='/etc/fstab') + argument_spec=dict( + boot=dict(default='yes', choices=['yes', 'no']), + dump=dict(), + fstab=dict(default='/etc/fstab'), + fstype=dict(), + name=dict(required=True, type='path'), + opts=dict(), + passno=dict(type='str'), + src=dict(type='path'), + state=dict( + required=True, + choices=['present', 'absent', 'mounted', 'unmounted']), ), supports_check_mode=True, - required_if = ( + required_if=( ['state', 'mounted', ['src', 'fstype']], ['state', 'present', ['src', 'fstype']] ) ) - changed = False - rc = 0 - args = {'name': module.params['name']} + args = { + 'name': module.params['name'] + } + if module.params['src'] is not None: args['src'] = module.params['src'] if module.params['fstype'] is not None: @@ -346,27 +499,37 @@ def main(): elif module.params['fstab'] is not None: args['fstab'] = module.params['fstab'] - # if fstab file does not exist, we first need to create it. This mainly - # happens when fstab optin is passed to the module. + # If fstab file does not exist, we first need to create it. This mainly + # happens when fstab option is passed to the module. if not os.path.exists(args['fstab']): if not os.path.exists(os.path.dirname(args['fstab'])): os.makedirs(os.path.dirname(args['fstab'])) - open(args['fstab'],'a').close() - # absent == remove from fstab and unmounted - # unmounted == do not change fstab state, but unmount - # present == add to fstab, do not change mount state - # mounted == add to fstab if not there and make sure it is mounted, if it has changed in fstab then remount it + open(args['fstab'], 'a').close() + + # absent: + # Remove from fstab and unmounted. + # unmounted: + # Do not change fstab state, but unmount. + # present: + # Add to fstab, do not change mount state. + # mounted: + # Add to fstab if not there and make sure it is mounted. If it has + # changed in fstab then remount it. state = module.params['state'] - name = module.params['name'] + name = module.params['name'] + if state == 'absent': name, changed = unset_mount(module, **args) + if changed and not module.check_mode: - if ismount(name): - res,msg = umount(module, **args) + if ismount(name) or is_bind_mounted(module, name): + res, msg = umount(module, **args) + if res: - module.fail_json(msg="Error unmounting %s: %s" % (name, msg)) + module.fail_json( + msg="Error unmounting %s: %s" % (name, msg)) if os.path.exists(name): try: @@ -374,61 +537,55 @@ def main(): except (OSError, IOError): e = get_exception() module.fail_json(msg="Error rmdir %s: %s" % (name, str(e))) - - module.exit_json(changed=changed, **args) - - if state == 'unmounted': - if ismount(name): + elif state == 'unmounted': + if ismount(name) or is_bind_mounted(module, name): if not module.check_mode: - res,msg = umount(module, **args) - if res: - module.fail_json(msg="Error unmounting %s: %s" % (name, msg)) - changed = True + res, msg = umount(module, **args) - module.exit_json(changed=changed, **args) + if res: + module.fail_json( + msg="Error unmounting %s: %s" % (name, msg)) - if state in ['mounted', 'present']: - if state == 'mounted': - if not os.path.exists(name) and not module.check_mode: - try: - os.makedirs(name) - except (OSError, IOError): - e = get_exception() - module.fail_json(msg="Error making dir %s: %s" % (name, str(e))) + changed = True + elif state == 'mounted': + if not os.path.exists(name) and not module.check_mode: + try: + os.makedirs(name) + except (OSError, IOError): + e = get_exception() + module.fail_json( + msg="Error making dir %s: %s" % (name, str(e))) name, changed = set_mount(module, **args) - if state == 'mounted': - res = 0 - if ismount(name): - if changed and not module.check_mode: - res,msg = mount(module, **args) - elif 'bind' in args.get('opts', []): - changed = True - cmd = 'mount -l' - rc, out, err = module.run_command(cmd) - allmounts = out.split('\n') - for mounts in allmounts[:-1]: - arguments = mounts.split() - if arguments[0] == args['src'] and arguments[2] == args['name'] and arguments[4] == args['fstype']: - changed = False - if changed: - res,msg = mount(module, **args) - else: + res = 0 + + if ismount(name): + if not module.check_mode: + res, msg = mount(module, **args) changed = True - if not module.check_mode: - res,msg = mount(module, **args) + elif 'bind' in args.get('opts', []): + changed = True + if is_bind_mounted(module, name, args['src'], args['fstype']): + changed = False - if res: - module.fail_json(msg="Error mounting %s: %s" % (name, msg)) + if changed and not module.check_mode: + res, msg = mount(module, **args) + else: + changed = True + if not module.check_mode: + res, msg = mount(module, **args) - module.exit_json(changed=changed, **args) + if res: + module.fail_json(msg="Error mounting %s: %s" % (name, msg)) + elif state == 'present': + name, changed = set_mount(module, **args) + else: + module.fail_json(msg='Unexpected position reached') - module.fail_json(msg='Unexpected position reached') + module.exit_json(changed=changed, **args) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ismount import * -main() +if __name__ == '__main__': + main() From 13c7288aeaf00f9eea441576cf4475a6f993168a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 23 Sep 2016 16:09:14 -0400 Subject: [PATCH 407/770] fixes exception raised when nxos_facts uses nxapi transport (#4988) This fixes a condition where an exception is raised when collecting `interface` facts and the transport is set to nxapi in the nxos_nxapi module. fixes ansible/ansible#17691 --- network/nxos/nxos_facts.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_facts.py b/network/nxos/nxos_facts.py index af8df350710..ff9c454aaa6 100644 --- a/network/nxos/nxos_facts.py +++ b/network/nxos/nxos_facts.py @@ -276,11 +276,11 @@ class Interfaces(FactsBase): def commands(self): add_command(self.runner, 'show interface', output='json') - resp = self.module.cli(['show ipv6 interface | wc lines']) - if int(resp[0]) > 1: + try: + self.module.cli('show ipv6 interface', 'json') add_command(self.runner, 'show ipv6 interface', output='json') self.ipv6 = True - else: + except NetworkError: self.ipv6 = False try: From edf361a5d4b45f2e7c7df1a133d5a09391508db2 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 23 Sep 2016 14:31:06 -0700 Subject: [PATCH 408/770] Fix #3153 again (#4989) --- system/mount.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/mount.py b/system/mount.py index 38785cd9303..1033c3f80ab 100644 --- a/system/mount.py +++ b/system/mount.py @@ -375,7 +375,7 @@ def mount(module, **kwargs): if get_platform().lower() == 'freebsd': cmd += ['-F', args['fstab']] - elif get_platform().lower() == 'linux': + elif get_platform().lower() == 'linux' and args['fstab'] != '/etc/fstab': cmd += ['-T', args['fstab']] cmd += [name] From 74120442f2b9ba34f1129b434994106d64ef04cb Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 23 Sep 2016 15:09:32 -0700 Subject: [PATCH 409/770] Fix handling of ansible-doc errors. (#4992) --- test/utils/shippable/docs.sh | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/test/utils/shippable/docs.sh b/test/utils/shippable/docs.sh index 9b5a6164f64..2858f87c997 100755 --- a/test/utils/shippable/docs.sh +++ b/test/utils/shippable/docs.sh @@ -42,10 +42,12 @@ pip list source hacking/env-setup +docs_status=0 + PAGER=/bin/cat \ ANSIBLE_DEPRECATION_WARNINGS=false \ bin/ansible-doc -l \ - 2>/tmp/ansible-doc.err + 2>/tmp/ansible-doc.err || docs_status=$? if [ -s /tmp/ansible-doc.err ]; then # report warnings as errors @@ -53,3 +55,8 @@ if [ -s /tmp/ansible-doc.err ]; then cat /tmp/ansible-doc.err exit 1 fi + +if [ "${docs_status}" -ne 0 ]; then + echo "Running 'ansible-doc -l' failed with no output on stderr and exit code: ${docs_status}" + exit 1 +fi From fb779bf30cfe4bbe15b2fe685974caeca71b941c Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 23 Sep 2016 16:05:14 -0700 Subject: [PATCH 410/770] Set PRIVILEGED=true for Linux integration tests. (#4993) This should allow test_mount tests to run on Shippable. --- shippable.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shippable.yml b/shippable.yml index a8dd0fc5a9f..d78203de3b9 100644 --- a/shippable.yml +++ b/shippable.yml @@ -8,16 +8,16 @@ matrix: exclude: - env: TEST=none include: - - env: TEST=integration IMAGE=ansible/ansible:centos6 - - env: TEST=integration IMAGE=ansible/ansible:centos7 - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide - - env: TEST=integration IMAGE=ansible/ansible:fedora23 - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap + - env: TEST=integration IMAGE=ansible/ansible:centos6 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 From e8ed51bce41f26c6928c2e5c98b0a2b738dc3861 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Fri, 23 Sep 2016 23:38:58 -0700 Subject: [PATCH 411/770] Revert "Set PRIVILEGED=true for Linux integration tests. (#4993)" This reverts commit fb779bf30cfe4bbe15b2fe685974caeca71b941c. --- shippable.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shippable.yml b/shippable.yml index d78203de3b9..a8dd0fc5a9f 100644 --- a/shippable.yml +++ b/shippable.yml @@ -8,16 +8,16 @@ matrix: exclude: - env: TEST=none include: - - env: TEST=integration IMAGE=ansible/ansible:centos6 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:centos6 + - env: TEST=integration IMAGE=ansible/ansible:centos7 + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide + - env: TEST=integration IMAGE=ansible/ansible:fedora23 + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 From cf243860ff1750314623c387c694b27e7598beb3 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 24 Sep 2016 06:28:34 -0700 Subject: [PATCH 412/770] Update minimum required version and ssh rate limit (#4995) --- network/dellos9/dellos9_command.py | 9 +++++++++ network/dellos9/dellos9_config.py | 8 ++++++++ network/dellos9/dellos9_facts.py | 7 +++++++ network/dellos9/dellos9_template.py | 7 +++++++ 4 files changed, 31 insertions(+) diff --git a/network/dellos9/dellos9_command.py b/network/dellos9/dellos9_command.py index a36c83a7b72..cc610855839 100755 --- a/network/dellos9/dellos9_command.py +++ b/network/dellos9/dellos9_command.py @@ -68,6 +68,15 @@ trying the command again. required: false default: 1 + +notes: + - This module requires Dell OS9 version 9.10.0.1P13 or above. + + - This module requires to increase the ssh connection rate limit. + Use the following command I(ip ssh connection-rate-limit 60) + to configure the same. This can be done via M(dnos_config) module + as well. + """ EXAMPLES = """ diff --git a/network/dellos9/dellos9_config.py b/network/dellos9/dellos9_config.py index 1d728c5319b..39ae652b305 100755 --- a/network/dellos9/dellos9_config.py +++ b/network/dellos9/dellos9_config.py @@ -139,6 +139,14 @@ required: false default: no choices: ['yes', 'no'] + +notes: + - This module requires Dell OS9 version 9.10.0.1P13 or above. + + - This module requires to increase the ssh connection rate limit. + Use the following command I(ip ssh connection-rate-limit 60) + to configure the same. This can be done via M(dnos_config) module + as well. """ EXAMPLES = """ diff --git a/network/dellos9/dellos9_facts.py b/network/dellos9/dellos9_facts.py index 4b8b2a83a44..c1946a773fc 100644 --- a/network/dellos9/dellos9_facts.py +++ b/network/dellos9/dellos9_facts.py @@ -43,6 +43,13 @@ not be collected. required: false default: '!config' +notes: + - This module requires Dell OS9 version 9.10.0.1P13 or above. + + - This module requires to increase the ssh connection rate limit. + Use the following command I(ip ssh connection-rate-limit 60) + to configure the same. This can be done via M(dnos_config) module + as well. """ EXAMPLES = """ diff --git a/network/dellos9/dellos9_template.py b/network/dellos9/dellos9_template.py index e4ec7364549..60cd00ab1a6 100755 --- a/network/dellos9/dellos9_template.py +++ b/network/dellos9/dellos9_template.py @@ -76,6 +76,13 @@ required: false default: null +notes: + - This module requires Dell OS9 version 9.10.0.1P13 or above. + + - This module requires to increase the ssh connection rate limit. + Use the following command I(ip ssh connection-rate-limit 60) + to configure the same. This can be done via M(dnos_config) module + as well. """ EXAMPLES = """ From 622ae9c67e1c784996d3ce5ea7632ddd1808c138 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Mon, 26 Sep 2016 07:58:14 -0600 Subject: [PATCH 413/770] Match existing INI file entry with leading whitespace (fixes #4997) (#4998) --- files/ini_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 2fc3d96f5f3..e26949a923f 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -109,7 +109,7 @@ def match_opt(option, line): option = re.escape(option) - return re.match('%s( |\t)*=' % option, line) \ + return re.match(' *%s( |\t)*=' % option, line) \ or re.match('# *%s( |\t)*=' % option, line) \ or re.match('; *%s( |\t)*=' % option, line) @@ -118,7 +118,7 @@ def match_opt(option, line): def match_active_opt(option, line): option = re.escape(option) - return re.match('%s( |\t)*=' % option, line) + return re.match(' *%s( |\t)*=' % option, line) # ============================================================== # do_ini From 25585937fb66395065a583591f763e5cfe41ca10 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Mon, 26 Sep 2016 19:01:44 +0100 Subject: [PATCH 414/770] Fixing bind mount on Linux (#1942) (#4439) * Fixing bind mount on Linux * Fixing broken implementation --- system/mount.py | 224 ++++++++++++++++++++++-------------------------- 1 file changed, 101 insertions(+), 123 deletions(-) diff --git a/system/mount.py b/system/mount.py index 1033c3f80ab..26b8eb566f3 100644 --- a/system/mount.py +++ b/system/mount.py @@ -21,6 +21,7 @@ # along with Ansible. If not, see . +from ansible.module_utils._text import to_native from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.basic import get_platform from ansible.module_utils.ismount import ismount @@ -84,8 +85,8 @@ - C(absent) and C(present) only deal with I(fstab) but will not affect current mounting. - If specifying C(mounted) and the mount point is not present, the mount - point will be created. Similarly. - - Specifying C(absent) will remove the mount point directory. + point will be created. + - Similarly, specifying C(absent) will remove the mount point directory. required: true choices: ["present", "absent", "mounted", "unmounted"] fstab: @@ -159,37 +160,19 @@ def _escape_fstab(v): replace('&', '\\046')) -def set_mount(module, **kwargs): +def set_mount(module, args): """Set/change a mount point location in fstab.""" - # solaris kwargs: - # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab - # linux: - # kwargs: name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - if get_platform() == 'SunOS': - args = dict( - opts='-', - passno='-', - fstab='/etc/vfstab', - boot='yes' - ) - new_line = ( - '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n') - else: - args = dict( - opts='defaults', - dump='0', - passno='0', - fstab='/etc/fstab' - ) - new_line = ( - '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n') - args.update(kwargs) - to_write = [] exists = False changed = False escaped_args = dict([(k, _escape_fstab(v)) for k, v in iteritems(args)]) + new_line = '%(src)s %(name)s %(fstype)s %(opts)s %(dump)s %(passno)s\n' + + if get_platform() == 'SunOS': + new_line = ( + '%(src)s - %(name)s %(fstype)s %(passno)s %(boot)s %(opts)s\n') + for line in open(args['fstab'], 'r').readlines(): if not line.strip(): to_write.append(line) @@ -201,14 +184,16 @@ def set_mount(module, **kwargs): continue - if len(line.split()) != 6 and get_platform() != 'SunOS': - # Not sure what this is or why it is here but it is not our fault - # so leave it be + # Check if we got a valid line for splitting + if ( + get_platform() == 'SunOS' and len(line.split()) != 7 or + get_platform() != 'SunOS' and len(line.split()) != 6): to_write.append(line) continue ld = {} + if get_platform() == 'SunOS': ( ld['src'], @@ -229,24 +214,24 @@ def set_mount(module, **kwargs): ld['passno'] ) = line.split() + # Check if we found the correct line if ld['name'] != escaped_args['name']: to_write.append(line) continue - # It exists - now see if what we have is different + # If we got here we found a match - let's check if there is any + # difference exists = True + args_to_check = ('src', 'fstype', 'opts', 'dump', 'passno') if get_platform() == 'SunOS': - for t in ('src', 'fstype', 'passno', 'boot', 'opts'): - if ld[t] != escaped_args[t]: - changed = True - ld[t] = escaped_args[t] - else: - for t in ('src', 'fstype', 'opts', 'dump', 'passno'): - if ld[t] != escaped_args[t]: - changed = True - ld[t] = escaped_args[t] + args_to_check = ('src', 'fstype', 'passno', 'boot', 'opts') + + for t in args_to_check: + if ld[t] != escaped_args[t]: + ld[t] = escaped_args[t] + changed = True if changed: to_write.append(new_line % ld) @@ -263,32 +248,13 @@ def set_mount(module, **kwargs): return (args['name'], changed) -def unset_mount(module, **kwargs): +def unset_mount(module, args): """Remove a mount point from fstab.""" - # solaris kwargs: - # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab - # linux kwargs: - # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - if get_platform() == 'SunOS': - args = dict( - opts='-', - passno='-', - fstab='/etc/vfstab', - boot='yes' - ) - else: - args = dict( - opts='default', - dump='0', - passno='0', - fstab='/etc/fstab' - ) - args.update(kwargs) - to_write = [] changed = False escaped_name = _escape_fstab(args['name']) + for line in open(args['fstab'], 'r').readlines(): if not line.strip(): to_write.append(line) @@ -300,9 +266,10 @@ def unset_mount(module, **kwargs): continue - if len(line.split()) != 6 and get_platform() != 'SunOS': - # Not sure what this is or why it is here but it is not our fault - # so leave it be + # Check if we got a valid line for splitting + if ( + get_platform() == 'SunOS' and len(line.split()) != 7 or + get_platform() != 'SunOS' and len(line.split()) != 6): to_write.append(line) continue @@ -343,40 +310,21 @@ def unset_mount(module, **kwargs): return (args['name'], changed) -def mount(module, **kwargs): +def mount(module, args): """Mount up a path or remount if needed.""" - # solaris kwargs: - # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab - # linux kwargs: - # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - if get_platform() == 'SunOS': - args = dict( - opts='-', - passno='-', - fstab='/etc/vfstab', - boot='yes' - ) - else: - args = dict( - opts='default', - dump='0', - passno='0', - fstab='/etc/fstab' - ) - args.update(kwargs) - mount_bin = module.get_bin_path('mount', required=True) - name = kwargs['name'] + name = args['name'] cmd = [mount_bin] if ismount(name): cmd += ['-o', 'remount'] - if get_platform().lower() == 'freebsd': - cmd += ['-F', args['fstab']] - elif get_platform().lower() == 'linux' and args['fstab'] != '/etc/fstab': - cmd += ['-T', args['fstab']] + if args['fstab'] != '/etc/fstab': + if get_platform() == 'FreeBSD': + cmd += ['-F', args['fstab']] + elif get_platform() == 'Linux': + cmd += ['-T', args['fstab']] cmd += [name] @@ -388,12 +336,11 @@ def mount(module, **kwargs): return rc, out+err -def umount(module, **kwargs): +def umount(module, dest): """Unmount a path.""" umount_bin = module.get_bin_path('umount', required=True) - name = kwargs['name'] - cmd = [umount_bin, name] + cmd = [umount_bin, dest] rc, out, err = module.run_command(cmd) @@ -424,7 +371,7 @@ def is_bind_mounted(module, dest, src=None, fstype=None): bin_path = module.get_bin_path('mount', required=True) cmd = '%s -l' % bin_path - if get_platform().lower() == 'linux': + if get_platform() == 'Linux': bin_path = module.get_bin_path('findmnt', required=True) cmd = '%s -nr %s' % (bin_path, dest) @@ -432,19 +379,37 @@ def is_bind_mounted(module, dest, src=None, fstype=None): mounts = [] if len(out): - mounts = out.strip().split('\n') + mounts = to_native(out).strip().split('\n') mount_pattern = re.compile('\[(.*)\]') for mnt in mounts: arguments = mnt.split() - if get_platform().lower() == 'linux': + if get_platform() == 'Linux': + source = arguments[1] result = mount_pattern.search(arguments[1]) - if len(result.groups()) == 1: + # This is only for LVM and tmpfs mounts + if result is not None and len(result.groups()) == 1: + source = result.group(1) + + if src is None: + # That's for unmounted/absent if arguments[0] == dest: is_mounted = True + else: + # That's for mounted + if arguments[0] == dest and source == src: + is_mounted = True + elif arguments[0] == dest and src.endswith(source): + # Check if it's tmpfs mount + sub_path = src[:len(src)-len(source)] + + if ( + is_bind_mounted(module, sub_path, 'tmpfs') and + source == src[len(sub_path):]): + is_mounted = True elif ( (arguments[0] == src or src is None) and arguments[2] == dest and @@ -480,24 +445,37 @@ def main(): ) changed = False - args = { - 'name': module.params['name'] - } - - if module.params['src'] is not None: - args['src'] = module.params['src'] - if module.params['fstype'] is not None: - args['fstype'] = module.params['fstype'] - if module.params['passno'] is not None: - args['passno'] = module.params['passno'] - if module.params['opts'] is not None: - args['opts'] = module.params['opts'] - if module.params['dump'] is not None: - args['dump'] = module.params['dump'] - if get_platform() == 'SunOS' and module.params['fstab'] == '/etc/fstab': + # solaris args: + # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab + # linux args: + # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab + if get_platform() == 'SunOS': + args = dict( + name=module.params['name'], + opts='-', + passno='-', + fstab='/etc/vfstab', + boot='yes' + ) + else: + args = dict( + name=module.params['name'], + opts='default', + dump='0', + passno='0', + fstab='/etc/fstab' + ) + + # FreeBSD doesn't have any 'default' so set 'rw' instead + if get_platform() == 'FreeBSD': + args['opts'] = 'rw' + + for key in ('src', 'fstype', 'passno', 'opts', 'dump', 'fstab'): + if module.params[key] is not None: + args[key] = module.params[key] + + if get_platform() == 'SunOS' and args['fstab'] == '/etc/fstab': args['fstab'] = '/etc/vfstab' - elif module.params['fstab'] is not None: - args['fstab'] = module.params['fstab'] # If fstab file does not exist, we first need to create it. This mainly # happens when fstab option is passed to the module. @@ -521,11 +499,11 @@ def main(): name = module.params['name'] if state == 'absent': - name, changed = unset_mount(module, **args) + name, changed = unset_mount(module, args) if changed and not module.check_mode: if ismount(name) or is_bind_mounted(module, name): - res, msg = umount(module, **args) + res, msg = umount(module, name) if res: module.fail_json( @@ -540,7 +518,7 @@ def main(): elif state == 'unmounted': if ismount(name) or is_bind_mounted(module, name): if not module.check_mode: - res, msg = umount(module, **args) + res, msg = umount(module, name) if res: module.fail_json( @@ -556,12 +534,12 @@ def main(): module.fail_json( msg="Error making dir %s: %s" % (name, str(e))) - name, changed = set_mount(module, **args) + name, changed = set_mount(module, args) res = 0 if ismount(name): - if not module.check_mode: - res, msg = mount(module, **args) + if changed and not module.check_mode: + res, msg = mount(module, args) changed = True elif 'bind' in args.get('opts', []): changed = True @@ -570,17 +548,17 @@ def main(): changed = False if changed and not module.check_mode: - res, msg = mount(module, **args) + res, msg = mount(module, args) else: changed = True if not module.check_mode: - res, msg = mount(module, **args) + res, msg = mount(module, args) if res: module.fail_json(msg="Error mounting %s: %s" % (name, msg)) elif state == 'present': - name, changed = set_mount(module, **args) + name, changed = set_mount(module, args) else: module.fail_json(msg='Unexpected position reached') From b2c6d39beca57b16e3d601acb85686ac3794da30 Mon Sep 17 00:00:00 2001 From: Denis Tiago Date: Thu, 14 Jul 2016 21:21:07 -0300 Subject: [PATCH 415/770] fix health instances count when we have more than one lb in asg --- cloud/amazon/ec2_asg.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index cc52e3de1fc..d9ed229522b 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -332,7 +332,7 @@ def elb_dreg(asg_connection, module, group_name, instance_id): def elb_healthy(asg_connection, elb_connection, module, group_name): - healthy_instances = [] + healthy_instances = set() as_group = asg_connection.get_all_groups(names=[group_name])[0] props = get_properties(as_group) # get healthy, inservice instances from ASG @@ -355,7 +355,7 @@ def elb_healthy(asg_connection, elb_connection, module, group_name): for i in lb_instances: if i.state == "InService": - healthy_instances.append(i.instance_id) + healthy_instances.add(i.instance_id) log.debug("{0}: {1}".format(i.instance_id, i.state)) return len(healthy_instances) From 57350f84cd3c752a05289d950eadac1f2b0a0633 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 26 Sep 2016 17:45:35 -0400 Subject: [PATCH 416/770] fixes return passing output from command through jxmlease in junos_command (#5044) The return string from the commands was not being passed through the jxmlease library and therefore being returned as a string instead of a json data structure. This also adds back the missing xml key in the return that includes the raw xml string. fixes #5001 --- network/junos/junos_command.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index 3286b0614e7..fd81285824e 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -151,15 +151,23 @@ retured: failed type: list sample: ['...', '...'] + +xml: + description: The raw XML reply from the device + returned: when format is xml + type: list + sample: [['...', '...'], ['...', '...']] """ import re import ansible.module_utils.junos + from ansible.module_utils.basic import get_exception from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError +from ansible.module_utils.junos import xml_to_json VALID_KEYS = { 'cli': frozenset(['command', 'output', 'prompt', 'response']), @@ -270,15 +278,19 @@ def main(): module.fail_json(msg=str(exc)) result = dict(changed=False, stdout=list()) + xmlout = list() for cmd in commands: try: output = runner.get_command(cmd['command'], cmd.get('output')) + xmlout.append(output) + output = xml_to_json(output) except ValueError: output = 'command not executed due to check_mode, see warnings' result['stdout'].append(output) result['warnings'] = warnings + result['xml'] = xmlout result['stdout_lines'] = list(to_lines(result['stdout'])) module.exit_json(**result) From 87f47a44c93b553bb01e0683372900e78e64af05 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 26 Sep 2016 22:43:05 -0400 Subject: [PATCH 417/770] fixes exception being raised when show configuration command issued (#5047) The junos_command expects commands to be returned as xml by default but `show configuration [options]` will return text not xml. This fix will set the output format for any command that starts with `show configuration` to text fixes #4628 --- network/junos/junos_command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index fd81285824e..faf6a4772e2 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -205,6 +205,10 @@ def parse(module, command_type): item['command_type'] = command_type + # show configuration [options] will return as text + if item['command'].startswith('show configuration'): + item['output'] = 'text' + parsed.append(item) return parsed From 84020b39ee79143291a6cc559fc4960b1f2804a9 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Mon, 26 Sep 2016 23:30:44 -0400 Subject: [PATCH 418/770] Put requested devices in correct format to enable config comparison. Fixes #5000. --- cloud/docker/docker_container.py | 34 +++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index 1768f9629ba..6af74a617c4 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -1181,6 +1181,7 @@ def has_different_configuration(self, image): self.parameters.expected_etc_hosts = self._convert_simple_dict_to_list('etc_hosts') self.parameters.expected_env = self._get_expected_env(image) self.parameters.expected_cmd = self._get_expected_cmd() + self.parameters.expected_devices = self._get_expected_devices() if not self.container.get('HostConfig'): self.fail("has_config_diff: Error parsing container properties. HostConfig missing.") @@ -1208,7 +1209,7 @@ def has_different_configuration(self, image): detach=detach, interactive=config.get('OpenStdin'), capabilities=host_config.get('CapAdd'), - devices=host_config.get('Devices'), + expected_devices=host_config.get('Devices'), dns_servers=host_config.get('Dns'), dns_opts=host_config.get('DnsOptions'), dns_search_domains=host_config.get('DnsSearch'), @@ -1433,6 +1434,37 @@ def has_extra_networks(self): extra_networks.append(dict(name=network, id=network_config['NetworkID'])) return extra, extra_networks + def _get_expected_devices(self): + if not self.parameters.devices: + return None + expected_devices = [] + for device in self.parameters.devices: + parts = device.split(':') + if len(parts) == 1: + expected_devices.append( + dict( + CgroupPermissions='rwm', + PathInContainer=parts[0], + PathOnHost=parts[0] + )) + elif len(parts) == 2: + parts = device.split(':') + expected_devices.append( + dict( + CgroupPermissions='rwm', + PathInContainer=parts[1], + PathOnHost=parts[0] + ) + ) + else: + expected_devices.append( + dict( + CgroupPermissions=parts[2], + PathInContainer=parts[1], + PathOnHost=parts[0] + )) + return expected_devices + def _get_expected_entrypoint(self): self.log('_get_expected_entrypoint') if not self.parameters.entrypoint: From 523780a7ca78108829aeaff59754edb7d96ba053 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 27 Sep 2016 00:36:11 -0400 Subject: [PATCH 419/770] Let docker-py handle decoding and JSON parsing of stream data. Fixes #4930. --- cloud/docker/docker_image.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/cloud/docker/docker_image.py b/cloud/docker/docker_image.py index 0a9ab09096f..7d97d984190 100644 --- a/cloud/docker/docker_image.py +++ b/cloud/docker/docker_image.py @@ -424,8 +424,7 @@ def push_image(self, name, tag=None): if not self.check_mode: status = None try: - for line in self.client.push(repository, tag=tag, stream=True): - line = json.loads(line) + for line in self.client.push(repository, tag=tag, stream=True, decode=True): self.log(line, pretty_print=True) if line.get('errorDetail'): raise Exception(line['errorDetail']['message']) From bdab56efa832d0d4a964ddc9c125c1bf5b7bd64e Mon Sep 17 00:00:00 2001 From: Nathaniel Case Date: Tue, 27 Sep 2016 13:23:50 -0400 Subject: [PATCH 420/770] eos_facts cleanup (#5057) * dict.iteritems does not exist in Python 3 Now just dict.items six.iteritems handles the change * Addresses point 1 Unsure if this is a good idea or not. * Addresses point 2 This shouldn't have any particular change, just marks load_comments as abstract * Remove unused import Addresses point 3 * Clarify invalid subset error message Addresses point 4 --- network/eos/eos_facts.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/network/eos/eos_facts.py b/network/eos/eos_facts.py index 89cc699a418..aa9145367ef 100644 --- a/network/eos/eos_facts.py +++ b/network/eos/eos_facts.py @@ -141,8 +141,8 @@ """ import re -from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, AddCommandError +from ansible.module_utils.six import iteritems from ansible.module_utils.eos import NetworkModule @@ -163,6 +163,10 @@ def __init__(self, runner): self.load_commands() + def load_commands(self): + raise NotImplementedError + + class Default(FactsBase): SYSTEM_MAP = { @@ -178,7 +182,7 @@ def load_commands(self): def populate(self): data = self.runner.get_command('show version', 'json') - for key, value in self.SYSTEM_MAP.iteritems(): + for key, value in iteritems(self.SYSTEM_MAP): if key in data: self.facts[value] = data[key] @@ -256,10 +260,10 @@ def populate(self): def populate_interfaces(self, data): facts = dict() - for key, value in data['interfaces'].iteritems(): + for key, value in iteritems(data['interfaces']): intf = dict() - for remote, local in self.INTERFACE_MAP.iteritems(): + for remote, local in iteritems(self.INTERFACE_MAP): if remote in value: intf[local] = value[remote] @@ -336,7 +340,8 @@ def main(): exclude = False if subset not in VALID_SUBSETS: - module.fail_json(msg='Bad subset') + module.fail_json(msg='Subset must be one of [%s], got %s' % + (', '.join(VALID_SUBSETS), subset)) if exclude: exclude_subsets.add(subset) @@ -365,11 +370,10 @@ def main(): inst.populate() facts.update(inst.facts) except Exception: - raise module.exit_json(out=module.from_json(runner.items)) ansible_facts = dict() - for key, value in facts.iteritems(): + for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value @@ -378,4 +382,3 @@ def main(): if __name__ == '__main__': main() - From 0a7ebef14ebb5fb0182866c231367b0ab1f613a8 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 27 Sep 2016 10:46:28 -0700 Subject: [PATCH 421/770] Detect tar type (bsd, gnu) and only use gnu tar. (#4352) * Detect tar type (bsd, gnu) and only use gnu tar. * Revert return code checking for TgzArchive. --- files/unarchive.py | 40 +++++++++++++++++++++++++++++++--------- 1 file changed, 31 insertions(+), 9 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index 0eb8e49eab4..8af1c4106ce 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -558,12 +558,12 @@ def unarchive(self): def can_handle_archive(self): if not self.cmd_path: - return False + return False, 'Command "unzip" not found.' cmd = [ self.cmd_path, '-l', self.src ] rc, out, err = self.module.run_command(cmd) if rc == 0: - return True - return False + return True, None + return False, 'Command "%s" could not handle archive.' % self.cmd_path # class to handle gzipped tar files @@ -586,6 +586,21 @@ def __init__(self, src, dest, file_args, module): self.zipflag = '-z' self._files_in_archive = [] + if self.cmd_path: + self.tar_type = self._get_tar_type() + else: + self.tar_type = None + + def _get_tar_type(self): + cmd = [self.cmd_path, '--version'] + (rc, out, err) = self.module.run_command(cmd) + tar_type = None + if out.startswith('bsdtar'): + tar_type = 'bsd' + elif out.startswith('tar') and 'GNU' in out: + tar_type = 'gnu' + return tar_type + @property def files_in_archive(self, force_refresh=False): if self._files_in_archive and not force_refresh: @@ -678,16 +693,19 @@ def unarchive(self): def can_handle_archive(self): if not self.cmd_path: - return False + return False, 'Commands "gtar" and "tar" not found.' + + if self.tar_type != 'gnu': + return False, 'Command "%s" detected as tar type %s. GNU tar required.' % (self.cmd_path, self.tar_type) try: if self.files_in_archive: - return True + return True, None except UnarchiveError: - pass + return False, 'Command "%s" could not handle archive.' % self.cmd_path # Errors and no files in archive assume that we weren't able to # properly unarchive it - return False + return False, 'Command "%s" found no files in archive.' % self.cmd_path # class to handle tar files that aren't compressed @@ -715,11 +733,15 @@ def __init__(self, src, dest, file_args, module): # try handlers in order and return the one that works or bail if none work def pick_handler(src, dest, file_args, module): handlers = [ZipArchive, TgzArchive, TarArchive, TarBzipArchive, TarXzArchive] + reasons = set() for handler in handlers: obj = handler(src, dest, file_args, module) - if obj.can_handle_archive(): + (can_handle, reason) = obj.can_handle_archive() + if can_handle: return obj - module.fail_json(msg='Failed to find handler for "%s". Make sure the required command to extract the file is installed.' % src) + reasons.add(reason) + reason_msg = ' '.join(reasons) + module.fail_json(msg='Failed to find handler for "%s". Make sure the required command to extract the file is installed. %s' % (src, reason_msg)) def main(): From 7994115bbc0272940e294cf7f9eddb667b04b69a Mon Sep 17 00:00:00 2001 From: David Shrewsbury Date: Tue, 27 Sep 2016 15:03:21 -0400 Subject: [PATCH 422/770] Fix os_network's create_network() call for older shade versions (#5058) A value for the project_id parameter to shade's create_network() call was always being sent, even if no value for 'project' was supplied. This was breaking folks with older versions of shade (< 1.6). Fixes PR https://github.com/ansible/ansible-modules-core/issues/3567 --- cloud/openstack/os_network.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_network.py b/cloud/openstack/os_network.py index 9a0c2516310..d80267e8937 100644 --- a/cloud/openstack/os_network.py +++ b/cloud/openstack/os_network.py @@ -222,8 +222,12 @@ def main(): if provider and StrictVersion(shade.__version__) < StrictVersion('1.5.0'): module.fail_json(msg="Shade >= 1.5.0 required to use provider options") - net = cloud.create_network(name, shared, admin_state_up, - external, provider, project_id) + if project_id is not None: + net = cloud.create_network(name, shared, admin_state_up, + external, provider, project_id) + else: + net = cloud.create_network(name, shared, admin_state_up, + external, provider) changed = True else: changed = False From cb29cbf6bc566b49aaf1bbd82a2e2d618aa584a4 Mon Sep 17 00:00:00 2001 From: Nathaniel Case Date: Tue, 27 Sep 2016 15:20:18 -0400 Subject: [PATCH 423/770] Fix imports in junos_template (#5059) --- network/junos/junos_template.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/network/junos/junos_template.py b/network/junos/junos_template.py index 2f8e64d5b5e..ec242a63d0b 100644 --- a/network/junos/junos_template.py +++ b/network/junos/junos_template.py @@ -100,7 +100,8 @@ src: config.j2 action: overwrite """ -from ansible.module_utils.junos import NetworkModule +from ansible.module_utils.network import NetworkModule +import ansible.module_utils.junos DEFAULT_COMMENT = 'configured by junos_template' From 9a0add286f466e8608095e18a19052fd6f79789b Mon Sep 17 00:00:00 2001 From: Nathaniel Case Date: Tue, 27 Sep 2016 17:06:03 -0400 Subject: [PATCH 424/770] Network module code cleanup (#5061) * Fix imports in junos_template * Python 3 compatibility in eos_command * Python 3 compatibility for ios_command * Clean up issues with ios_facts * Python 3 compatibility for ios_facts * Import shuffle in ios_template * Python 3 compatibility for iosxr_command * Clean up iosxr_facts.py * Python 3 compatibility for iosxr_facts * Python 3 compatibility for junos_command * Python 3 compatibility for ops_command * Cleanup issues with ops_facts * Python 3 compatibility for ops_facts * Cleanup issues with ops_template * Python 3 compatibility for vyos_command * Cleanup issues with vyos_facts * Python 3 compatibility for vyos_facts --- network/eos/eos_command.py | 5 +++-- network/ios/ios_command.py | 8 +++++--- network/ios/ios_facts.py | 20 +++++++++++++------- network/ios/ios_template.py | 3 ++- network/iosxr/iosxr_command.py | 8 +++++--- network/iosxr/iosxr_facts.py | 20 ++++++++++++-------- network/junos/junos_command.py | 9 +++------ network/openswitch/ops_command.py | 9 +++++---- network/openswitch/ops_facts.py | 11 +++++++---- network/openswitch/ops_template.py | 5 +++-- network/vyos/vyos_command.py | 8 +++++--- network/vyos/vyos_facts.py | 16 +++++++++++----- 12 files changed, 74 insertions(+), 48 deletions(-) diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index b0ab3286b6f..a506075b269 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -152,18 +152,19 @@ from ansible.module_utils.netcli import AddCommandError from ansible.module_utils.netcli import FailedConditionsError from ansible.module_utils.netcli import FailedConditionalError +from ansible.module_utils.six import string_types VALID_KEYS = ['command', 'output', 'prompt', 'response'] def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item def parse_commands(module): for cmd in module.params['commands']: - if isinstance(cmd, basestring): + if isinstance(cmd, string_types): cmd = dict(command=cmd, output=None) elif 'command' not in cmd: module.fail_json(msg='command keyword argument is required') diff --git a/network/ios/ios_command.py b/network/ios/ios_command.py index 5f2e12780b0..4204a73a682 100644 --- a/network/ios/ios_command.py +++ b/network/ios/ios_command.py @@ -139,22 +139,24 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.ios from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.ios import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.six import string_types VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item def parse_commands(module): for cmd in module.params['commands']: - if isinstance(cmd, basestring): + if isinstance(cmd, string_types): cmd = dict(command=cmd, output=None) elif 'command' not in cmd: module.fail_json(msg='command keyword argument is required') diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py index 884e9b5b296..699637d9c02 100644 --- a/network/ios/ios_facts.py +++ b/network/ios/ios_facts.py @@ -126,9 +126,11 @@ import re import itertools -from ansible.module_utils.basic import get_exception +import ansible.module_utils.ios from ansible.module_utils.netcli import CommandRunner, AddCommandError -from ansible.module_utils.ios import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.six.moves import zip def add_command(runner, command): @@ -147,6 +149,9 @@ def __init__(self, runner): self.commands() + def commands(self): + raise NotImplementedError + class Default(FactsBase): def commands(self): @@ -199,7 +204,7 @@ def populate(self): self.facts['filesystems'] = self.parse_filesystems(data) data = self.runner.get_command('show memory statistics | include Processor') - match = re.findall('\s(\d+)\s', data) + match = re.findall(r'\s(\d+)\s', data) if match: self.facts['memtotal_mb'] = int(match[0]) / 1024 self.facts['memfree_mb'] = int(match[1]) / 1024 @@ -244,7 +249,7 @@ def populate(self): def populate_interfaces(self, interfaces): facts = dict() - for key, value in interfaces.iteritems(): + for key, value in iteritems(interfaces): intf = dict() intf['description'] = self.parse_description(value) intf['macaddress'] = self.parse_macaddress(value) @@ -266,11 +271,11 @@ def populate_interfaces(self, interfaces): return facts def populate_ipv6_interfaces(self, data): - for key, value in data.iteritems(): + for key, value in iteritems(data): self.facts['interfaces'][key]['ipv6'] = list() addresses = re.findall(r'\s+(.+), subnet', value, re.M) subnets = re.findall(r', subnet is (.+)$', value, re.M) - for addr, subnet in itertools.izip(addresses, subnets): + for addr, subnet in zip(addresses, subnets): ipv6 = dict(address=addr.strip(), subnet=subnet.strip()) self.add_ip_address(addr.strip(), 'ipv6') self.facts['interfaces'][key]['ipv6'].append(ipv6) @@ -297,6 +302,7 @@ def parse_neighbors(self, neighbors): def parse_interfaces(self, data): parsed = dict() + key = '' for line in data.split('\n'): if len(line) == 0: continue @@ -444,7 +450,7 @@ def main(): module.exit_json(out=module.from_json(runner.items)) ansible_facts = dict() - for key, value in facts.iteritems(): + for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value diff --git a/network/ios/ios_template.py b/network/ios/ios_template.py index 52e82f26e3f..90b9554e8a3 100644 --- a/network/ios/ios_template.py +++ b/network/ios/ios_template.py @@ -114,8 +114,9 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.ios from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.ios import NetworkModule, NetworkError +from ansible.module_utils.ios import NetworkModule def get_config(module): config = module.params['config'] or dict() diff --git a/network/iosxr/iosxr_command.py b/network/iosxr/iosxr_command.py index 1d6acc887b8..b66d1ec3f05 100644 --- a/network/iosxr/iosxr_command.py +++ b/network/iosxr/iosxr_command.py @@ -138,22 +138,24 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.iosxr from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.iosxr import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.six import string_types VALID_KEYS = ['command', 'output', 'prompt', 'response'] def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item def parse_commands(module): for cmd in module.params['commands']: - if isinstance(cmd, basestring): + if isinstance(cmd, string_types): cmd = dict(command=cmd, output=None) elif 'command' not in cmd: module.fail_json(msg='command keyword argument is required') diff --git a/network/iosxr/iosxr_facts.py b/network/iosxr/iosxr_facts.py index f045080ee63..7af1023ffd6 100644 --- a/network/iosxr/iosxr_facts.py +++ b/network/iosxr/iosxr_facts.py @@ -116,11 +116,12 @@ type: dict """ import re -import itertools -from ansible.module_utils.basic import get_exception +import ansible.module_utils.iosxr from ansible.module_utils.netcli import CommandRunner, AddCommandError -from ansible.module_utils.iosxr import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.six import iteritems +from ansible.module_utils.six.moves import zip def add_command(runner, command): @@ -139,6 +140,9 @@ def __init__(self, runner): self.commands() + def commands(self): + raise NotImplementedError + class Default(FactsBase): def commands(self): @@ -223,7 +227,7 @@ def populate(self): def populate_interfaces(self, interfaces): facts = dict() - for key, value in interfaces.iteritems(): + for key, value in iteritems(interfaces): intf = dict() intf['description'] = self.parse_description(value) intf['macaddress'] = self.parse_macaddress(value) @@ -244,11 +248,11 @@ def populate_interfaces(self, interfaces): return facts def populate_ipv6_interfaces(self, data): - for key, value in data.iteritems(): + for key, value in iteritems(data): self.facts['interfaces'][key]['ipv6'] = list() addresses = re.findall(r'\s+(.+), subnet', value, re.M) subnets = re.findall(r', subnet is (.+)$', value, re.M) - for addr, subnet in itertools.izip(addresses, subnets): + for addr, subnet in zip(addresses, subnets): ipv6 = dict(address=addr.strip(), subnet=subnet.strip()) self.add_ip_address(addr.strip(), 'ipv6') self.facts['interfaces'][key]['ipv6'].append(ipv6) @@ -276,6 +280,7 @@ def parse_neighbors(self, neighbors): def parse_interfaces(self, data): parsed = dict() + key = '' for line in data.split('\n'): if len(line) == 0: continue @@ -416,11 +421,10 @@ def main(): inst.populate() facts.update(inst.facts) except Exception: - raise module.exit_json(out=module.from_json(runner.items)) ansible_facts = dict() - for key, value in facts.iteritems(): + for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index faf6a4772e2..fb80f0acad2 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -158,16 +158,14 @@ type: list sample: [['...', '...'], ['...', '...']] """ -import re import ansible.module_utils.junos - - from ansible.module_utils.basic import get_exception from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError from ansible.module_utils.junos import xml_to_json +from ansible.module_utils.six import string_types VALID_KEYS = { 'cli': frozenset(['command', 'output', 'prompt', 'response']), @@ -177,7 +175,7 @@ def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item @@ -189,7 +187,7 @@ def parse(module, command_type): parsed = list() for item in (items or list()): - if isinstance(item, basestring): + if isinstance(item, string_types): item = dict(command=item, output=None) elif 'command' not in item: module.fail_json(msg='command keyword argument is required') @@ -302,4 +300,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/network/openswitch/ops_command.py b/network/openswitch/ops_command.py index f74ef6191fb..43dab36a839 100644 --- a/network/openswitch/ops_command.py +++ b/network/openswitch/ops_command.py @@ -126,22 +126,24 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.openswitch from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.openswitch import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.six import string_types VALID_KEYS = ['command', 'prompt', 'response'] def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item def parse_commands(module): for cmd in module.params['commands']: - if isinstance(cmd, basestring): + if isinstance(cmd, string_types): cmd = dict(command=cmd, output=None) elif 'command' not in cmd: module.fail_json(msg='command keyword argument is required') @@ -220,4 +222,3 @@ def main(): if __name__ == '__main__': main() - diff --git a/network/openswitch/ops_facts.py b/network/openswitch/ops_facts.py index 162dc710f5e..5be10b38d40 100644 --- a/network/openswitch/ops_facts.py +++ b/network/openswitch/ops_facts.py @@ -171,9 +171,10 @@ """ import re -from ansible.module_utils.basic import get_exception +import ansible.module_utils.openswitch from ansible.module_utils.netcli import CommandRunner, AddCommandError -from ansible.module_utils.openswitch import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.six import iteritems def add_command(runner, command): @@ -196,6 +197,9 @@ def __init__(self, module, runner): if self.transport == 'cli': self.commands() + def commands(self): + raise NotImplementedError + def populate(self): getattr(self, self.transport)() @@ -395,11 +399,10 @@ def main(): inst.populate() facts.update(inst.facts) except Exception: - raise module.exit_json(out=module.from_json(runner.items)) ansible_facts = dict() - for key, value in facts.iteritems(): + for key, value in iteritems(facts): # this is to maintain capability with ops_facts 2.1 if key.startswith('_'): ansible_facts[key[1:]] = value diff --git a/network/openswitch/ops_template.py b/network/openswitch/ops_template.py index 48f21c0ce9c..a47abda3a5e 100644 --- a/network/openswitch/ops_template.py +++ b/network/openswitch/ops_template.py @@ -95,10 +95,11 @@ type: list sample: [...] """ -import copy +import ansible.module_utils.openswitch from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.openswitch import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.openswitch import HAS_OPS def get_config(module): diff --git a/network/vyos/vyos_command.py b/network/vyos/vyos_command.py index 49e41b6127c..0d09c4320f2 100644 --- a/network/vyos/vyos_command.py +++ b/network/vyos/vyos_command.py @@ -126,22 +126,24 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.vyos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.vyos import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.six import string_types VALID_KEYS = ['command', 'output', 'prompt', 'response'] def to_lines(stdout): for item in stdout: - if isinstance(item, basestring): + if isinstance(item, string_types): item = str(item).split('\n') yield item def parse_commands(module): for cmd in module.params['commands']: - if isinstance(cmd, basestring): + if isinstance(cmd, string_types): cmd = dict(command=cmd, output=None) elif 'command' not in cmd: module.fail_json(msg='command keyword argument is required') diff --git a/network/vyos/vyos_facts.py b/network/vyos/vyos_facts.py index 3c09f36505a..5be19a78e4d 100644 --- a/network/vyos/vyos_facts.py +++ b/network/vyos/vyos_facts.py @@ -100,8 +100,11 @@ """ import re -from ansible.module_utils.netcmd import CommandRunner -from ansible.module_utils.vyos import NetworkModule +import ansible.module_utils.vyos +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.netcli import CommandRunner +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.six import iteritems class FactsBase(object): @@ -112,6 +115,9 @@ def __init__(self, runner): self.commands() + def commands(self): + raise NotImplementedError + class Default(FactsBase): @@ -160,7 +166,7 @@ def populate(self): entry = None for line in commits.split('\n'): - match = re.match('(\d+)\s+(.+)by(.+)via(.+)', line) + match = re.match(r'(\d+)\s+(.+)by(.+)via(.+)', line) if match: if entry: entries.append(entry) @@ -288,7 +294,7 @@ def main(): for key in runable_subsets: instances.append(FACT_SUBSETS[key](runner)) - runner.run_commands() + runner.run() try: for inst in instances: @@ -299,7 +305,7 @@ def main(): module.fail_json(msg='unknown failure', output=runner.items, exc=str(exc)) ansible_facts = dict() - for key, value in facts.iteritems(): + for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value From a1c3ce0ad17f080e009ad9c8f1cc6173e2b67575 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 27 Sep 2016 18:59:19 -0400 Subject: [PATCH 425/770] fixes error when 'delete ...' config command is specified but doesn't exist (#5064) The junos_config module will generate an exception if a 'delete ...' config command is attempted to be loaded into a device configuration. This change will first check to see if the delete command is valid and filter it out of the change set if it is not valid. fixes #5040 --- network/junos/junos_config.py | 48 +++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index d7458d956c7..30a2286097b 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -166,6 +166,7 @@ from ansible.module_utils.basic import get_exception from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig DEFAULT_COMMENT = 'configured by junos_config' @@ -189,6 +190,46 @@ def guess_format(config): return 'text' +def config_to_commands(config): + set_format = config.startswith('set') or config.startswith('delete') + candidate = NetworkConfig(indent=4, contents=config, device_os='junos') + if not set_format: + candidate = [c.line for c in candidate.items] + commands = list() + # this filters out less specific lines + for item in candidate: + for index, entry in enumerate(commands): + if item.startswith(entry): + del commands[index] + break + commands.append(item) + + else: + commands = str(candidate).split('\n') + + return commands + +def diff_commands(commands, config): + config = [str(c).replace("'", '') for c in config] + + updates = list() + visited = set() + + for item in commands: + if not item.startswith('set') and not item.startswith('delete'): + raise ValueError('line must start with either `set` or `delete`') + + elif item.startswith('set') and item[4:] not in config: + updates.append(item) + + elif item.startswith('delete'): + for entry in config: + if entry.startswith(item[7:]) and item not in visited: + updates.append(item) + visited.add(item) + + return updates + def load_config(module, result): candidate = module.params['lines'] or module.params['src'] @@ -204,6 +245,13 @@ def load_config(module, result): config_format = 'set' kwargs['config_format'] = config_format + # this is done to filter out `delete ...` statements which map to + # nothing in the config as that will cause an exception to be raised + if config_format == 'set': + config = module.config.get_config() + config = config_to_commands(config) + candidate = diff_commands(candidate, config) + diff = module.config.load_config(candidate, **kwargs) if diff: From 340ff310757f133ac47b8bcbc68e610982cd0b7a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 27 Sep 2016 20:59:40 -0400 Subject: [PATCH 426/770] catches exception if conditional cannot be parsed (#5067) If the conditional cannot be parsed, the module will now catch the exception and return a well formed failed message. fixes #5060 --- network/junos/junos_command.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index fb80f0acad2..f370ace3d8a 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -263,8 +263,12 @@ def main(): exc = get_exception() warnings.append('duplicate command detected: %s' % cmd) - for item in conditionals: - runner.add_conditional(item) + try: + for item in conditionals: + runner.add_conditional(item) + except (ValueError, AttributeError): + exc = get_exception() + module.fail_json(msg=str(exc)) runner.retries = module.params['retries'] runner.interval = module.params['interval'] From f3a2fb3dae36fbf03e899d616a5d947ffaf1ac55 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 27 Sep 2016 22:17:32 -0400 Subject: [PATCH 427/770] fixes exception raised due to KeyError (#5068) The vyos_config module would error when looking for a key called `updates` in module.params. There is no such key. This fixes the problem. --- network/vyos/vyos_config.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/vyos/vyos_config.py b/network/vyos/vyos_config.py index 706f64911fc..7ee3a272bdf 100644 --- a/network/vyos/vyos_config.py +++ b/network/vyos/vyos_config.py @@ -244,12 +244,11 @@ def run(module, result): result['updates'] = updates - if module.params['update'] != 'check': - load_config(module, updates, result) + load_config(module, updates, result) - if result.get('filtered'): - result['warnings'].append('Some configuration commands where ' - 'removed, please see the filtered key') + if result.get('filtered'): + result['warnings'].append('Some configuration commands where ' + 'removed, please see the filtered key') def main(): From 9b2ee881d9470e1db0c3c7eef4cbb2fa28002db2 Mon Sep 17 00:00:00 2001 From: Kevin Carter Date: Wed, 28 Sep 2016 04:40:02 -0500 Subject: [PATCH 428/770] apt: fix cache time handling (#1517) This change is in response to issue #1497 where the apt module would not properly updating the apt cache in some situations and never returned a state change on cache update when the module was used without or without an item to be installed or upgraded. The change simply allows the apt module to update the cache when update_cache option is used without or without a set cache_valid_time. If cache_valid_time is set and the on disk mtime for apt cache is ">" the provided amount of seconds, which now has a default of 0, the apt cache will be updated. Additionally if no upgrade, package, or deb is installed or changed but the apt cache is updated the module will return a changed state which will help users to know that the state of the environment has changed due to a task operation, even if it was only an apt cache update. fixes #1497 Signed-off-by: Kevin Carter --- packaging/os/apt.py | 112 ++++++++++++++++++++++++++++---------------- 1 file changed, 72 insertions(+), 40 deletions(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 29b8fb8f84e..eeaf4aa61c3 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -47,9 +47,9 @@ choices: [ "yes", "no" ] cache_valid_time: description: - - If C(update_cache) is specified and the last run is less or equal than I(cache_valid_time) seconds ago, the C(update_cache) gets skipped. + - Update the apt cache if its older than the I(cache_valid_time). This option is set in seconds. required: false - default: no + default: 0 purge: description: - Will force purging of configuration files if the module state is set to I(absent). @@ -696,12 +696,37 @@ def download(module, deb): return deb +def get_cache_mtime(): + """Return mtime of a valid apt cache file. + Stat the apt cache file and if no cache file is found return 0 + :returns: ``int`` + """ + if os.path.exists(APT_UPDATE_SUCCESS_STAMP_PATH): + return os.stat(APT_UPDATE_SUCCESS_STAMP_PATH).st_mtime + elif os.path.exists(APT_LISTS_PATH): + return os.stat(APT_LISTS_PATH).st_mtime + else: + return 0 + + +def get_updated_cache_time(): + """Return the mtime time stamp and the updated cache time. + Always retrieve the mtime of the apt cache or set the `cache_mtime` + variable to 0 + :returns: ``tuple`` + """ + cache_mtime = get_cache_mtime() + mtimestamp = datetime.datetime.fromtimestamp(cache_mtime) + updated_cache_time = int(time.mktime(mtimestamp.timetuple())) + return mtimestamp, updated_cache_time + + def main(): module = AnsibleModule( argument_spec = dict( state = dict(default='present', choices=['installed', 'latest', 'removed', 'absent', 'present', 'build-dep']), update_cache = dict(default=False, aliases=['update-cache'], type='bool'), - cache_valid_time = dict(type='int'), + cache_valid_time = dict(type='int', default=0), purge = dict(default=False, type='bool'), package = dict(default=None, aliases=['pkg', 'name'], type='list'), deb = dict(default=None, type='path'), @@ -772,30 +797,16 @@ def main(): # reopen cache w/ modified config cache.open(progress=None) + + mtimestamp, updated_cache_time = get_updated_cache_time() + # Cache valid time is default 0, which will update the cache if + # needed and `update_cache` was set to true + updated_cache = False if p['update_cache']: - # Default is: always update the cache - cache_valid = False now = datetime.datetime.now() - if p.get('cache_valid_time', False): - try: - mtime = os.stat(APT_UPDATE_SUCCESS_STAMP_PATH).st_mtime - except: - # Looks like the update-success-stamp is not available - # Fallback: Checking the mtime of the lists - try: - mtime = os.stat(APT_LISTS_PATH).st_mtime - except: - # No mtime could be read. We update the cache to be safe - mtime = False - - if mtime: - tdelta = datetime.timedelta(seconds=p['cache_valid_time']) - mtimestamp = datetime.datetime.fromtimestamp(mtime) - if mtimestamp + tdelta >= now: - cache_valid = True - updated_cache_time = int(time.mktime(mtimestamp.timetuple())) - - if cache_valid is not True: + tdelta = datetime.timedelta(seconds=p['cache_valid_time']) + if not mtimestamp + tdelta >= now: + # Retry to update the cache up to 3 times for retry in range(3): try: cache.update() @@ -806,12 +817,16 @@ def main(): module.fail_json(msg='Failed to update apt cache.') cache.open(progress=None) updated_cache = True - updated_cache_time = int(time.mktime(now.timetuple())) + mtimestamp, updated_cache_time = get_updated_cache_time() + + # If theres nothing else to do exit. This will set state as + # changed based on if the cache was updated. if not p['package'] and not p['upgrade'] and not p['deb']: - module.exit_json(changed=False, cache_updated=updated_cache, cache_update_time=updated_cache_time) - else: - updated_cache = False - updated_cache_time = 0 + module.exit_json( + changed=updated_cache, + cache_updated=updated_cache, + cache_update_time=updated_cache_time + ) force_yes = p['force'] @@ -843,16 +858,33 @@ def main(): state_upgrade = True if p['state'] == 'build-dep': state_builddep = True - result = install(module, packages, cache, upgrade=state_upgrade, - default_release=p['default_release'], - install_recommends=install_recommends, - force=force_yes, dpkg_options=dpkg_options, - build_dep=state_builddep, autoremove=autoremove, - only_upgrade=p['only_upgrade'], - allow_unauthenticated=allow_unauthenticated) - (success, retvals) = result - retvals['cache_updated']=updated_cache - retvals['cache_update_time']=updated_cache_time + + success, retvals = install( + module, + packages, + cache, + upgrade=state_upgrade, + default_release=p['default_release'], + install_recommends=install_recommends, + force=force_yes, + dpkg_options=dpkg_options, + build_dep=state_builddep, + autoremove=autoremove, + only_upgrade=p['only_upgrade'], + allow_unauthenticated=allow_unauthenticated + ) + + # Store if the cache has been updated + retvals['cache_updated'] = updated_cache + # Store when the update time was last + retvals['cache_update_time'] = updated_cache_time + # If the cache was updated and the general state change was set to + # False make sure that the change in cache state is acurately + # updated by setting the general changed state to the same as + # the cache state. + if updated_cache and not retvals['changed']: + retvals['changed'] = updated_cache + if success: module.exit_json(**retvals) else: From 6b5d30ff1140e6fde51e9eef7b62cc4a228e1850 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 28 Sep 2016 13:59:55 +0100 Subject: [PATCH 429/770] typo in module name (#5062) --- network/junos/junos_package.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/junos/junos_package.py b/network/junos/junos_package.py index 78a0f024839..e893ee9df56 100644 --- a/network/junos/junos_package.py +++ b/network/junos/junos_package.py @@ -94,7 +94,7 @@ """ import ansible.module_utils.junos -from ansible.module_utils.newtork import NetworkModule +from ansible.module_utils.network import NetworkModule try: from jnpr.junos.utils.sw import SW From 7808f42affa91ab88cc3c100d6f95a9b1204cf90 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Wed, 28 Sep 2016 16:28:16 +0100 Subject: [PATCH 430/770] Ultimate fix of the mount module for Linux (#5055) --- system/mount.py | 187 ++++++++++++++++++++++++++++++++++++------------ 1 file changed, 140 insertions(+), 47 deletions(-) diff --git a/system/mount.py b/system/mount.py index 26b8eb566f3..c5d7411ac8b 100644 --- a/system/mount.py +++ b/system/mount.py @@ -28,7 +28,6 @@ from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils.six import iteritems import os -import re DOCUMENTATION = ''' @@ -354,7 +353,7 @@ def umount(module, dest): # from @jupeter -- https://github.com/ansible/ansible-modules-core/pull/2923 # @jtyr -- https://github.com/ansible/ansible-modules-core/issues/4439 # and @abadger to relicense from GPLv3+ -def is_bind_mounted(module, dest, src=None, fstype=None): +def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None): """Return whether the dest is bind mounted :arg module: The AnsibleModule (used for helper functions) @@ -364,62 +363,147 @@ def is_bind_mounted(module, dest, src=None, fstype=None): ensure that we are detecting that the correct source is mounted there. :kwarg fstype: The filesystem type. If specified this is also used to help ensure that we are detecting the right mount. + :kwarg linux_mounts: Cached list of mounts for Linux. :returns: True if the dest is mounted with src otherwise False. """ is_mounted = False - bin_path = module.get_bin_path('mount', required=True) - cmd = '%s -l' % bin_path if get_platform() == 'Linux': - bin_path = module.get_bin_path('findmnt', required=True) - cmd = '%s -nr %s' % (bin_path, dest) + if src is None: + # That's for unmounted/absent + if dest in linux_mounts: + is_mounted = True + else: + # That's for mounted + if dest in linux_mounts and linux_mounts[dest]['src'] == src: + is_mounted = True + else: + bin_path = module.get_bin_path('mount', required=True) + cmd = '%s -l' % bin_path + rc, out, err = module.run_command(cmd) + mounts = [] - rc, out, err = module.run_command(cmd) - mounts = [] + if len(out): + mounts = to_native(out).strip().split('\n') - if len(out): - mounts = to_native(out).strip().split('\n') + for mnt in mounts: + arguments = mnt.split() - mount_pattern = re.compile('\[(.*)\]') + if ( + (arguments[0] == src or src is None) and + arguments[2] == dest and + (arguments[4] == fstype or fstype is None)): + is_mounted = True - for mnt in mounts: - arguments = mnt.split() + if is_mounted: + break - if get_platform() == 'Linux': - source = arguments[1] - result = mount_pattern.search(arguments[1]) + return is_mounted - # This is only for LVM and tmpfs mounts - if result is not None and len(result.groups()) == 1: - source = result.group(1) - if src is None: - # That's for unmounted/absent - if arguments[0] == dest: - is_mounted = True - else: - # That's for mounted - if arguments[0] == dest and source == src: - is_mounted = True - elif arguments[0] == dest and src.endswith(source): - # Check if it's tmpfs mount - sub_path = src[:len(src)-len(source)] - - if ( - is_bind_mounted(module, sub_path, 'tmpfs') and - source == src[len(sub_path):]): - is_mounted = True - elif ( - (arguments[0] == src or src is None) and - arguments[2] == dest and - (arguments[4] == fstype or fstype is None)): - is_mounted = True - - if is_mounted: - break +def get_linux_mounts(module): + """Gather mount information""" - return is_mounted + mntinfo_file = "/proc/self/mountinfo" + + try: + f = open(mntinfo_file) + except IOError: + module.fail_json(msg="Cannot open file %s" % mntinfo_file) + + lines = map(str.strip, f.readlines()) + + try: + f.close() + except IOError: + module.fail_json(msg="Cannot close file %s" % mntinfo_file) + + mntinfo = [] + + for line in lines: + fields = line.split() + + record = { + 'root': fields[3], + 'dst': fields[4], + 'opts': fields[5], + 'fields': fields[6:-4], + 'fs': fields[-3], + 'src': fields[-2], + } + + mntinfo.append(record) + + mounts = {} + + for i, mnt in enumerate(mntinfo): + src = mnt['src'] + + if mnt['fs'] == 'tmpfs' and mnt['root'] != '/': + ### Example: + # 65 19 0:35 / /tmp rw shared:25 - tmpfs tmpfs rw + # 210 65 0:35 /aaa /tmp/bbb rw shared:25 - tmpfs tmpfs rw + ### Expected result: + # src=/tmp/aaa + ### + + shared = None + + # Search for the shared field + for fld in mnt['fields']: + if fld.startswith('shared'): + shared = fld + + if shared is None: + break + + dest = None + + # Search fo the record with the same field + for j, m in enumerate(mntinfo): + if j < i: + if shared in m['fields']: + dest = m['dst'] + else: + break + + if dest is not None: + src = "%s%s" % (dest, mnt['root']) + else: + break + + elif mnt['root'] != '/' and len(mnt['fields']) > 0: + ### Example: + # 67 19 8:18 / /mnt/disk2 rw shared:26 - ext4 /dev/sdb2 rw + # 217 65 8:18 /test /tmp/ccc rw shared:26 - ext4 /dev/sdb2 rw + ### Expected result: + # src=/mnt/disk2/test + ### + + # Search for parent + for j, m in enumerate(mntinfo): + if j < i: + if m['src'] == mnt['src']: + src = "%s%s" % (m['dst'], mnt['root']) + else: + break + + elif mnt['root'] != '/' and len(mnt['fields']) == 0: + ### Example: + # 27 20 8:1 /tmp/aaa /tmp/bbb rw - ext4 /dev/sdb2 rw + ### Expected result: + # src=/tmp/aaa + ### + src = mnt['root'] + + mounts[mnt['dst']] = { + 'src': src, + 'opts': mnt['opts'], + 'fs': mnt['fs'] + } + + return mounts def main(): @@ -470,6 +554,14 @@ def main(): if get_platform() == 'FreeBSD': args['opts'] = 'rw' + linux_mounts = [] + + # Cache all mounts here in order we have consistent results if we need to + # call is_bind_mouted() multiple times + if get_platform() == 'Linux': + linux_mounts = get_linux_mounts(module) + + # Override defaults with user specified params for key in ('src', 'fstype', 'passno', 'opts', 'dump', 'fstab'): if module.params[key] is not None: args[key] = module.params[key] @@ -502,7 +594,7 @@ def main(): name, changed = unset_mount(module, args) if changed and not module.check_mode: - if ismount(name) or is_bind_mounted(module, name): + if ismount(name) or is_bind_mounted(module, linux_mounts, name): res, msg = umount(module, name) if res: @@ -516,7 +608,7 @@ def main(): e = get_exception() module.fail_json(msg="Error rmdir %s: %s" % (name, str(e))) elif state == 'unmounted': - if ismount(name) or is_bind_mounted(module, name): + if ismount(name) or is_bind_mounted(module, linux_mounts, name): if not module.check_mode: res, msg = umount(module, name) @@ -544,7 +636,8 @@ def main(): elif 'bind' in args.get('opts', []): changed = True - if is_bind_mounted(module, name, args['src'], args['fstype']): + if is_bind_mounted( + module, linux_mounts, name, args['src'], args['fstype']): changed = False if changed and not module.check_mode: From 752c80f8b7a531ae8f2b208eca981559f65ad04b Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 28 Sep 2016 16:52:48 +0100 Subject: [PATCH 431/770] deprecated _template network modules: Rename in modules-core (#5072) --- network/eos/{eos_template.py => _eos_template.py} | 1 + network/ios/{ios_template.py => _ios_template.py} | 1 + network/iosxr/{iosxr_template.py => _iosxr_template.py} | 1 + network/junos/{junos_template.py => _junos_template.py} | 1 + network/nxos/{nxos_template.py => _nxos_template.py} | 1 + network/openswitch/{ops_template.py => _ops_template.py} | 1 + 6 files changed, 6 insertions(+) rename network/eos/{eos_template.py => _eos_template.py} (99%) rename network/ios/{ios_template.py => _ios_template.py} (99%) rename network/iosxr/{iosxr_template.py => _iosxr_template.py} (98%) rename network/junos/{junos_template.py => _junos_template.py} (98%) rename network/nxos/{nxos_template.py => _nxos_template.py} (99%) rename network/openswitch/{ops_template.py => _ops_template.py} (99%) diff --git a/network/eos/eos_template.py b/network/eos/_eos_template.py similarity index 99% rename from network/eos/eos_template.py rename to network/eos/_eos_template.py index a5d0f38c93c..d546b0c98ff 100644 --- a/network/eos/eos_template.py +++ b/network/eos/_eos_template.py @@ -28,6 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. +deprecated: Deprecated in 2.2. Use eos_config instead extends_documentation_fragment: eos options: src: diff --git a/network/ios/ios_template.py b/network/ios/_ios_template.py similarity index 99% rename from network/ios/ios_template.py rename to network/ios/_ios_template.py index 90b9554e8a3..5791dfbf170 100644 --- a/network/ios/ios_template.py +++ b/network/ios/_ios_template.py @@ -28,6 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. +deprecated: Deprecated in 2.2. Use eos_config instead extends_documentation_fragment: ios options: src: diff --git a/network/iosxr/iosxr_template.py b/network/iosxr/_iosxr_template.py similarity index 98% rename from network/iosxr/iosxr_template.py rename to network/iosxr/_iosxr_template.py index 55a98fc2c57..79ddaa42600 100644 --- a/network/iosxr/iosxr_template.py +++ b/network/iosxr/_iosxr_template.py @@ -28,6 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. +deprecated: Deprecated in 2.2. Use eos_config instead extends_documentation_fragment: iosxr options: src: diff --git a/network/junos/junos_template.py b/network/junos/_junos_template.py similarity index 98% rename from network/junos/junos_template.py rename to network/junos/_junos_template.py index ec242a63d0b..30dac995787 100644 --- a/network/junos/junos_template.py +++ b/network/junos/_junos_template.py @@ -27,6 +27,7 @@ from a template file onto a remote device running Junos. The module will return the differences in configuration if the diff option is specified on the Ansible command line +deprecated: Deprecated in 2.2. Use eos_config instead extends_documentation_fragment: junos options: src: diff --git a/network/nxos/nxos_template.py b/network/nxos/_nxos_template.py similarity index 99% rename from network/nxos/nxos_template.py rename to network/nxos/_nxos_template.py index c8cc638f189..d8c8caf974b 100644 --- a/network/nxos/nxos_template.py +++ b/network/nxos/_nxos_template.py @@ -28,6 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. +deprecated: Deprecated in 2.2. Use eos_config instead extends_documentation_fragment: nxos options: src: diff --git a/network/openswitch/ops_template.py b/network/openswitch/_ops_template.py similarity index 99% rename from network/openswitch/ops_template.py rename to network/openswitch/_ops_template.py index a47abda3a5e..2fb21c263ac 100644 --- a/network/openswitch/ops_template.py +++ b/network/openswitch/_ops_template.py @@ -28,6 +28,7 @@ against a provided candidate configuration. If there are changes, the candidate configuration is merged with the current configuration and pushed into OpenSwitch +deprecated: Deprecated in 2.2. Use eos_config instead extends_documentation_fragment: openswitch options: src: From ca0e78d12a1d588fd66c30b28ebe28e8252b0d2c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 28 Sep 2016 11:12:00 -0700 Subject: [PATCH 432/770] Fix mount default options -- should be defaults, not default --- system/mount.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/mount.py b/system/mount.py index c5d7411ac8b..90bf6303fc0 100644 --- a/system/mount.py +++ b/system/mount.py @@ -544,7 +544,7 @@ def main(): else: args = dict( name=module.params['name'], - opts='default', + opts='defaults', dump='0', passno='0', fstab='/etc/fstab' From 4fcb9dcefff796ad3f6a30320f8888d31e6dfcb8 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 28 Sep 2016 14:26:25 -0400 Subject: [PATCH 433/770] fixes junos_template ignoring the action directive (#5080) This updates the junos_template to properly process the action directive when loading the configuration. --- network/junos/_junos_template.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/network/junos/_junos_template.py b/network/junos/_junos_template.py index 30dac995787..c65525d8491 100644 --- a/network/junos/_junos_template.py +++ b/network/junos/_junos_template.py @@ -101,9 +101,11 @@ src: config.j2 action: overwrite """ -from ansible.module_utils.network import NetworkModule import ansible.module_utils.junos +from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError + DEFAULT_COMMENT = 'configured by junos_template' def main(): @@ -125,24 +127,35 @@ def main(): confirm = module.params['confirm'] commit = not module.check_mode + replace = False + overwrite = False + action = module.params['action'] + if action == 'overwrite': + overwrite = True + elif action == 'replace': + replace = True src = module.params['src'] fmt = module.params['config_format'] if action == 'overwrite' and fmt == 'set': module.fail_json(msg="overwrite cannot be used when format is " - "set per junos documentation") + "set per junos-pyez documentation") results = dict(changed=False) results['_backup'] = str(module.config.get_config()).strip() - diff = module.config.load_config(src, action=action, comment=comment, - format=fmt, commit=commit, confirm=confirm) + try: + diff = module.config.load_config(src, commit=commit, replace=replace, + confirm=confirm, comment=comment, config_format=fmt) - if diff: - results['changed'] = True - results['diff'] = dict(prepared=diff) + if diff: + results['changed'] = True + results['diff'] = dict(prepared=diff) + except NetworkError: + exc = get_exception() + module.fail_json(msg=str(exc), **exc.kwargs) module.exit_json(**results) From 8bc1b322976bcfceafe4f265f13c095f3fd42a0c Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 28 Sep 2016 20:58:33 -0400 Subject: [PATCH 434/770] fixes junos_command module paring of wait_for strings (#5083) The junos_command module wasn't properly parsing strings to apply conditionals due to the return value not being converted to json before the results where handed to the runner. --- network/junos/junos_command.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index f370ace3d8a..02b2a04e7b0 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -151,12 +151,6 @@ retured: failed type: list sample: ['...', '...'] - -xml: - description: The raw XML reply from the device - returned: when format is xml - type: list - sample: [['...', '...'], ['...', '...']] """ import ansible.module_utils.junos @@ -164,6 +158,7 @@ from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError +from ansible.module_utils.netcli import FailedConditionalError from ansible.module_utils.junos import xml_to_json from ansible.module_utils.six import string_types @@ -279,24 +274,23 @@ def main(): except FailedConditionsError: exc = get_exception() module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) + except FailedConditionalError: + exc = get_exception() + module.fail_json(msg=str(exc), failed_conditional=exc.failed_conditional) except NetworkError: exc = get_exception() module.fail_json(msg=str(exc)) result = dict(changed=False, stdout=list()) - xmlout = list() for cmd in commands: try: output = runner.get_command(cmd['command'], cmd.get('output')) - xmlout.append(output) - output = xml_to_json(output) except ValueError: output = 'command not executed due to check_mode, see warnings' result['stdout'].append(output) result['warnings'] = warnings - result['xml'] = xmlout result['stdout_lines'] = list(to_lines(result['stdout'])) module.exit_json(**result) From c1a51d74ff46422cec55de41c8f2f84d368efa9e Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Thu, 29 Sep 2016 06:35:44 +0100 Subject: [PATCH 435/770] Fixing bind mount pattern in the mount module (#5084) --- system/mount.py | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/system/mount.py b/system/mount.py index 90bf6303fc0..ea76a0a9568 100644 --- a/system/mount.py +++ b/system/mount.py @@ -441,12 +441,12 @@ def get_linux_mounts(module): src = mnt['src'] if mnt['fs'] == 'tmpfs' and mnt['root'] != '/': - ### Example: + # == Example: # 65 19 0:35 / /tmp rw shared:25 - tmpfs tmpfs rw # 210 65 0:35 /aaa /tmp/bbb rw shared:25 - tmpfs tmpfs rw - ### Expected result: + # == Expected result: # src=/tmp/aaa - ### + # == shared = None @@ -456,7 +456,7 @@ def get_linux_mounts(module): shared = fld if shared is None: - break + continue dest = None @@ -471,15 +471,15 @@ def get_linux_mounts(module): if dest is not None: src = "%s%s" % (dest, mnt['root']) else: - break + continue elif mnt['root'] != '/' and len(mnt['fields']) > 0: - ### Example: + # == Example: # 67 19 8:18 / /mnt/disk2 rw shared:26 - ext4 /dev/sdb2 rw # 217 65 8:18 /test /tmp/ccc rw shared:26 - ext4 /dev/sdb2 rw - ### Expected result: + # == Expected result: # src=/mnt/disk2/test - ### + # == # Search for parent for j, m in enumerate(mntinfo): @@ -490,13 +490,27 @@ def get_linux_mounts(module): break elif mnt['root'] != '/' and len(mnt['fields']) == 0: - ### Example: + # == Example 1: # 27 20 8:1 /tmp/aaa /tmp/bbb rw - ext4 /dev/sdb2 rw - ### Expected result: + # == Example 2: + # 204 136 253:2 /rootfs / rw - ext4 /dev/sdb2 rw + # 141 140 253:2 /rootfs/tmp/aaa /tmp/bbb rw - ext4 /dev/sdb2 rw + # == Expected result: # src=/tmp/aaa - ### + # == + src = mnt['root'] + # Search for parent + for j, m in enumerate(mntinfo): + if j < i: + if ( + m['src'] == mnt['src'] and + mnt['root'].startswith(m['root'])): + src = src.replace("%s/" % m['root'], '/', 1) + else: + break + mounts[mnt['dst']] = { 'src': src, 'opts': mnt['opts'], From 537a7eb9243e394d4a60a3583c21b9e9ade68ade Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 28 Sep 2016 23:09:13 -0700 Subject: [PATCH 436/770] Set some hosts privileged so they can perform mount tests (#5087) --- shippable.yml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/shippable.yml b/shippable.yml index a8dd0fc5a9f..f5314c723fa 100644 --- a/shippable.yml +++ b/shippable.yml @@ -9,22 +9,22 @@ matrix: - env: TEST=none include: - env: TEST=integration IMAGE=ansible/ansible:centos6 - - env: TEST=integration IMAGE=ansible/ansible:centos7 - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide - - env: TEST=integration IMAGE=ansible/ansible:fedora23 - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap + - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true + - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 + - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 - env: TEST=integration PLATFORM=windows VERSION=2012-RTM - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM - - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE + - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE PRIVILEGED=true - env: TEST=integration PLATFORM=osx VERSION=10.11 From 44c5b056cb4f6a36add355236203944f61f02185 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 30 Sep 2016 00:53:09 -0400 Subject: [PATCH 437/770] 1st draft of include docs --- utilities/logic/include.py | 59 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 utilities/logic/include.py diff --git a/utilities/logic/include.py b/utilities/logic/include.py new file mode 100644 index 00000000000..c251c501dd0 --- /dev/null +++ b/utilities/logic/include.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# -*- mode: python -*- +# 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 = ''' +--- +author: + - "Ansible Core Team (@ansible)" +module: include +short_description: include a play or task list. +description: + - Loads a file with a list of plays or tasks to be executed in the current playbook. + - Files with a list of plays can only be included at the top level, lists of tasks can only be included where tasks normally run (in play). + - Before 2.0 all includes were 'static', executed at play load time. + - Since 2.0 task includes are dynamic and behave more like real tasks. This means they can be looped, skipped and use variables from any source. + Ansible tries to auto detect this, use the `static` directive (new in 2.1) to bypass autodetection. +version_added: "0.6" +options: + free-form: + description: + - This module allows you to specify the name of the file directly w/o any other options. +notes: + - This is really not a module, though it appears as such, this is a feature of the Ansible Engine, as such it cannot be overridden the same way a module can. +''' + +EXAMPLES = """ +# include a play after another play +- hosts: localhost + tasks: + - debug: msg="play1" + +- include: otherplays.yml + + +# include task list in play +- hosts: all + tasks: + - debug: msg=task1 + - include: stuff.yml + - debug: msg=task10 + +# dyanmic include task list in play +- hosts: all + tasks: + - debug: msg=task1 + - include: {{hostvar}}.yml + static: no + when: hostvar is defined +""" + +RETURN = """ +# this module does not return anything except plays or tasks to execute +""" From 75507e75691df358dcdc97d4a3ed99f201945e3d Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Fri, 30 Sep 2016 11:26:11 -0400 Subject: [PATCH 438/770] Check status of finished spot instance requests (#4990) Per #3877, the code to wait for spot instance requests to finish would hang for the full wait time if any spot request failed for any reason. This commit introduces status checks for spot requests, so if the request fails, finishes, or is cancelled the task will fail/succeed accordingly. One edge case introduced here is tha if a user terminates the instance associated with the request manually it won't fail the play, under the presumption that the user *wants* the instance terminated. --- cloud/amazon/ec2.py | 75 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 58 insertions(+), 17 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 3bc6e4d37a2..b6f30b25547 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -796,6 +796,63 @@ def boto_supports_param_in_spot_request(ec2, param): method = getattr(ec2, 'request_spot_instances') return param in method.func_code.co_varnames +def await_spot_requests(module, ec2, spot_requests, count): + """ + Wait for a group of spot requests to be fulfilled, or fail. + + module: Ansible module object + ec2: authenticated ec2 connection object + spot_requests: boto.ec2.spotinstancerequest.SpotInstanceRequest object returned by ec2.request_spot_instances + count: Total number of instances to be created by the spot requests + + Returns: + list of instance ID's created by the spot request(s) + """ + spot_wait_timeout = int(module.params.get('spot_wait_timeout')) + wait_complete = time.time() + spot_wait_timeout + + spot_req_inst_ids = dict() + while time.time() < wait_complete: + reqs = ec2.get_all_spot_instance_requests() + for sirb in spot_requests: + if sirb.id in spot_req_inst_ids: + continue + for sir in reqs: + if sir.id != sirb.id: + continue # this is not our spot instance + if sir.instance_id is not None: + spot_req_inst_ids[sirb.id] = sir.instance_id + elif sir.state == 'open': + continue # still waiting, nothing to do here + elif sir.state == 'active': + continue # Instance is created already, nothing to do here + elif sir.state == 'failed': + module.fail_json(msg="Spot instance request %s failed with status %s and fault %s:%s" % ( + sir.id, sir.status.code, sir.fault.code, sir.fault.message)) + elif sir.state == 'cancelled': + module.fail_json(msg="Spot instance request %s was cancelled before it could be fulfilled." % sir.id) + elif sir.state == 'closed': + # instance is terminating or marked for termination + # this may be intentional on the part of the operator, + # or it may have been terminated by AWS due to capacity, + # price, or group constraints in this case, we'll fail + # the module if the reason for the state is anything + # other than termination by user. Codes are documented at + # http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/spot-bid-status.html + if sir.status.code == 'instance-terminated-by-user': + # do nothing, since the user likely did this on purpose + pass + else: + spot_msg = "Spot instance request %s was closed by AWS with the status %s and fault %s:%s" + module.fail_json(msg=spot_msg % (sir.id, sir.status.code, sir.fault.code, sir.fault.message)) + + if len(spot_req_inst_ids) < count: + time.sleep(5) + else: + return spot_req_inst_ids.values() + module.fail_json(msg = "wait for spot requests timeout on %s" % time.asctime()) + + def enforce_count(module, ec2, vpc): exact_count = module.params.get('exact_count') @@ -1106,23 +1163,7 @@ def create_instances(module, ec2, vpc, override_count=None): # Now we have to do the intermediate waiting if wait: - spot_req_inst_ids = dict() - spot_wait_timeout = time.time() + spot_wait_timeout - while spot_wait_timeout > time.time(): - reqs = ec2.get_all_spot_instance_requests() - for sirb in res: - if sirb.id in spot_req_inst_ids: - continue - for sir in reqs: - if sir.id == sirb.id and sir.instance_id is not None: - spot_req_inst_ids[sirb.id] = sir.instance_id - if len(spot_req_inst_ids) < count: - time.sleep(5) - else: - break - if spot_wait_timeout <= time.time(): - module.fail_json(msg = "wait for spot requests timeout on %s" % time.asctime()) - instids = spot_req_inst_ids.values() + instids = await_spot_requests(module, ec2, res, count) except boto.exception.BotoServerError as e: module.fail_json(msg = "Instance creation failed => %s: %s" % (e.error_code, e.error_message)) From ac42b85225388cedefc002fec93b4be81249b52c Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 30 Sep 2016 09:52:25 -0700 Subject: [PATCH 439/770] remove broken ansible_user_uid fact from Windows setup --- windows/setup.ps1 | 1 - 1 file changed, 1 deletion(-) diff --git a/windows/setup.ps1 b/windows/setup.ps1 index ff7ee2e5942..fcbe9ca1303 100644 --- a/windows/setup.ps1 +++ b/windows/setup.ps1 @@ -127,7 +127,6 @@ Set-Attr $result.ansible_facts "ansible_owner_contact" ([string] $win32_cs.Prima Set-Attr $result.ansible_facts "ansible_user_dir" $env:userprofile Set-Attr $result.ansible_facts "ansible_user_gecos" "" # Win32_UserAccount.FullName is probably the right thing here, but it can be expensive to get on large domains Set-Attr $result.ansible_facts "ansible_user_id" $env:username -Set-Attr $result.ansible_facts "ansible_user_uid" ([int] $user.User.Value.Substring(42)) Set-Attr $result.ansible_facts "ansible_user_sid" $user.User.Value $date = New-Object psobject From 137d1373d330c52c43c25c83986b7b0210013d51 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:16:10 -0400 Subject: [PATCH 440/770] Fixing nxos_vtp_version (#5038) --- network/nxos/nxos_vtp_version.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_vtp_version.py b/network/nxos/nxos_vtp_version.py index 0d24e9c37c0..90fad190160 100644 --- a/network/nxos/nxos_vtp_version.py +++ b/network/nxos/nxos_vtp_version.py @@ -264,10 +264,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -296,7 +293,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -400,6 +397,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_vtp_config(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 2704b208cc64ef8217bed512165f94c3314bba93 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:16:32 -0400 Subject: [PATCH 441/770] Fixing nxos_vtp_password (#5037) --- network/nxos/nxos_vtp_password.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index d4528b70179..1f78c8e267e 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -286,10 +286,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -318,7 +315,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -462,6 +459,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_vtp_config(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From ba3485999a3d4f7d98fa6e171d5b7f1365b2752a Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:16:47 -0400 Subject: [PATCH 442/770] Fixing nxos_vrrp (#5035) --- network/nxos/nxos_vrrp.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_vrrp.py b/network/nxos/nxos_vrrp.py index 073ee8d2e58..c389a90dec4 100644 --- a/network/nxos/nxos_vrrp.py +++ b/network/nxos/nxos_vrrp.py @@ -277,6 +277,15 @@ def execute_config_command(commands, module): 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_vrrp(command, response, module): @@ -323,7 +332,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -619,6 +628,8 @@ def main(): execute_config_command(cmds, module) changed = True end_state = get_existing_vrrp(interface, group, module, name) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 690bbcb1488f3aac9e434d7c0772adcae884a92f Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:17:07 -0400 Subject: [PATCH 443/770] Fixing nxos_vtp_domain (#5036) --- network/nxos/nxos_vtp_domain.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 7ea1b8c8cde..806b01f79c7 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -269,10 +269,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -301,7 +298,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -405,6 +402,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_vtp_config(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 39cd41f636bfe600efea237eccf395afcac45e4b Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:17:21 -0400 Subject: [PATCH 444/770] Fixing nxos_vrf_interface (#5034) --- network/nxos/nxos_vrf_interface.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index c0742468397..fe80c0f89ea 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -251,6 +251,15 @@ def execute_config_command(commands, module): 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_vrf_interface(command, response, module): @@ -290,7 +299,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -466,6 +475,8 @@ def main(): changed = True changed_vrf = get_interface_info(interface, module) end_state = dict(interface=interface, vrf=changed_vrf) + if 'configure' in commands: + commands.pop(0) results = {} results['proposed'] = proposed @@ -481,4 +492,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From db818dddcb88f20afa7c8578d439e4fea6b2171f Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:17:34 -0400 Subject: [PATCH 445/770] Fixing nxos_vpc (#5031) --- network/nxos/nxos_vpc.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py index 62635e84610..3fae9cee00d 100644 --- a/network/nxos/nxos_vpc.py +++ b/network/nxos/nxos_vpc.py @@ -305,6 +305,15 @@ def execute_config_command(commands, module): 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): @@ -349,7 +358,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -629,6 +638,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_vpc(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 770fd68b42f6a8590b386aca22c7a5c53ae6fdc1 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:17:54 -0400 Subject: [PATCH 446/770] Fixing nxos_vrf (#5033) --- network/nxos/nxos_vrf.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index bcce2872eb0..40e11a70b9f 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -274,6 +274,15 @@ def execute_config_command(commands, module): 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_vrf(module, command, response): @@ -316,7 +325,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -503,6 +512,8 @@ def main(): execute_config_command(commands, module) changed = True end_state = get_vrf(vrf, module) + if 'configure' in commands: + commands.pop(0) results = {} results['proposed'] = proposed @@ -515,4 +526,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From 3b4bbb2497fb8a60f3d9bf2a013660330becda4e Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:18:10 -0400 Subject: [PATCH 447/770] Fixing nxos_vpc_interface (#5032) --- network/nxos/nxos_vpc_interface.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 4480c58727d..a621c1b1632 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -257,6 +257,15 @@ def execute_config_command(commands, module): 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') + response = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) return response @@ -300,7 +309,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -563,6 +572,8 @@ def main(): if 'error' in output.lower(): module.fail_json(msg=output.replace('\n', '')) end_state = get_portchannel_vpc_config(module, portchannel) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 7e2a87b43f9a8ac1ea1c7f7ade0965d76ccb4c4b Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:18:23 -0400 Subject: [PATCH 448/770] Fixing nxos_udld (#5029) --- network/nxos/nxos_udld.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_udld.py b/network/nxos/nxos_udld.py index cd377870ee3..7c903fcfc6f 100644 --- a/network/nxos/nxos_udld.py +++ b/network/nxos/nxos_udld.py @@ -293,10 +293,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -325,7 +322,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -487,6 +484,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_udld_global(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From c7e711bd35a185870b9f98f85ef3458076bea66e Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:18:40 -0400 Subject: [PATCH 449/770] Fixing nxos_switchport (#5028) --- network/nxos/nxos_switchport.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_switchport.py b/network/nxos/nxos_switchport.py index 49fce57aee8..1b6cbf70ae0 100644 --- a/network/nxos/nxos_switchport.py +++ b/network/nxos/nxos_switchport.py @@ -607,10 +607,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -639,7 +636,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -792,6 +789,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_switchport(interface, module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 0d985071e180a2bdafe043042cda8e3bbe003313 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:18:58 -0400 Subject: [PATCH 450/770] Fixing nxos_udld_interface (#5030) --- network/nxos/nxos_udld_interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_udld_interface.py b/network/nxos/nxos_udld_interface.py index 593aaa3fcd7..44bb3c5ebe5 100644 --- a/network/nxos/nxos_udld_interface.py +++ b/network/nxos/nxos_udld_interface.py @@ -292,10 +292,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -324,7 +321,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -500,6 +497,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_udld_interface(module, interface) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From d75121b3c11732f9822017dee29714cf2a2deb68 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:19:12 -0400 Subject: [PATCH 451/770] Fixing nxos_snmp_user (#5027) --- network/nxos/nxos_snmp_user.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_snmp_user.py b/network/nxos/nxos_snmp_user.py index 9e1c051739d..db10d51f4d5 100644 --- a/network/nxos/nxos_snmp_user.py +++ b/network/nxos/nxos_snmp_user.py @@ -297,10 +297,7 @@ def get_cli_body_ssh(command, response, module, text=False): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -329,7 +326,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -541,6 +538,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_snmp_user(user, module) + if 'configure' in cmds: + cmds.pop(0) if store: existing['group'] = store From f836de5e4bca2d6559d63cedc30e9f7e41167581 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:19:27 -0400 Subject: [PATCH 452/770] Fixing nxos_snmp_traps (#5026) --- network/nxos/nxos_snmp_traps.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py index c48eafa0eba..63f1a3e3e78 100644 --- a/network/nxos/nxos_snmp_traps.py +++ b/network/nxos/nxos_snmp_traps.py @@ -286,10 +286,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -318,7 +315,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -481,6 +478,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_snmp_traps(group, module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 21fe7a1e134e63f5c8d7bd160d42ab26f243c8bb Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:19:40 -0400 Subject: [PATCH 453/770] Fixing nxos_snmp_location (#5025) --- network/nxos/nxos_snmp_location.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py index b829b83cdad..29d5cf576ed 100644 --- a/network/nxos/nxos_snmp_location.py +++ b/network/nxos/nxos_snmp_location.py @@ -275,10 +275,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -307,7 +304,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -403,6 +400,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_snmp_location(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 26ebb1acb936c24875d8184dedc1d3d5a832a89d Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:19:54 -0400 Subject: [PATCH 454/770] Fixing nxos_snmp_host (#5024) --- network/nxos/nxos_snmp_host.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_snmp_host.py b/network/nxos/nxos_snmp_host.py index e767f1ce43c..92f050fe5a7 100644 --- a/network/nxos/nxos_snmp_host.py +++ b/network/nxos/nxos_snmp_host.py @@ -316,10 +316,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -348,7 +345,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -621,6 +618,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_snmp_host(snmp_host, module) + if 'configure' in cmds: + cmds.pop(0) if store: existing['vrf_filter'] = store From 099aa0d82d12515cfd9c2c8cb96dcedec785d56e Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:20:08 -0400 Subject: [PATCH 455/770] Fixing nxos_snmp_contact (#5023) --- network/nxos/nxos_snmp_contact.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index e6982a8a4d4..aaaeab4fb76 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -268,10 +268,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -300,7 +297,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -383,6 +380,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_snmp_contact(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 11f628a1242634eaa5061bfac81f0cecc938bf43 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:20:23 -0400 Subject: [PATCH 456/770] Fixing nxos_snmp_community (#5022) --- network/nxos/nxos_snmp_community.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_snmp_community.py b/network/nxos/nxos_snmp_community.py index 137dd7715b0..5abc428fb8e 100644 --- a/network/nxos/nxos_snmp_community.py +++ b/network/nxos/nxos_snmp_community.py @@ -283,10 +283,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -315,7 +312,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -485,6 +482,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_snmp_community(module, community) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 33b8a897022fbc966a8e82d0a31a144cfb017806 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:20:37 -0400 Subject: [PATCH 457/770] Fixing nxos_smu (#5021) --- network/nxos/nxos_smu.py | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index 310fe7fcbc1..f89aeb37b96 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -256,7 +256,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -286,12 +286,21 @@ def remote_file_exists(module, dst, file_system='bootflash:'): def execute_config_command(commands, module): try: - response = module.configure(commands) + output = module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) - return response + except AttributeError: + try: + commands.insert(0, 'configure') + module.cli.add_commands(commands, output='config') + output = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return output def apply_patch(module, commands): @@ -353,11 +362,14 @@ def main(): if not module.check_mode and commands: try: apply_patch(module, commands) - changed=True + changed = True except ShellError: e = get_exception() module.fail_json(msg=str(e)) + if 'configure' in commands: + commands.pop(0) + module.exit_json(changed=changed, pkg=pkg, file_system=file_system, From c4941285cb49fe087147e9d091dc76b39f700825 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:20:51 -0400 Subject: [PATCH 458/770] Fixing nxos_portchannel (#5019) --- network/nxos/nxos_portchannel.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index 68af16c8f1d..48985a13178 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -343,10 +343,7 @@ def execute_config_command(commands, module): def get_cli_body_ssh(command, response, module): try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -375,7 +372,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() From 9f0aec160b1c5d552cf7b88bf26b64467778d8d8 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:21:06 -0400 Subject: [PATCH 459/770] Fixing nxos_pim_interface (#5018) --- network/nxos/nxos_pim_interface.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_pim_interface.py b/network/nxos/nxos_pim_interface.py index 68f0cbb775c..9556b03ab65 100644 --- a/network/nxos/nxos_pim_interface.py +++ b/network/nxos/nxos_pim_interface.py @@ -373,10 +373,7 @@ def get_cli_body_ssh(command, response, module, text=False): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -405,7 +402,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -915,6 +912,8 @@ def main(): 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 From 88a21027873170ddb8a3ec425aabb28223f07399 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:21:20 -0400 Subject: [PATCH 460/770] Fixing nxos_ntp_options (#5017) --- network/nxos/nxos_ntp_options.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py index 69dd954cda2..9c03013241f 100644 --- a/network/nxos/nxos_ntp_options.py +++ b/network/nxos/nxos_ntp_options.py @@ -289,10 +289,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -321,7 +318,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -501,6 +498,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_ntp_options(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From a6c021219e33d1f69eb7ac02489cb6fc7382554c Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:21:35 -0400 Subject: [PATCH 461/770] Fixing nxos_ntp_auth (#5016) --- network/nxos/nxos_ntp_auth.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py index 28da3a74468..1161709970f 100644 --- a/network/nxos/nxos_ntp_auth.py +++ b/network/nxos/nxos_ntp_auth.py @@ -308,10 +308,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -340,7 +337,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -555,6 +552,8 @@ def main(): delta = dict(set(end_state.iteritems()).difference(existing.iteritems())) if delta or (len(existing) != len(end_state)): changed = True + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 82828fc1a0c036b21fc52406e3f7a03419550ccf Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:21:50 -0400 Subject: [PATCH 462/770] Fixing nxos_ntp (#5015) --- network/nxos/nxos_ntp.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_ntp.py b/network/nxos/nxos_ntp.py index 81de38bef84..18cbe2b1a7c 100644 --- a/network/nxos/nxos_ntp.py +++ b/network/nxos/nxos_ntp.py @@ -310,10 +310,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -342,7 +339,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -615,6 +612,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_ntp_existing(address, peer_type, module)[0] + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 5cc4a7766a6a9314ec7847d434573d49cf8563c0 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:22:04 -0400 Subject: [PATCH 463/770] Fixing nxos_mtu (#5014) --- network/nxos/nxos_mtu.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/network/nxos/nxos_mtu.py b/network/nxos/nxos_mtu.py index e8cd0f205ca..26fccca6153 100644 --- a/network/nxos/nxos_mtu.py +++ b/network/nxos/nxos_mtu.py @@ -304,10 +304,7 @@ def get_cli_body_ssh(command, response, module): body = response else: try: - if isinstance(response[0], str): - body = [json.loads(response[0])] - else: - body = response + body = [json.loads(response[0])] except ValueError: module.fail_json(msg='Command does not support JSON output', command=command) @@ -336,7 +333,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -580,6 +577,8 @@ def main(): end_state = get_mtu(interface, module) else: end_state = get_system_mtu(module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From c977859ae55a3233883f971325ca5090eaf0dad3 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:22:24 -0400 Subject: [PATCH 464/770] Fxing nxos_ip_interface (#5013) --- network/nxos/nxos_ip_interface.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index fa9abfd782f..eef79543d56 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -258,6 +258,15 @@ def execute_config_command(commands, module): 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): @@ -303,7 +312,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -680,6 +689,8 @@ def main(): changed = True end_state, address_list = get_ip_interface(interface, version, module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 41d862737614a8a455cb4d82a8121eed8354b585 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:22:48 -0400 Subject: [PATCH 465/770] Fixing nxos_interface (#5012) --- network/nxos/nxos_interface.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/network/nxos/nxos_interface.py b/network/nxos/nxos_interface.py index b8ca51fe57b..927e93d3a88 100644 --- a/network/nxos/nxos_interface.py +++ b/network/nxos/nxos_interface.py @@ -291,9 +291,6 @@ def load_config(module, candidate): return result # END OF COMMON CODE -BOOLEANS_TRUE = ['yes', 'on', '1', 'true', 'True', 1, True] -BOOLEANS_FALSE = ['no', 'off', '0', 'false', 'False', 0, False] -ACCEPTED = BOOLEANS_TRUE + BOOLEANS_FALSE def is_default_interface(interface, module): """Checks to see if interface exists and if it is a default config @@ -697,6 +694,15 @@ def execute_config_command(commands, module): 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): @@ -741,7 +747,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -804,8 +810,7 @@ def main(): interface_type=dict(required=False, choices=['loopback', 'portchannel', 'svi', 'nve']), ip_forward=dict(required=False, choices=['enable', 'disable']), - fabric_forwarding_anycast_gateway=dict(required=False, type='bool', - choices=ACCEPTED), + fabric_forwarding_anycast_gateway=dict(required=False, type='bool'), state=dict(choices=['absent', 'present', 'default'], default='present', required=False), include_defaults=dict(default=True), @@ -926,6 +931,7 @@ def main(): normalized_interface) else: end_state = get_interfaces_dict(module)[interface_type] + cmds = [cmd for cmd in cmds if cmd != 'configure'] results = {} results['proposed'] = proposed @@ -938,4 +944,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From c88404c11977df048b3468b6993e8d8c1cebff3e Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:23:17 -0400 Subject: [PATCH 466/770] Fixing nxos_acl_interface (#5007) --- network/nxos/nxos_acl_interface.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 07060df43b1..83ab0d41568 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -449,6 +449,15 @@ def execute_config_command(commands, module): 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 main(): @@ -510,6 +519,8 @@ def main(): interfaces_acls, this_dir_acl_intf = other_existing_acl( end_state_acls, interface, direction) end_state = this_dir_acl_intf + if 'configure' in cmds: + cmds.pop(0) else: cmds = [] From 8da99d4e703a027a2a4be0abef491ced3c57fe13 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:23:34 -0400 Subject: [PATCH 467/770] Fixing nxos_acl (#5006) * Fixing nxos_acl * Fixing nxos_acl --- network/nxos/nxos_acl.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index d2e13a3d831..b0e3223eb64 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -439,7 +439,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -624,6 +624,15 @@ def execute_config_command(commands, module): 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 main(): @@ -787,6 +796,8 @@ def main(): execute_config_command(cmds, module) changed = True new_existing_core, end_state, seqs = get_acl(module, name, seq) + if 'configure' in cmds: + cmds.pop(0) results['proposed'] = proposed results['existing'] = existing_core From 813a14f046920a0cd62ee535600bafb1251a63db Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:24:12 -0400 Subject: [PATCH 468/770] Fixing nxos_aaa_server (#5002) * Fixing command output formatting * Fixing cmds --- network/nxos/nxos_aaa_server.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_aaa_server.py b/network/nxos/nxos_aaa_server.py index 43cc3425f26..33988657e6f 100644 --- a/network/nxos/nxos_aaa_server.py +++ b/network/nxos/nxos_aaa_server.py @@ -349,7 +349,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -543,7 +543,7 @@ def main(): command = default_aaa_server(existing, proposed, server_type) if command: commands.append(command) - + cmds = flatten_list(commands) if cmds: if module.check_mode: @@ -552,6 +552,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_aaa_server_info(server_type, module) + if 'configure' in cmds: + cmds.pop(0) results = {} results['proposed'] = proposed From 21078d74623bc07eec6a95a307a1b247b864daf2 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:24:44 -0400 Subject: [PATCH 469/770] Fixing nxos_vlan (#5005) * Fixing nxos_vlan * Fixing docstring * Fixing docstring --- network/nxos/nxos_vlan.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index c7071c10918..cb06bd84fbc 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -456,6 +456,15 @@ def execute_config_command(commands, module): 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): @@ -500,7 +509,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -564,7 +573,6 @@ def main(): proposed_vlans_list = numerical_sort(vlan_range_to_list( vlan_id or vlan_range)) existing_vlans_list = numerical_sort(get_list_of_vlans(module)) - commands = [] existing = None @@ -599,7 +607,7 @@ def main(): end_state_vlans_list = existing_vlans_list if commands: - if existing: + if existing.get('mapped_vni'): if (existing.get('mapped_vni') != proposed.get('mapped_vni') and existing.get('mapped_vni') != '0' and proposed.get('mapped_vni') != 'default'): commands.insert(1, 'no vn-segment') @@ -610,6 +618,8 @@ def main(): execute_config_command(commands, module) changed = True end_state_vlans_list = numerical_sort(get_list_of_vlans(module)) + if 'configure' in commands: + commands.pop(0) if vlan_id: end_state = get_vlan(vlan_id, module) From 201d041c4b308f5bfb346458737161449b29ca43 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Fri, 30 Sep 2016 16:25:02 -0400 Subject: [PATCH 470/770] Add ethernet-link-oam mapping to nxos_feature (#4956) * Add ethernet-link-oam mapping * Adding port-secutiry mapping * Fixing command output format --- network/nxos/nxos_feature.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index 8b9b831deb1..15d49cc73c0 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -298,7 +298,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -399,7 +399,10 @@ def validate_feature(module, mode='show'): 'sla sender': 'sla_sender', 'ssh': 'sshServer', 'tacacs+': 'tacacs', - 'telnet': 'telnetServer'}, + 'telnet': 'telnetServer', + 'ethernet-link-oam': 'elo', + 'port-security': 'eth_port_sec' + }, 'config': { 'nve': 'nv overlay', @@ -413,6 +416,8 @@ def validate_feature(module, mode='show'): 'sshServer': 'ssh', 'tacacs': 'tacacs+', 'telnetServer': 'telnet', + 'elo': 'ethernet-link-oam', + 'eth_port_sec': 'port-security' } } From 2db006450dbd6852703a8cda1333e53234ebe75a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 30 Sep 2016 16:25:31 -0400 Subject: [PATCH 471/770] roll up of unicode fixes in junos modules (#5113) * fixes junos_template (fixes #3962) * fixes junos_config --- network/junos/_junos_template.py | 2 +- network/junos/junos_config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/network/junos/_junos_template.py b/network/junos/_junos_template.py index c65525d8491..c50deeef2c5 100644 --- a/network/junos/_junos_template.py +++ b/network/junos/_junos_template.py @@ -144,7 +144,7 @@ def main(): "set per junos-pyez documentation") results = dict(changed=False) - results['_backup'] = str(module.config.get_config()).strip() + results['_backup'] = unicode(module.config.get_config()).strip() try: diff = module.config.load_config(src, commit=commit, replace=replace, diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 30a2286097b..afa711bd3bf 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -210,7 +210,7 @@ def config_to_commands(config): return commands def diff_commands(commands, config): - config = [str(c).replace("'", '') for c in config] + config = [unicode(c).replace("'", '') for c in config] updates = list() visited = set() From 18f710fe32b54732862c930be6f297ab2da07e6a Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 30 Sep 2016 16:16:24 -0700 Subject: [PATCH 472/770] add JSON junk filter to async_wrapper (#5107) --- utilities/logic/async_wrapper.py | 64 +++++++++++++++++++++++++++++++- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 822f9dc9d6c..05054504b46 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -70,6 +70,50 @@ def daemonize_self(): os.dup2(dev_null.fileno(), sys.stdout.fileno()) os.dup2(dev_null.fileno(), sys.stderr.fileno()) +# NB: this function copied from module_utils/json_utils.py. Ensure any changes are propagated there. +# FUTURE: AnsibleModule-ify this module so it's Ansiballz-compatible and can use the module_utils copy of this function. +def _filter_non_json_lines(data): + ''' + Used to filter unrelated output around module JSON output, like messages from + tcagetattr, or where dropbear spews MOTD on every single command (which is nuts). + + Filters leading lines before first line-starting occurrence of '{' or '[', and filter all + trailing lines after matching close character (working from the bottom of output). + ''' + warnings = [] + + # Filter initial junk + lines = data.splitlines() + + for start, line in enumerate(lines): + line = line.strip() + if line.startswith(u'{'): + endchar = u'}' + break + elif line.startswith(u'['): + endchar = u']' + break + else: + raise ValueError('No start of json char found') + + # Filter trailing junk + lines = lines[start:] + + for reverse_end_offset, line in enumerate(reversed(lines)): + if line.strip().endswith(endchar): + break + else: + raise ValueError('No end of json char found') + + if reverse_end_offset > 0: + # Trailing junk is uncommon and can point to things the user might + # want to change. So print a warning if we find any + trailing_junk = lines[len(lines) - reverse_end_offset:] + warnings.append('Module invocation had junk after the JSON data: %s' % '\n'.join(trailing_junk)) + + lines = lines[:(len(lines) - reverse_end_offset)] + + return ('\n'.join(lines), warnings) def _run_module(wrapped_cmd, jid, job_path): @@ -82,6 +126,8 @@ def _run_module(wrapped_cmd, jid, job_path): result = {} outdata = '' + filtered_outdata = '' + stderr = '' try: cmd = shlex.split(wrapped_cmd) script = subprocess.Popen(cmd, shell=False, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) @@ -89,7 +135,19 @@ def _run_module(wrapped_cmd, jid, job_path): if PY3: outdata = outdata.decode('utf-8', 'surrogateescape') stderr = stderr.decode('utf-8', 'surrogateescape') - result = json.loads(outdata) + + (filtered_outdata, json_warnings) = _filter_non_json_lines(outdata) + + result = json.loads(filtered_outdata) + + if json_warnings: + # merge JSON junk warnings with any existing module warnings + module_warnings = result.get('warnings', []) + if type(module_warnings) is not list: + module_warnings = [module_warnings] + module_warnings.extend(json_warnings) + result['warnings'] = module_warnings + if stderr: result['stderr'] = stderr jobfile.write(json.dumps(result)) @@ -100,11 +158,13 @@ def _run_module(wrapped_cmd, jid, job_path): "failed": 1, "cmd" : wrapped_cmd, "msg": str(e), + "outdata": outdata, # temporary notice only + "stderr": stderr } result['ansible_job_id'] = jid jobfile.write(json.dumps(result)) - except: + except (ValueError, Exception): result = { "failed" : 1, "cmd" : wrapped_cmd, From dad21ce886169af26115ee7cb1fa21042d3a216d Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Sat, 1 Oct 2016 11:33:54 -0700 Subject: [PATCH 473/770] Remove the dellosX_template module (#5110) --- network/dellos10/dellos10_template.py | 180 ------------------------- network/dellos6/dellos6_template.py | 174 ------------------------ network/dellos9/dellos9_template.py | 186 -------------------------- 3 files changed, 540 deletions(-) delete mode 100644 network/dellos10/dellos10_template.py delete mode 100644 network/dellos6/dellos6_template.py delete mode 100755 network/dellos9/dellos9_template.py diff --git a/network/dellos10/dellos10_template.py b/network/dellos10/dellos10_template.py deleted file mode 100644 index b9bec6a3311..00000000000 --- a/network/dellos10/dellos10_template.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/python -# -# (c) 2015 Peter Sprygada, -# -# Copyright (c) 2016 Dell Inc. -# -# 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: dellos10_template -version_added: "2.2" -author: "Senthil Kumar Ganesan (@skg-net)" -short_description: Manage Dell OS10 device configurations over SSH. -description: - - Manages Dell OS10 network device configurations over SSH. This module - allows implementors to work with the device running-config. It - provides a way to push a set of commands onto a network device - by evaluating the current running-config and only pushing configuration - commands that are not already configured. The config source can - be a set of commands or a template. -extends_documentation_fragment: dellos10 -options: - src: - description: - - The path to the config source. The source can be either a - file with config or a template that will be merged during - runtime. By default the task will first search for the source - file in role or playbook root folder in templates unless a full - path to the file is given. - required: true - force: - description: - - The force argument instructs the module not to consider the - current device running-config. When set to true, this will - cause the module to push the contents of I(src) into the device - without first checking if already configured. This argument is - mutually exclusive with I(config). - required: false - default: false - choices: [ "true", "false" ] - backup: - description: - - When this argument is configured true, the module will backup - the running-config from the node prior to making any changes. - The backup file will be written to backup_{{ hostname }} in - the root of the playbook directory. This argument is - mutually exclusive with I(config). - - required: false - default: false - choices: [ "true", "false" ] - config: - description: - - The module, by default, will connect to the remote device and - retrieve the current running-config to use as a base for comparing - against the contents of source. There are times when it is not - desirable to have the task get the current running-config for - every task. The I(config) argument allows the implementer to - pass in the configuration to use as the base config for - comparison. This argument is mutually exclusive with - I(force) and I(backup). - - required: false - default: null -""" - -EXAMPLES = """ -- name: push a configuration onto the device - dellos10_template: - host: hostname - username: foo - src: config.j2 - -- name: forceable push a configuration onto the device - dellos10_template: - host: hostname - username: foo - src: config.j2 - force: yes - -- name: provide the base configuration for comparison - dellos10_template: - host: hostname - username: foo - src: candidate_config.txt - config: current_config.txt -""" - -RETURN = """ -updates: - description: The set of commands that will be pushed to the remote device - returned: always - type: list - sample: ['...', '...'] - -_backup: - description: The current running config of the remote device. - returned: when running config is present in the remote device. - type: list - sample: ['...', '...'] - -responses: - description: The set of responses from issuing the commands on the device - returned: when not check_mode - type: list - sample: ['...', '...'] -""" -from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dellos10 - - -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config.get_config() - return config - - -def main(): - """ main entry point for module execution - """ - - argument_spec = dict( - src=dict(), - force=dict(default=False, type='bool'), - backup=dict(default=False, type='bool'), - config=dict(), - ) - - mutually_exclusive = [('config', 'backup'), ('config', 'force')] - - module = NetworkModule(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) - - result = dict(changed=False) - - candidate = NetworkConfig(contents=module.params['src'], indent=1) - - - contents = get_config(module) - - if contents: - config = NetworkConfig(contents=contents[0], indent=1) - result['_backup'] = contents[0] - - commands = list() - if not module.params['force']: - commands = dumps(candidate.difference(config), 'commands') - else: - commands = str(candidate) - - if commands: - commands = commands.split('\n') - if not module.check_mode: - response = module.config(commands) - result['responses'] = response - result['changed'] = True - - result['updates'] = commands - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/network/dellos6/dellos6_template.py b/network/dellos6/dellos6_template.py deleted file mode 100644 index a1209b46d98..00000000000 --- a/network/dellos6/dellos6_template.py +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/python -# -# (c) 2015 Peter Sprygada, -# -# Copyright (c) 2016 Dell Inc. -# -# 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: dellos6_template -version_added: "2.2" -author: "Dhivya P (@dhivyap)" -short_description: Manage Dell OS6 device configurations over SSH. -description: - - Manages Dell OS6 network device configurations over SSH. This module - allows implementors to work with the device running-config. It - provides a way to push a set of commands onto a network device - by evaluating the current running-config and only pushing configuration - commands that are not already configured. The config source can - be a set of commands or a template. -extends_documentation_fragment: dellos6 -options: - src: - description: - - The path to the config source. The source can be either a - file with config or a template that will be merged during - runtime. By default the task will first search for the source - file in role or playbook root folder in templates unless a full - path to the file is given. - required: true - force: - description: - - The force argument instructs the module not to consider the - current device running-config. When set to true, this will - cause the module to push the contents of I(src) into the device - without first checking if already configured. This argument is - mutually exclusive with I(config). - required: false - default: false - choices: [ "true", "false" ] - backup: - description: - - When this argument is configured true, the module will backup - the running-config from the node prior to making any changes. - The backup file will be written to backup_{{ hostname }} in - the root of the playbook directory. This argument is - mutually exclusive with I(config). - required: false - default: false - choices: [ "true", "false" ] - config: - description: - - The module, by default, will connect to the remote device and - retrieve the current running-config to use as a base for comparing - against the contents of source. There are times when it is not - desirable to have the task get the current running-config for - every task. The I(config) argument allows the implementer to - pass in the configuration to use as the base config for - comparison. This argument is mutually exclusive with - I(force) and I(backup). - required: false - default: null -""" - -EXAMPLES = """ -- name: push a configuration onto the device - dellos6_template: - host: hostname - username: foo - src: config.j2 - -- name: forceable push a configuration onto the device - dellos6_template: - host: hostname - username: foo - src: config.j2 - force: yes - -- name: provide the base configuration for comparison - dellos6_template: - host: hostname - username: foo - src: candidate_config.txt - config: current_config.txt -""" - -RETURN = """ -updates: - description: The set of commands that will be pushed to the remote device - returned: always - type: list - sample: ['...', '...'] - -_backup: - description: The current running config of the remote device. - returned: when running config is present in the remote device. - type: list - sample: ['...', '...'] - -responses: - description: The set of responses from issuing the commands on the device - returned: when not check_mode - type: list - sample: ['...', '...'] -""" -from ansible.module_utils.netcfg import dumps -from ansible.module_utils.network import NetworkModule -from ansible.module_utils.dellos6 import Dellos6NetworkConfig - - -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config.get_config() - return config - - -def main(): - """ main entry point for module execution - """ - - argument_spec = dict( - src=dict(), - force=dict(default=False, type='bool'), - backup=dict(default=False, type='bool'), - config=dict(), - ) - mutually_exclusive = [('config', 'backup'), ('config', 'force')] - - module = NetworkModule(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) - - result = dict(changed=False) - candidate = Dellos6NetworkConfig(contents=module.params['src'], indent=0) - - contents = get_config(module) - if contents: - config = Dellos6NetworkConfig(contents=contents[0], indent=0) - result['_backup'] = contents[0] - commands = list() - - if not module.params['force']: - commands = dumps(candidate.difference(config), 'commands') - else: - commands = str(candidate) - - if commands: - commands = commands.split('\n') - if not module.check_mode: - response = module.config(commands) - result['responses'] = response - result['changed'] = True - - result['updates'] = commands - module.exit_json(**result) - - -if __name__ == '__main__': - main() diff --git a/network/dellos9/dellos9_template.py b/network/dellos9/dellos9_template.py deleted file mode 100755 index 60cd00ab1a6..00000000000 --- a/network/dellos9/dellos9_template.py +++ /dev/null @@ -1,186 +0,0 @@ -#!/usr/bin/python -# -# (c) 2015 Peter Sprygada, -# -# Copyright (c) 2016 Dell Inc. -# -# 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: dellos9_template -version_added: "2.2" -author: "Dhivya P (@dhivyap)" -short_description: Manage Dell OS9 device configurations over SSH. -description: - - Manages Dell OS9 network device configurations over SSH. This module - allows implementors to work with the device running-config. It - provides a way to push a set of commands onto a network device - by evaluating the current running-config and only pushing configuration - commands that are not already configured. The config source can - be a set of commands or a template. -extends_documentation_fragment: dellos9 -options: - src: - description: - - The path to the config source. The source can be either a - file with config or a template that will be merged during - runtime. By default the task will first search for the source - file in role or playbook root folder in templates unless a full - path to the file is given. - required: true - force: - description: - - The force argument instructs the module not to consider the - current device running-config. When set to true, this will - cause the module to push the contents of I(src) into the device - without first checking if already configured. This argument is - mutually exclusive with I(config). - required: false - default: false - choices: [ "true", "false" ] - backup: - description: - - When this argument is configured true, the module will backup - the running-config from the node prior to making any changes. - The backup file will be written to backup_{{ hostname }} in - the root of the playbook directory. This argument is - mutually exclusive with I(config). - - required: false - default: false - choices: [ "true", "false" ] - config: - description: - - The module, by default, will connect to the remote device and - retrieve the current running-config to use as a base for comparing - against the contents of source. There are times when it is not - desirable to have the task get the current running-config for - every task. The I(config) argument allows the implementer to - pass in the configuration to use as the base config for - comparison. This argument is mutually exclusive with - I(force) and I(backup). - - required: false - default: null -notes: - - This module requires Dell OS9 version 9.10.0.1P13 or above. - - - This module requires to increase the ssh connection rate limit. - Use the following command I(ip ssh connection-rate-limit 60) - to configure the same. This can be done via M(dnos_config) module - as well. -""" - -EXAMPLES = """ -- name: push a configuration onto the device - dellos9_template: - host: hostname - username: foo - src: config.j2 - -- name: forceable push a configuration onto the device - dellos9_template: - host: hostname - username: foo - src: config.j2 - force: yes - -- name: provide the base configuration for comparison - dellos9_template: - host: hostname - username: foo - src: candidate_config.txt - config: current_config.txt -""" - -RETURN = """ -updates: - description: The set of commands that will be pushed to the remote device - returned: always - type: list - sample: ['...', '...'] - -_backup: - description: The current running config of the remote device. - returned: when running config is present in the remote device. - type: list - sample: ['...', '...'] - -responses: - description: The set of responses from issuing the commands on the device - returned: when not check_mode - type: list - sample: ['...', '...'] -""" -from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.network import NetworkModule -import ansible.module_utils.dellos9 - - -def get_config(module): - config = module.params['config'] or dict() - if not config and not module.params['force']: - config = module.config.get_config() - return config - - -def main(): - """ main entry point for module execution - """ - - argument_spec = dict( - src=dict(), - force=dict(default=False, type='bool'), - backup=dict(default=False, type='bool'), - config=dict(), - ) - - mutually_exclusive = [('config', 'backup'), ('config', 'force')] - - module = NetworkModule(argument_spec=argument_spec, - mutually_exclusive=mutually_exclusive, - supports_check_mode=True) - - result = dict(changed=False) - - candidate = NetworkConfig(contents=module.params['src'], indent=1) - - contents = get_config(module) - - if contents: - config = NetworkConfig(contents=contents[0], indent=1) - result['_backup'] = contents[0] - - commands = list() - if not module.params['force']: - commands = dumps(candidate.difference(config), 'commands') - else: - commands = str(candidate) - - if commands: - commands = commands.split('\n') - if not module.check_mode: - response = module.config(commands) - result['responses'] = response - result['changed'] = True - - result['updates'] = commands - module.exit_json(**result) - - -if __name__ == '__main__': - main() From ef85fcb068adeea6498f62809abb8ff488ef5192 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Sat, 1 Oct 2016 20:34:21 -0400 Subject: [PATCH 474/770] Fixing nxos_hsrp (#5009) * Fixing nxos_hsrp * Adding space after = --- network/nxos/nxos_hsrp.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index be7cbd5c2b0..b218d295910 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -277,12 +277,21 @@ def load_config(module, candidate): def execute_config_command(commands, module): try: - response = module.configure(commands) + output = module.configure(commands) except ShellError: clie = get_exception() module.fail_json(msg='Error sending CLI commands', error=str(clie), commands=commands) - return response + except AttributeError: + try: + commands.insert(0, 'configure') + module.cli.add_commands(commands, output='config') + output = module.cli.run_commands() + except ShellError: + clie = get_exception() + module.fail_json(msg='Error sending CLI commands', + error=str(clie), commands=commands) + return output def get_cli_body_ssh(command, response, module): @@ -329,7 +338,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -675,6 +684,8 @@ def main(): validate_config(body, vip, module) changed = True end_state = get_hsrp_group(group, interface, module) + if 'configure' in commands: + commands.pop(0) results = {} results['proposed'] = proposed From d47e38a8f99473be4099c7d2afb26de0c739fdfa Mon Sep 17 00:00:00 2001 From: Gabriele Date: Sat, 1 Oct 2016 20:34:31 -0400 Subject: [PATCH 475/770] Fixing nxos_igmp_interface (#5010) --- network/nxos/nxos_igmp_interface.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index 5f74d34eab1..1f5aa5d9a88 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -430,7 +430,7 @@ def execute_show(cmds, module, command_type=None): module.cli.add_commands(cmds, output=command_type) response = module.cli.run_commands() else: - module.cli.add_commands(cmds, output=command_type) + module.cli.add_commands(cmds, raw=True) response = module.cli.run_commands() except ShellError: clie = get_exception() @@ -715,6 +715,15 @@ def execute_config_command(commands, module): 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 main(): @@ -881,6 +890,8 @@ def main(): execute_config_command(cmds, module) changed = True end_state = get_igmp_interface(module, interface) + if 'configure' in cmds: + cmds.pop(0) results['proposed'] = proposed results['existing'] = existing_copy @@ -892,4 +903,4 @@ def main(): if __name__ == '__main__': - main() + main() \ No newline at end of file From d35cd80b7e77feacbfaa35c85c1c0ba422a3f165 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Sat, 1 Oct 2016 20:34:43 -0400 Subject: [PATCH 476/770] Improving nxos_igmp_snooping (#5011) --- network/nxos/nxos_igmp_snooping.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/network/nxos/nxos_igmp_snooping.py b/network/nxos/nxos_igmp_snooping.py index 1e11a9246d5..8b8ad6620af 100644 --- a/network/nxos/nxos_igmp_snooping.py +++ b/network/nxos/nxos_igmp_snooping.py @@ -537,6 +537,8 @@ def main(): changed = True execute_config_command(cmds, module) end_state = get_igmp_snooping(module) + if 'configure' in cmds: + cmds.pop(0) results['proposed'] = proposed results['existing'] = existing From 4b3c892284a7f68b187199be7725744661c53993 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 2 Oct 2016 08:54:20 -0700 Subject: [PATCH 477/770] (re)add post-watchdog-launch sleep to Windows async_wrapper (#5122) fixed apparent race where subprocess appears to never start --- windows/async_wrapper.ps1 | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1 index a4f90094837..54d820fe58b 100644 --- a/windows/async_wrapper.ps1 +++ b/windows/async_wrapper.ps1 @@ -324,6 +324,10 @@ Function Start-Watchdog { # FUTURE: use CreateProcess + stream redirection to watch for/return quick watchdog failures? $result = $([wmiclass]"Win32_Process").Create($exec_path, $null, $pstartup) + # On fast + idle machines, the process never starts without this delay. Hopefully the switch to + # Win32 process launch will make this unnecessary. + Sleep -Seconds 1 + $watchdog_pid = $result.ProcessId return $watchdog_pid From 886757b4dd1de67fb52e1096dca91385169dccbc Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 2 Oct 2016 14:57:58 -0400 Subject: [PATCH 478/770] adds check for AddConditionError when adding conditional statements (#5123) The Conditional instance will now raise the AddConditionError and this change instructs eos_command to catch the error and return a nicely formed error message --- network/eos/eos_command.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index a506075b269..e1a0f806df7 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -149,7 +149,7 @@ from ansible.module_utils.basic import get_exception from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner -from ansible.module_utils.netcli import AddCommandError +from ansible.module_utils.netcli import AddCommandError, AddConditionError from ansible.module_utils.netcli import FailedConditionsError from ansible.module_utils.netcli import FailedConditionalError from ansible.module_utils.six import string_types @@ -212,8 +212,13 @@ def main(): exc = get_exception() warnings.append('duplicate command detected: %s' % cmd) - for item in conditionals: - runner.add_conditional(item) + try: + for item in conditionals: + runner.add_conditional(item) + except AddConditionError: + exc = get_exception() + module.fail_json(msg=str(exc), condition=exc.condition) + runner.retries = module.params['retries'] runner.interval = module.params['interval'] From 8afe2402dbc83eb624b6920a4f46aa9a26547c01 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 2 Oct 2016 15:11:11 -0400 Subject: [PATCH 479/770] adds exception handling for AddConditionError (#5124) AddConditionErrors are now handled by nxos_command and a well formed error is returned from the module --- network/nxos/nxos_command.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/network/nxos/nxos_command.py b/network/nxos/nxos_command.py index 9dc01a913c4..af302c2f752 100644 --- a/network/nxos/nxos_command.py +++ b/network/nxos/nxos_command.py @@ -155,7 +155,7 @@ from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import FailedConditionsError from ansible.module_utils.netcli import FailedConditionalError -from ansible.module_utils.netcli import AddCommandError +from ansible.module_utils.netcli import AddCommandError, AddConditionError VALID_KEYS = ['command', 'output', 'prompt', 'response'] @@ -214,8 +214,12 @@ def main(): exc = get_exception() warnings.append('duplicate command detected: %s' % cmd) - for item in conditionals: - runner.add_conditional(item) + try: + for item in conditionals: + runner.add_conditional(item) + except AddConditionError: + exc = get_exception() + module.fail_json(msg=str(exc), condition=exc.condition) runner.retries = module.params['retries'] runner.interval = module.params['interval'] From 423b4a59092db4cee0ec97e8e43e92cc1b16bd81 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Sun, 2 Oct 2016 15:14:43 -0400 Subject: [PATCH 480/770] Adding more details on DOCSTRING about how to use the module (#5121) --- network/nxos/nxos_install_os.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/network/nxos/nxos_install_os.py b/network/nxos/nxos_install_os.py index 566b03f5152..4f5150e689c 100644 --- a/network/nxos/nxos_install_os.py +++ b/network/nxos/nxos_install_os.py @@ -27,7 +27,10 @@ - The module will fail due to timeout issues, but the install will go on anyway. Ansible's block and rescue can be leveraged to handle this kind of failure and check actual module results. See EXAMPLE for more about - this. + this. The first task on the rescue block is needed to make sure the + device has completed all checks and it started to reboot. The second + task is needed to wait the device to come back up. Last two tasks are + used to verify the installation process's been successful. - Do not include full file paths, just the name of the file(s) stored on the top level flash directory. - You must know if your platform supports taking a kickstart image as a @@ -62,6 +65,13 @@ password: "{{ pwd }}" transport: nxapi rescue: + - name: Wait for device to perform checks + wait_for: + port: 22 + state: stopped + timeout: 300 + delay: 60 + host: "{{ inventory_hostname }}" - name: Wait for device to come back up wait_for: port: 22 From 4cb27d914f64c868a60ddd0d0633645e708fb107 Mon Sep 17 00:00:00 2001 From: Pradeep Date: Fri, 30 Sep 2016 12:09:05 +0530 Subject: [PATCH 481/770] Typo Fix --- cloud/amazon/ec2_asg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index d9ed229522b..c63ebdef4e8 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -778,7 +778,7 @@ def wait_for_term_inst(connection, module, term_instances): lifecycle = instance_facts[i]['lifecycle_state'] health = instance_facts[i]['health_status'] log.debug("Instance {0} has state of {1},{2}".format(i,lifecycle,health )) - if lifecycle == 'Terminating' or healthy == 'Unhealthy': + if lifecycle == 'Terminating' or health == 'Unhealthy': count += 1 time.sleep(10) From 857e1d374bb09a59f09f3edb8500baa9a5c12d6a Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 2 Oct 2016 16:46:38 -0400 Subject: [PATCH 482/770] adds exception handling for adding an invalid condition (#5125) This change will now handle a problem adding a condition that raises an AddConditionError and return a well formed error to the user. --- network/junos/junos_command.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index 02b2a04e7b0..6fc2b0a4ef3 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -158,7 +158,7 @@ from ansible.module_utils.network import NetworkModule, NetworkError from ansible.module_utils.netcli import CommandRunner from ansible.module_utils.netcli import AddCommandError, FailedConditionsError -from ansible.module_utils.netcli import FailedConditionalError +from ansible.module_utils.netcli import FailedConditionalError, AddConditionError from ansible.module_utils.junos import xml_to_json from ansible.module_utils.six import string_types @@ -261,9 +261,9 @@ def main(): try: for item in conditionals: runner.add_conditional(item) - except (ValueError, AttributeError): + except (ValueError, AddConditionError): exc = get_exception() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), condition=exc.condition) runner.retries = module.params['retries'] runner.interval = module.params['interval'] From e4c5a13a7ad6355f779a88456780bdcd86baee31 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 2 Oct 2016 21:41:21 -0700 Subject: [PATCH 483/770] Fix assemble module for python3 --- files/assemble.py | 45 ++++++++++++++++++++++++--------------------- 1 file changed, 24 insertions(+), 21 deletions(-) diff --git a/files/assemble.py b/files/assemble.py index 6eea02e547b..39edbdd36a4 100644 --- a/files/assemble.py +++ b/files/assemble.py @@ -2,6 +2,7 @@ # -*- coding: utf-8 -*- # (c) 2012, Stephen Fromm +# (c) 2016, Toshio Kuratomi # # This file is part of Ansible # @@ -18,11 +19,6 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import os -import os.path -import tempfile -import re - DOCUMENTATION = ''' --- module: assemble @@ -108,42 +104,53 @@ - assemble: src=/etc/ssh/conf.d/ dest=/etc/ssh/sshd_config validate='/usr/sbin/sshd -t -f %s' ''' +import codecs +import os +import os.path +import re +import tempfile + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.six import b + + # =========================================== # Support method def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, ignore_hidden=False): ''' assemble a file from a directory of fragments ''' tmpfd, temp_path = tempfile.mkstemp() - tmp = os.fdopen(tmpfd,'w') + tmp = os.fdopen(tmpfd, 'wb') delimit_me = False add_newline = False for f in sorted(os.listdir(src_path)): if compiled_regexp and not compiled_regexp.search(f): continue - fragment = "%s/%s" % (src_path, f) + fragment = u"%s/%s" % (src_path, f) if not os.path.isfile(fragment) or (ignore_hidden and os.path.basename(fragment).startswith('.')): continue - fragment_content = file(fragment).read() + fragment_content = open(fragment, 'rb').read() # always put a newline between fragments if the previous fragment didn't end with a newline. if add_newline: - tmp.write('\n') + tmp.write(b('\n')) # delimiters should only appear between fragments if delimit_me: if delimiter: # un-escape anything like newlines - delimiter = delimiter.decode('unicode-escape') + delimiter = codecs.escape_decode(delimiter)[0] tmp.write(delimiter) # always make sure there's a newline after the # delimiter, so lines don't run together - if delimiter[-1] != '\n': - tmp.write('\n') + if delimiter[-1] != b('\n'): + tmp.write(b('\n')) tmp.write(fragment_content) delimit_me = True - if fragment_content.endswith('\n'): + if fragment_content.endswith(b('\n')): add_newline = False else: add_newline = True @@ -151,6 +158,7 @@ def assemble_from_fragments(src_path, delimiter=None, compiled_regexp=None, igno tmp.close() return temp_path + def cleanup(path, result=None): # cleanup just in case if os.path.exists(path): @@ -162,8 +170,6 @@ def cleanup(path, result=None): if result is not None: result['warnings'] = ['Unable to remove temp file (%s): %s' % (path, str(e))] -# ============================================================== -# main def main(): @@ -201,7 +207,7 @@ def main(): if not os.path.isdir(src): module.fail_json(msg="Source (%s) is not a directory" % src) - if regexp != None: + if regexp is not None: try: compiled_regexp = re.compile(regexp) except re.error: @@ -248,8 +254,5 @@ def main(): result['msg'] = "OK" module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * - -main() - +if __name__ == '__main__': + main() From 6c4d71a7fab60601846363506bc8eebe9c52c240 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Mon, 3 Oct 2016 07:27:47 -0700 Subject: [PATCH 484/770] fixed windows setup to run in check_mode --- windows/setup.ps1 | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/windows/setup.ps1 b/windows/setup.ps1 index fcbe9ca1303..fef2ade25c4 100644 --- a/windows/setup.ps1 +++ b/windows/setup.ps1 @@ -17,9 +17,7 @@ # WANT_JSON # POWERSHELL_COMMON -# enabled $params (David O'Brien, 06/08/2015) -$params = Parse-Args $args; - +$params = Parse-Args $args -supports_check_mode $true Function Get-CustomFacts { [cmdletBinding()] From 2779ead45bbe5ca852dc8468893494c8aa64708a Mon Sep 17 00:00:00 2001 From: John R Barker Date: Mon, 3 Oct 2016 18:20:52 +0100 Subject: [PATCH 485/770] Add matching quotes (#5133) Will fix broken syntax highlighting --- utilities/logic/include_role.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index cdb29738d43..127313ec9f8 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -71,8 +71,8 @@ include_role: name: myrole with_items: - - '{{roleinput1}}" - - '{{roleinput2}}" + - "{{roleinput1}}" + - "{{roleinput2}}" loop_control: loop_var: roleinputvar From f1c27391638846094684d2d134768e0de1c0ecc6 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Mon, 3 Oct 2016 13:23:42 -0400 Subject: [PATCH 486/770] Handle termination_protection parameter when restarting instances (#5076) * Restart EC2 instances with multiple network interfaces A previous bug, #3234, caused instances with multiple ENI's to fail when being started or stopped because sourceDestCheck is a per-interface attribute, but we use the boto global access to it (which only works when there's a single ENI). This patch handles a variant of that bug that only surfaced when restarting an instance, and catches the same type of exception. * Default termination_protection to None instead of False AWS defaults the value of termination_protection to False, so we don't need to explicitly send `False` when the user hasn't specified a termination protection level. Before this patch, the below pair of tasks would: 1. Create an instance (enabling termination_protection) 2. Restart that instance (disabling termination_protection) Now, the default None value would prevent the restart task from disabling termination_protection. ``` - name: make an EC2 instance ec2: vpc_subnet_id: {{ subnet }} instance_type: t2.micro termination_protection: yes exact_count: 1 count_tag: Name: TestInstance instance_tags: Name: TestInstance group_id: {{ group }} image: ami-7172b611 wait: yes - name: restart a protected EC2 instance ec2: vpc_subnet_id: {{ subnet }} state: restarted instance_tags: Name: TestInstance group_id: {{ group }} image: ami-7172b611 wait: yes ``` --- cloud/amazon/ec2.py | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index b6f30b25547..bb0e9008e57 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1369,7 +1369,8 @@ def startstop_instances(module, ec2, instance_ids, state, instance_tags): exception=traceback.format_exc(exc)) # Check "termination_protection" attribute - if inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection: + if (inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection + and termination_protection is not None): inst.modify_attribute('disableApiTermination', termination_protection) changed = True @@ -1436,8 +1437,6 @@ def restart_instances(module, ec2, instance_ids, state, instance_tags): termination_protection = module.params.get('termination_protection') changed = False instance_dict_array = [] - source_dest_check = module.params.get('source_dest_check') - termination_protection = module.params.get('termination_protection') if not isinstance(instance_ids, list) or len(instance_ids) < 1: # Fail unless the user defined instance tags @@ -1455,17 +1454,30 @@ def restart_instances(module, ec2, instance_ids, state, instance_tags): # Check that our instances are not in the state we want to take # Check (and eventually change) instances attributes and instances state - running_instances_array = [] for res in ec2.get_all_instances(instance_ids, filters=filters): for inst in res.instances: # Check "source_dest_check" attribute - if inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: - inst.modify_attribute('sourceDestCheck', source_dest_check) - changed = True + try: + if inst.vpc_id is not None and inst.get_attribute('sourceDestCheck')['sourceDestCheck'] != source_dest_check: + inst.modify_attribute('sourceDestCheck', source_dest_check) + changed = True + except boto.exception.EC2ResponseError as exc: + # instances with more than one Elastic Network Interface will + # fail, because they have the sourceDestCheck attribute defined + # per-interface + if exc.code == 'InvalidInstanceID': + for interface in inst.interfaces: + if interface.source_dest_check != source_dest_check: + ec2.modify_network_interface_attribute(interface.id, "sourceDestCheck", source_dest_check) + changed = True + else: + module.fail_json(msg='Failed to handle source_dest_check state for instance {0}, error: {1}'.format(inst.id, exc), + exception=traceback.format_exc(exc)) # Check "termination_protection" attribute - if inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection: + if (inst.get_attribute('disableApiTermination')['disableApiTermination'] != termination_protection + and termination_protection is not None): inst.modify_attribute('disableApiTermination', termination_protection) changed = True @@ -1510,7 +1522,7 @@ def main(): instance_profile_name = dict(), instance_ids = dict(type='list', aliases=['instance_id']), source_dest_check = dict(type='bool', default=True), - termination_protection = dict(type='bool', default=False), + termination_protection = dict(type='bool', default=None), state = dict(default='present', choices=['present', 'absent', 'running', 'restarted', 'stopped']), instance_initiated_shutdown_behavior=dict(default=None, choices=['stop', 'terminate']), exact_count = dict(type='int', default=None), From 95755d1859354bf9da5eb0d8a4b4b65e8c5b0431 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 3 Oct 2016 14:09:42 -0700 Subject: [PATCH 487/770] We've changed run_command to return native_strings on python3, this means that we don't get bytes back by default. We probably do want bytes here so modify our call to run_command so we get bytes instead of text. --- commands/command.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/commands/command.py b/commands/command.py index 410efb02ca2..6c75d3f0567 100644 --- a/commands/command.py +++ b/commands/command.py @@ -95,13 +95,13 @@ import datetime import glob -import re import shlex import os from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import b + def check_command(commandline): arguments = { 'chown': 'owner', 'chmod': 'mode', 'chgrp': 'group', 'ln': 'state=link', 'mkdir': 'state=directory', @@ -142,9 +142,9 @@ def main(): shell = module.params['_uses_shell'] chdir = module.params['chdir'] executable = module.params['executable'] - args = module.params['_raw_params'] - creates = module.params['creates'] - removes = module.params['removes'] + args = module.params['_raw_params'] + creates = module.params['creates'] + removes = module.params['removes'] warn = module.params['warn'] if args.strip() == '': @@ -167,9 +167,9 @@ def main(): ) if removes: - # do not run the command if the line contains removes=filename - # and the filename does not exist. This allows idempotence - # of command executions. + # do not run the command if the line contains removes=filename + # and the filename does not exist. This allows idempotence + # of command executions. if not glob.glob(removes): module.exit_json( cmd=args, @@ -186,7 +186,7 @@ def main(): args = shlex.split(args) startd = datetime.datetime.now() - rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell) + rc, out, err = module.run_command(args, executable=executable, use_unsafe_shell=shell, encoding=None) endd = datetime.datetime.now() delta = endd - startd From 0ee774ff150b8c9b2fb6a0273737f4ebd3c10dcd Mon Sep 17 00:00:00 2001 From: Laurent Godet Date: Mon, 3 Oct 2016 22:45:57 +0100 Subject: [PATCH 488/770] Fix daemon_reload in systemd module --- system/systemd.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/systemd.py b/system/systemd.py index 874b99528a6..a92c776a038 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -76,7 +76,7 @@ # Example action to stop service cron on debian, if running - systemd: name=cron state=stopped # Example action to restart service cron on centos, in all cases, also issue deamon-reload to pick up config changes -- systemd: state=restarted daemon_reload: yes name=crond +- systemd: state=restarted daemon_reload=yes name=crond # Example action to reload service httpd, in all cases - systemd: name=httpd state=reloaded # Example action to enable service httpd and ensure it is not masked From c5b2dafa59da3dd2acc8ca042c8d87bd0dd99385 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Tue, 4 Oct 2016 18:45:09 +0100 Subject: [PATCH 489/770] Remove docs for commit which no longer exists (#5152) --- network/ios/ios_config.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index bddb9e40ea6..7ca12716d42 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -110,17 +110,6 @@ required: false default: false choices: ["true", "false"] - commit: - description: - - This argument specifies the update method to use when applying the - configuration changes to the remote node. If the value is set to - I(merge) the configuration updates are merged with the running- - config. If the value is set to I(check), no changes are made to - the remote host. - required: false - default: merge - choices: ['merge', 'check'] - version_added: "2.2" backup: description: - This argument will cause the module to create a full backup of @@ -134,7 +123,7 @@ version_added: "2.2" config: description: - - The C(config) argument allows the playbook desginer to supply + - The C(config) argument allows the playbook designer to supply the base configuration to be used to validate configuration changes necessary. If this argument is provided, the module will not download the running-config from the remote node. From 6103082c256c53465a089c643ce7bd6c4697afeb Mon Sep 17 00:00:00 2001 From: John R Barker Date: Tue, 4 Oct 2016 21:19:30 +0100 Subject: [PATCH 490/770] Remove reference to parents parameter which doesn't exist (#5143) --- network/junos/junos_config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index afa711bd3bf..80cfc39a4f9 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -43,8 +43,7 @@ to load into the remote system. The path can either be a full system path to the configuration file if the value starts with / or relative to the root of the implemented role or playbook. - This argument is mutually exclusive with the I(lines) and - I(parents) arguments. + This argument is mutually exclusive with the I(lines) argument. required: false default: null version_added: "2.2" From aee430adf93a3c4618aedb5a89b5f87c98e9c7f9 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Tue, 4 Oct 2016 16:21:50 -0400 Subject: [PATCH 491/770] checks commit comment to make sure it doesn't exceed 60 characters (#5155) The comment argument can be at most 60 characters per the IOS XR command line. If a comment is > 60 characters, the module will now gracefully error and return a well formed message. fixes 5146 --- network/iosxr/iosxr_config.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 0dc6984fc48..15978ff108b 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -194,6 +194,9 @@ def check_args(module, warnings): + if module.params['comment']: + if len(module.params['comment']) > 60: + module.fail_json(msg='comment argument cannot be more than 60 characters') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' From a292a2c16885060ca2ca3643904bb119420a5117 Mon Sep 17 00:00:00 2001 From: Gabriele Date: Tue, 4 Oct 2016 16:23:33 -0400 Subject: [PATCH 492/770] Fixing docstring (#5130) --- network/nxos/nxos_install_os.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/nxos/nxos_install_os.py b/network/nxos/nxos_install_os.py index 4f5150e689c..705e13b316d 100644 --- a/network/nxos/nxos_install_os.py +++ b/network/nxos/nxos_install_os.py @@ -29,8 +29,8 @@ of failure and check actual module results. See EXAMPLE for more about this. The first task on the rescue block is needed to make sure the device has completed all checks and it started to reboot. The second - task is needed to wait the device to come back up. Last two tasks are - used to verify the installation process's been successful. + task is needed to wait for the device to come back up. The last two tasks + are used to verify the installation process was successful. - Do not include full file paths, just the name of the file(s) stored on the top level flash directory. - You must know if your platform supports taking a kickstart image as a From a10ce8ff25ec21878ac64400bba695d38925f79d Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Tue, 4 Oct 2016 22:03:28 -0400 Subject: [PATCH 493/770] Improved build and pull error handling --- cloud/docker/docker_service.py | 53 +++++++++++++++++++++++----------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index 3e7d4335813..b0231ef80d4 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -464,7 +464,7 @@ try: from compose import __version__ as compose_version from compose.cli.command import project_from_options - from compose.service import ConvergenceStrategy + from compose.service import ConvergenceStrategy, NoSuchImageError from compose.cli.main import convergence_strategy_from_opts, build_action_from_opts, image_type_from_opt from compose.const import DEFAULT_TIMEOUT except ImportError as exc: @@ -736,19 +736,30 @@ def cmd_pull(self): for service in self.project.get_services(self.services, include_deps=False): self.log('Pulling image for service %s' % service.name) # store the existing image ID - image = service.image() - old_image_id = None - if image and image.get('Id'): - old_image_id = image['Id'] + old_image_id = '' + try: + image = service.image() + if image and image.get('Id'): + old_image_id = image['Id'] + except NoSuchImageError: + pass + except Exception as exc: + self.client.fail("Error: service image lookup failed - %s" % str(exc)) # pull the image - service.pull(ignore_pull_failures=False) + try: + service.pull(ignore_pull_failures=False) + except Exception as exc: + self.client.fail("Error: pull failed with %s" % str(exc)) # store the new image ID - image = service.image() - new_image_id = None - if image and image.get('Id'): - new_image_id = image['Id'] + new_image_id = '' + try: + image = service.image() + if image and image.get('Id'): + new_image_id = image['Id'] + except NoSuchImageError as exc: + self.client.fail("Error: service image lookup failed after pull - %s" % str(exc)) if new_image_id != old_image_id: # if a new image was pulled @@ -756,7 +767,7 @@ def cmd_pull(self): result['actions'][service.name] = dict() result['actions'][service.name]['pulled_image'] = dict( name=service.image_name, - id=service.image()['Id'] + id=new_image_id ) return result @@ -770,13 +781,21 @@ def cmd_build(self): self.log('Building image for service %s' % service.name) if service.can_be_built(): # store the existing image ID - image = service.image() - old_image_id = None - if image and image.get('Id'): - old_image_id = image['Id'] + old_image_id = '' + try: + image = service.image() + if image and image.get('Id'): + old_image_id = image['Id'] + except NoSuchImageError: + pass + except Exception as exc: + self.client.fail("Error: service image lookup failed - %s" % str(exc)) # build the image - new_image_id = service.build(pull=True, no_cache=self.nocache) + try: + new_image_id = service.build(pull=True, no_cache=self.nocache) + except Exception as exc: + self.client.fail("Error: build failed with %s" % str(exc)) if new_image_id not in old_image_id: # if a new image was built @@ -784,7 +803,7 @@ def cmd_build(self): result['actions'][service.name] = dict() result['actions'][service.name]['built_image'] = dict( name=service.image_name, - id=service.image()['Id'] + id=new_image_id ) return result From fba5883dd19fd30a02ec2df3c1bd837ec7e91635 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 5 Oct 2016 00:45:27 -0400 Subject: [PATCH 494/770] Improved enumeration of actions --- cloud/docker/docker_service.py | 133 +++++++++++++++++++-------------- 1 file changed, 77 insertions(+), 56 deletions(-) diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index b0231ef80d4..369854626c6 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -484,7 +484,7 @@ u'tls_verify': u'--tlsverify' } - + class ContainerManager(DockerBaseClass): def __init__(self, client): @@ -598,11 +598,11 @@ def _get_auth_options(self): return options def cmd_up(self): - + start_deps = self.dependencies service_names = self.services detached = True - result = dict(changed=False, actions=dict(), ansible_facts=dict()) + result = dict(changed=False, actions=[], ansible_facts=dict()) up_options = { u'--no-recreate': False, @@ -624,26 +624,29 @@ def cmd_up(self): self.log("convergence strategy: %s" % converge) if self.pull: - result.update(self.cmd_pull()) + pull_output = self.cmd_pull() + result['changed'] = pull_output['changed'] + result['actions'] += pull_output['actions'] if self.build: - result.update(self.cmd_build()) + build_output = self.cmd_build() + result['changed'] = build_output['changed'] + result['actions'] += build_output['actions'] for service in self.project.services: if not service_names or service.name in service_names: plan = service.convergence_plan(strategy=converge) if plan.action != 'noop': result['changed'] = True - if self.debug or self.check_mode: - if not result['actions'].get(service.name): - result['actions'][service.name] = dict() - result['actions'][service.name][plan.action] = [] + result_action = dict(service=service.name) + result_action[plan.action] = [] for container in plan.containers: - result['actions'][service.name][plan.action].append(dict( + result_action[plan.action].append(dict( id=container.id, name=container.name, short_id=container.short_id, )) + result['actions'].append(result_action) if not self.check_mode and result['changed']: try: @@ -661,13 +664,19 @@ def cmd_up(self): self.client.fail("Error starting project - %s" % str(exc)) if self.stopped: - result.update(self.cmd_stop(service_names)) + stop_output = self.cmd_stop(service_names) + result['changed'] = stop_output['changed'] + result['actions'] += stop_output['actions'] if self.restarted: - result.update(self.cmd_restart(service_names)) + restart_output = self.cmd_restart(service_names) + result['changed'] = restart_output['changed'] + result['actions'] += restart_output['actions'] if self.scale: - result.update(self.cmd_scale()) + scale_output = self.cmd_scale() + result['changed'] = scale_output['changed'] + result['actions'] += scale_output['actions'] for service in self.project.services: result['ansible_facts'][service.name] = dict() @@ -729,11 +738,14 @@ def cmd_up(self): def cmd_pull(self): result = dict( changed=False, - actions=dict(), + actions=[], ) if not self.check_mode: for service in self.project.get_services(self.services, include_deps=False): + if 'image' not in service.options: + continue + self.log('Pulling image for service %s' % service.name) # store the existing image ID old_image_id = '' @@ -764,22 +776,24 @@ def cmd_pull(self): if new_image_id != old_image_id: # if a new image was pulled result['changed'] = True - result['actions'][service.name] = dict() - result['actions'][service.name]['pulled_image'] = dict( - name=service.image_name, - id=new_image_id - ) + result['actions'].append(dict( + service=service.name, + pulled_image=dict( + name=service.image_name, + id=new_image_id + ) + )) return result def cmd_build(self): result = dict( changed=False, - actions=dict(), + actions=[] ) if not self.check_mode: for service in self.project.get_services(self.services, include_deps=False): - self.log('Building image for service %s' % service.name) if service.can_be_built(): + self.log('Building image for service %s' % service.name) # store the existing image ID old_image_id = '' try: @@ -800,53 +814,55 @@ def cmd_build(self): if new_image_id not in old_image_id: # if a new image was built result['changed'] = True - result['actions'][service.name] = dict() - result['actions'][service.name]['built_image'] = dict( - name=service.image_name, - id=new_image_id - ) + result['actions'].append(dict( + service=service.name, + built_image=dict( + name=service.image_name, + id=new_image_id + ) + )) return result def cmd_down(self): result = dict( changed=False, - actions=dict(), + actions=[] ) - for service in self.project.services: containers = service.containers(stopped=True) if len(containers): result['changed'] = True - if self.debug or self.check_mode: - result['actions'][service.name] = dict() - result['actions'][service.name]['deleted'] = [container.name for container in containers] - + result['actions'].append(dict( + service=service.name, + deleted=[container.name for container in containers] + )) if not self.check_mode and result['changed']: image_type = image_type_from_opt('--rmi', self.remove_images) try: self.project.down(image_type, self.remove_volumes, self.remove_orphans) except Exception as exc: self.client.fail("Error stopping project - %s" % str(exc)) - return result def cmd_stop(self, service_names): result = dict( changed=False, - actions=dict() + actions=[] ) for service in self.project.services: if not service_names or service.name in service_names: - result['actions'][service.name] = dict() - result['actions'][service.name]['stop'] = [] + service_res = dict( + service=service.name, + stop=[] + ) for container in service.containers(stopped=False): result['changed'] = True - if self.debug: - result['actions'][service.name]['stop'].append(dict( - id=container.id, - name=container.name, - short_id=container.short_id, - )) + service_res['stop'].append(dict( + id=container.id, + name=container.name, + short_id=container.short_id + )) + result['actions'].append(service_res) if not self.check_mode and result['changed']: try: @@ -859,22 +875,24 @@ def cmd_stop(self, service_names): def cmd_restart(self, service_names): result = dict( changed=False, - actions=dict() + actions=[] ) for service in self.project.services: if not service_names or service.name in service_names: - result['actions'][service.name] = dict() - result['actions'][service.name]['restart'] = [] + service_res = dict( + service=service.name, + restart=[] + ) for container in service.containers(stopped=True): result['changed'] = True - if self.debug or self.check_mode: - result['actions'][service.name]['restart'].append(dict( - id=container.id, - name=container.name, - short_id=container.short_id, - )) - + service_res['restart'].append(dict( + id=container.id, + name=container.name, + short_id=container.short_id + )) + result['actions'].append(service_res) + if not self.check_mode and result['changed']: try: self.project.restart(service_names=service_names, timeout=self.timeout) @@ -886,22 +904,25 @@ def cmd_restart(self, service_names): def cmd_scale(self): result = dict( changed=False, - actions=dict() + actions=[] ) for service in self.project.services: if service.name in self.scale: - result['actions'][service.name] = dict() + service_res = dict( + service=service.name, + scale=0 + ) containers = service.containers(stopped=True) if len(containers) != self.scale[service.name]: result['changed'] = True - if self.debug or self.check_mode: - result['actions'][service.name]['scale'] = self.scale[service.name] - len(containers) + service_res['scale'] = self.scale[service.name] - len(containers) if not self.check_mode: try: service.scale(int(self.scale[service.name])) except Exception as exc: self.client.fail("Error scaling %s - %s" % (service.name, str(exc))) + result['actions'].append(service_res) return result From 0156cb942e608df6f53760d33a9b3dfe56b409d3 Mon Sep 17 00:00:00 2001 From: Chris Houseknecht Date: Wed, 5 Oct 2016 07:33:50 -0400 Subject: [PATCH 495/770] Surface Compose stdout on failure Signed-off-by: Chris Houseknecht --- cloud/docker/docker_service.py | 84 ++++++++++++++++++++++++++-------- 1 file changed, 64 insertions(+), 20 deletions(-) diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index 369854626c6..c4fa36c9af6 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -452,6 +452,9 @@ HAS_COMPOSE_EXC = None MINIMUM_COMPOSE_VERSION = '1.7.0' +import sys +import re + try: import yaml except ImportError as exc: @@ -463,6 +466,7 @@ try: from compose import __version__ as compose_version + from compose.project import ProjectError from compose.cli.command import project_from_options from compose.service import ConvergenceStrategy, NoSuchImageError from compose.cli.main import convergence_strategy_from_opts, build_action_from_opts, image_type_from_opt @@ -473,6 +477,7 @@ DEFAULT_TIMEOUT = 10 from ansible.module_utils.docker_common import * +from contextlib import contextmanager AUTH_PARAM_MAPPING = { @@ -484,7 +489,32 @@ u'tls_verify': u'--tlsverify' } - + +@contextmanager +def stdout_redirector(path_name): + old_stdout = sys.stdout + fd = open(path_name, 'w') + sys.stdout = fd + try: + yield + finally: + sys.stdout = old_stdout + +def get_stdout(path_name): + full_stdout = '' + last_line = '' + with open(path_name, 'r') as fd: + for line in fd: + # strip terminal format/color chars + new_line = re.sub(r'\x1b\[.+m', '', line.encode('ascii')) + full_stdout += new_line + if new_line.strip(): + # Assuming last line contains the error message + last_line = new_line.strip().encode('utf-8') + fd.close() + os.remove(path_name) + return full_stdout, last_line + class ContainerManager(DockerBaseClass): def __init__(self, client): @@ -649,19 +679,25 @@ def cmd_up(self): result['actions'].append(result_action) if not self.check_mode and result['changed']: + _, fd_name = tempfile.mkstemp(prefix="ansible") try: - do_build = build_action_from_opts(up_options) - self.log('Setting do_build to %s' % do_build) - self.project.up( - service_names=service_names, - start_deps=start_deps, - strategy=converge, - do_build=do_build, - detached=detached, - remove_orphans=self.remove_orphans, - timeout=self.timeout) + with stdout_redirector(fd_name): + do_build = build_action_from_opts(up_options) + self.log('Setting do_build to %s' % do_build) + self.project.up( + service_names=service_names, + start_deps=start_deps, + strategy=converge, + do_build=do_build, + detached=detached, + remove_orphans=self.remove_orphans, + timeout=self.timeout) except Exception as exc: - self.client.fail("Error starting project - %s" % str(exc)) + full_stdout, last_line= get_stdout(fd_name) + self.client.module.fail_json(msg="Error starting project %s" % str(exc), module_stderr=last_line, + module_stdout=full_stdout) + else: + get_stdout(fd_name) if self.stopped: stop_output = self.cmd_stop(service_names) @@ -863,13 +899,17 @@ def cmd_stop(self, service_names): short_id=container.short_id )) result['actions'].append(service_res) - if not self.check_mode and result['changed']: + _, fd_name = tempfile.mkstemp(prefix="ansible") try: - self.project.stop(service_names=service_names, timeout=self.timeout) + with stdout_redirector(fd_name): + self.project.stop(service_names=service_names, timeout=self.timeout) except Exception as exc: - self.client.fail("Error stopping services for %s - %s" % (self.project.name, str(exc))) - + full_stdout, last_line = get_stdout(fd_name) + self.client.module.fail_json(msg="Error stopping project %s" % str(exc), module_stderr=last_line, + module_stdout=full_stdout) + else: + get_stdout(fd_name) return result def cmd_restart(self, service_names): @@ -894,11 +934,16 @@ def cmd_restart(self, service_names): result['actions'].append(service_res) if not self.check_mode and result['changed']: + _, fd_name = tempfile.mkstemp(prefix="ansible") try: - self.project.restart(service_names=service_names, timeout=self.timeout) + with stdout_redirector(fd_name): + self.project.restart(service_names=service_names, timeout=self.timeout) except Exception as exc: - self.client.fail("Error restarting services for %s - %s" % (self.project.name, str(exc))) - + full_stdout, last_line = get_stdout(fd_name) + self.client.module.fail_json(msg="Error restarting project %s" % str(exc), module_stderr=last_line, + module_stdout=full_stdout) + else: + get_stdout(fd_name) return result def cmd_scale(self): @@ -906,7 +951,6 @@ def cmd_scale(self): changed=False, actions=[] ) - for service in self.project.services: if service.name in self.scale: service_res = dict( From de9dc9b58a9ea388d97739f9fdd60b8d8922c667 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 4 Oct 2016 12:44:10 -0700 Subject: [PATCH 496/770] Fix exception hierarchy for digital ocean and some cleanups of pep8 style Fixes #4613 --- cloud/digital_ocean/digital_ocean.py | 27 ++++++---- .../digital_ocean_block_storage.py | 51 +++++++++++-------- cloud/digital_ocean/digital_ocean_domain.py | 21 ++++---- cloud/digital_ocean/digital_ocean_sshkey.py | 18 +++---- cloud/digital_ocean/digital_ocean_tag.py | 9 ++-- 5 files changed, 68 insertions(+), 58 deletions(-) diff --git a/cloud/digital_ocean/digital_ocean.py b/cloud/digital_ocean/digital_ocean.py index 1a5e879ba6f..e75e77e9bd1 100644 --- a/cloud/digital_ocean/digital_ocean.py +++ b/cloud/digital_ocean/digital_ocean.py @@ -109,7 +109,7 @@ notes: - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. They both refer to the v2 token. - As of Ansible 1.9.5 and 2.0, Version 2 of the DigitalOcean API is used, this removes C(client_id) and C(api_key) options in favor of C(api_token). - - If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired. + - If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired. Upgrade Ansible or, if unable to, try downloading the latest version of this module from github and putting it into a 'library' directory. requirements: - "python >= 2.6" @@ -180,6 +180,8 @@ import os import time +import traceback + from distutils.version import LooseVersion HAS_DOPY = True @@ -191,15 +193,21 @@ except ImportError: HAS_DOPY = False -class TimeoutError(DoError): - def __init__(self, msg, id): +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native + + +class TimeoutError(Exception): + def __init__(self, msg, id_): super(TimeoutError, self).__init__(msg) - self.id = id + self.id = id_ + class JsonfyMixIn(object): def to_json(self): return self.__dict__ + class Droplet(JsonfyMixIn): manager = None @@ -283,6 +291,7 @@ def list_all(cls): json = cls.manager.all_active_droplets() return map(cls, json) + class SSH(JsonfyMixIn): manager = None @@ -318,6 +327,7 @@ def add(cls, name, key_pub): json = cls.manager.new_ssh_key(name, key_pub) return cls(json) + def core(module): def getkeyordie(k): v = module.params[k] @@ -385,7 +395,7 @@ def getkeyordie(k): if not droplet: module.exit_json(changed=False, msg='The droplet is not found.') - event_json = droplet.destroy() + droplet.destroy() module.exit_json(changed=True) elif command == 'ssh': @@ -446,12 +456,9 @@ def main(): try: core(module) except TimeoutError as e: - module.fail_json(msg=str(e), id=e.id) + module.fail_json(msg=to_native(e), id=e.id) except (DoError, Exception) as e: - module.fail_json(msg=str(e)) - -# import module snippets -from ansible.module_utils.basic import * + module.fail_json(msg=to_native(e), exception=traceback.format_exc()) if __name__ == '__main__': main() diff --git a/cloud/digital_ocean/digital_ocean_block_storage.py b/cloud/digital_ocean/digital_ocean_block_storage.py index 0087942323b..42c9df73d13 100644 --- a/cloud/digital_ocean/digital_ocean_block_storage.py +++ b/cloud/digital_ocean/digital_ocean_block_storage.py @@ -16,15 +16,12 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . -import json -import time - DOCUMENTATION = ''' --- module: digital_ocean_block_storage short_description: Create/destroy or attach/detach Block Storage volumes in DigitalOcean description: - - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet. + - Create/destroy Block Storage volume in DigitalOcean, or attach/detach Block Storage volume to a droplet. version_added: "2.2" options: command: @@ -113,9 +110,19 @@ sample: "69b25d9a-494c-12e6-a5af-001f53126b44" ''' +import json +import os +import time + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + + class DOBlockStorageException(Exception): pass + class Response(object): def __init__(self, resp, info): @@ -137,6 +144,7 @@ def json(self): def status_code(self): return self.info["status"] + class Rest(object): def __init__(self, module, headers): @@ -169,6 +177,7 @@ def post(self, path, data=None, headers=None): def delete(self, path, data=None, headers=None): return self.send('DELETE', path, data, headers) + class DOBlockStorage(object): def __init__(self, module): @@ -206,9 +215,9 @@ def get_attached_droplet_ID(self, volume_name, region): json = response.json if status == 200: volumes = json['volumes'] - if len(volumes)>0: + if len(volumes) > 0: droplet_ids = volumes[0]['droplet_ids'] - if len(droplet_ids)>0: + if len(droplet_ids) > 0: return droplet_ids[0] return None else: @@ -216,10 +225,10 @@ def get_attached_droplet_ID(self, volume_name, region): def attach_detach_block_storage(self, method, volume_name, region, droplet_id): data = { - 'type' : method, - 'volume_name' : volume_name, - 'region' : region, - 'droplet_id' : droplet_id + 'type': method, + 'volume_name': volume_name, + 'region': region, + 'droplet_id': droplet_id } response = self.rest.post('volumes/actions', data=data) status = response.status_code @@ -239,10 +248,10 @@ def create_block_storage(self): region = self.get_key_or_fail('region') description = self.module.params['description'] data = { - 'size_gigabytes' : block_size, - 'name' : volume_name, - 'description' : description, - 'region' : region + 'size_gigabytes': block_size, + 'name': volume_name, + 'description': description, + 'region': region } response = self.rest.post("volumes", data=data) status = response.status_code @@ -259,7 +268,7 @@ def delete_block_storage(self): region = self.get_key_or_fail('region') url = 'volumes?name={}®ion={}'.format(volume_name, region) attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) - if attached_droplet_id != None: + if attached_droplet_id is not None: self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) response = self.rest.delete(url) status = response.status_code @@ -276,8 +285,8 @@ def attach_block_storage(self): region = self.get_key_or_fail('region') droplet_id = self.get_key_or_fail('droplet_id') attached_droplet_id = self.get_attached_droplet_ID(volume_name, region) - if attached_droplet_id != None: - if attached_droplet_id==droplet_id: + if attached_droplet_id is not None: + if attached_droplet_id == droplet_id: self.module.exit_json(changed=False) else: self.attach_detach_block_storage('detach', volume_name, region, attached_droplet_id) @@ -291,6 +300,7 @@ def detach_block_storage(self): changed_status = self.attach_detach_block_storage('detach', volume_name, region, droplet_id) self.module.exit_json(changed=changed_status) + def handle_request(module): block_storage = DOBlockStorage(module) command = module.params['command'] @@ -301,11 +311,12 @@ def handle_request(module): elif state == 'absent': block_storage.delete_block_storage() elif command == 'attach': - if state =='present': + if state == 'present': block_storage.attach_block_storage() elif state == 'absent': block_storage.detach_block_storage() + def main(): module = AnsibleModule( argument_spec=dict( @@ -329,7 +340,5 @@ def main(): e = get_exception() module.fail_json(msg='Unable to load %s' % e.message) -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/cloud/digital_ocean/digital_ocean_domain.py b/cloud/digital_ocean/digital_ocean_domain.py index 565fec030f0..b91e47ffb67 100644 --- a/cloud/digital_ocean/digital_ocean_domain.py +++ b/cloud/digital_ocean/digital_ocean_domain.py @@ -46,7 +46,7 @@ notes: - Two environment variables can be used, DO_API_KEY and DO_API_TOKEN. They both refer to the v2 token. - As of Ansible 1.9.5 and 2.0, Version 2 of the DigitalOcean API is used, this removes C(client_id) and C(api_key) options in favor of C(api_token). - - If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired. + - If you are running Ansible 1.9.4 or earlier you might not be able to use the included version of this module as the API version used has been retired. requirements: - "python >= 2.6" @@ -82,7 +82,7 @@ ''' import os -import time +import traceback try: from dopy.manager import DoError, DoManager @@ -90,15 +90,14 @@ except ImportError as e: HAS_DOPY = False -class TimeoutError(DoError): - def __init__(self, msg, id): - super(TimeoutError, self).__init__(msg) - self.id = id +from ansible.module_utils.basic import AnsibleModule + class JsonfyMixIn(object): def to_json(self): return self.__dict__ + class DomainRecord(JsonfyMixIn): manager = None @@ -106,7 +105,7 @@ def __init__(self, json): self.__dict__.update(json) update_attr = __init__ - def update(self, data = None, record_type = None): + def update(self, data=None, record_type=None): json = self.manager.edit_domain_record(self.domain_id, self.id, record_type if record_type is not None else self.record_type, @@ -118,6 +117,7 @@ def destroy(self): json = self.manager.destroy_domain_record(self.domain_id, self.id) return json + class Domain(JsonfyMixIn): manager = None @@ -165,6 +165,7 @@ def find(cls, name=None, id=None): return False + def core(module): def getkeyordie(k): v = module.params[k] @@ -236,12 +237,8 @@ def main(): try: core(module) - except TimeoutError as e: - module.fail_json(msg=str(e), id=e.id) except (DoError, Exception) as e: - module.fail_json(msg=str(e)) + module.fail_json(msg=str(e), exception=traceback.format_exc()) -# import module snippets -from ansible.module_utils.basic import * if __name__ == '__main__': main() diff --git a/cloud/digital_ocean/digital_ocean_sshkey.py b/cloud/digital_ocean/digital_ocean_sshkey.py index 25f3b1ef7d0..e15822dc94b 100644 --- a/cloud/digital_ocean/digital_ocean_sshkey.py +++ b/cloud/digital_ocean/digital_ocean_sshkey.py @@ -69,7 +69,7 @@ ''' import os -import time +import traceback try: from dopy.manager import DoError, DoManager @@ -77,15 +77,14 @@ except ImportError: HAS_DOPY = False -class TimeoutError(DoError): - def __init__(self, msg, id): - super(TimeoutError, self).__init__(msg) - self.id = id +from ansible.module_utils.basic import AnsibleModule + class JsonfyMixIn(object): def to_json(self): return self.__dict__ + class SSH(JsonfyMixIn): manager = None @@ -121,6 +120,7 @@ def add(cls, name, key_pub): json = cls.manager.new_ssh_key(name, key_pub) return cls(json) + def core(module): def getkeyordie(k): v = module.params[k] @@ -135,7 +135,6 @@ def getkeyordie(k): except KeyError as e: module.fail_json(msg='Unable to load %s' % e.message) - changed = True state = module.params['state'] SSH.setup(client_id, api_key) @@ -154,6 +153,7 @@ def getkeyordie(k): key.destroy() module.exit_json(changed=True) + def main(): module = AnsibleModule( argument_spec = dict( @@ -173,12 +173,8 @@ def main(): try: core(module) - except TimeoutError as e: - module.fail_json(msg=str(e), id=e.id) except (DoError, Exception) as e: - module.fail_json(msg=str(e)) + module.fail_json(msg=str(e), exception=traceback.format_exc()) -# import module snippets -from ansible.module_utils.basic import * if __name__ == '__main__': main() diff --git a/cloud/digital_ocean/digital_ocean_tag.py b/cloud/digital_ocean/digital_ocean_tag.py index 2e06a1d5a0a..825b57db21d 100644 --- a/cloud/digital_ocean/digital_ocean_tag.py +++ b/cloud/digital_ocean/digital_ocean_tag.py @@ -105,6 +105,10 @@ ''' import json +import os + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url class Response(object): @@ -123,7 +127,7 @@ def json(self): return None try: return json.loads(self.body) - except ValueError as e: + except ValueError: return None @property @@ -250,8 +254,5 @@ def main(): except Exception as e: module.fail_json(msg=str(e)) -# import module snippets -from ansible.module_utils.basic import * # noqa -from ansible.module_utils.urls import * if __name__ == '__main__': main() From 7e19d375b32363d5d8788995f4ebb49028404ac4 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 5 Oct 2016 14:59:02 -0700 Subject: [PATCH 497/770] Emit an error message if six is not installed. dopy 0.3.7 makes use of six but doesn't list it as a requirement. This means that people installing with pip won't get six installed, leading to errors. Upstream released dopy-0.3.7a to address that but pip thinks that is an alpha release. pip does not install alpha releases by default so users aren't helped by that. This change makes ansible emit a good error message in this case. Fixes #4613 --- cloud/digital_ocean/digital_ocean.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/cloud/digital_ocean/digital_ocean.py b/cloud/digital_ocean/digital_ocean.py index e75e77e9bd1..a90f27f71e3 100644 --- a/cloud/digital_ocean/digital_ocean.py +++ b/cloud/digital_ocean/digital_ocean.py @@ -184,17 +184,22 @@ from distutils.version import LooseVersion -HAS_DOPY = True +try: + import six + HAS_SIX = True +except ImportError: + HAS_SIX = False + +HAS_DOPY = False try: import dopy from dopy.manager import DoError, DoManager - if LooseVersion(dopy.__version__) < LooseVersion('0.3.2'): - HAS_DOPY = False + if LooseVersion(dopy.__version__) >= LooseVersion('0.3.2'): + HAS_DOPY = True except ImportError: - HAS_DOPY = False + pass from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_native class TimeoutError(Exception): @@ -450,15 +455,17 @@ def main(): ['id', 'name'], ), ) + if not HAS_DOPY and not HAS_SIX: + module.fail_json(msg='dopy >= 0.3.2 is required for this module. dopy requires six but six is not installed. Make sure both dopy and six are installed.') if not HAS_DOPY: module.fail_json(msg='dopy >= 0.3.2 required for this module') try: core(module) except TimeoutError as e: - module.fail_json(msg=to_native(e), id=e.id) + module.fail_json(msg=str(e), id=e.id) except (DoError, Exception) as e: - module.fail_json(msg=to_native(e), exception=traceback.format_exc()) + module.fail_json(msg=str(e), exception=traceback.format_exc()) if __name__ == '__main__': main() From b4f6a25195d8d2420184347bc65e89d9da4f592a Mon Sep 17 00:00:00 2001 From: Elena Washington Date: Fri, 3 Jun 2016 09:47:31 -0400 Subject: [PATCH 498/770] Make is so that the params param truly isn't required (fix for #3860) --- cloud/amazon/rds_param_group.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/rds_param_group.py b/cloud/amazon/rds_param_group.py index 3ef82835aea..a3353cb33dc 100644 --- a/cloud/amazon/rds_param_group.py +++ b/cloud/amazon/rds_param_group.py @@ -234,7 +234,7 @@ def main(): immediate = module.params.get('immediate') or False if state == 'present': - for required in ['name', 'description', 'engine', 'params']: + for required in ['name', 'description', 'engine']: if not module.params.get(required): module.fail_json(msg = str("Parameter %s required for state='present'" % required)) else: From 149f10f8b7f0dfe669b3c5ef26c2f4107f589e61 Mon Sep 17 00:00:00 2001 From: Bill Nottingham Date: Thu, 6 Oct 2016 14:32:57 -0400 Subject: [PATCH 499/770] Fix deprecation notices. (#5180) --- network/ios/_ios_template.py | 2 +- network/iosxr/_iosxr_template.py | 2 +- network/junos/_junos_template.py | 2 +- network/nxos/_nxos_template.py | 2 +- network/openswitch/_ops_template.py | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/network/ios/_ios_template.py b/network/ios/_ios_template.py index 5791dfbf170..59076983515 100644 --- a/network/ios/_ios_template.py +++ b/network/ios/_ios_template.py @@ -28,7 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. -deprecated: Deprecated in 2.2. Use eos_config instead +deprecated: Deprecated in 2.2. Use ios_config instead extends_documentation_fragment: ios options: src: diff --git a/network/iosxr/_iosxr_template.py b/network/iosxr/_iosxr_template.py index 79ddaa42600..f39ca4d116c 100644 --- a/network/iosxr/_iosxr_template.py +++ b/network/iosxr/_iosxr_template.py @@ -28,7 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. -deprecated: Deprecated in 2.2. Use eos_config instead +deprecated: Deprecated in 2.2. Use iosxr_config instead extends_documentation_fragment: iosxr options: src: diff --git a/network/junos/_junos_template.py b/network/junos/_junos_template.py index c50deeef2c5..868c52001fd 100644 --- a/network/junos/_junos_template.py +++ b/network/junos/_junos_template.py @@ -27,7 +27,7 @@ from a template file onto a remote device running Junos. The module will return the differences in configuration if the diff option is specified on the Ansible command line -deprecated: Deprecated in 2.2. Use eos_config instead +deprecated: Deprecated in 2.2. Use junos_config instead extends_documentation_fragment: junos options: src: diff --git a/network/nxos/_nxos_template.py b/network/nxos/_nxos_template.py index d8c8caf974b..371b93b0077 100644 --- a/network/nxos/_nxos_template.py +++ b/network/nxos/_nxos_template.py @@ -28,7 +28,7 @@ by evaluating the current running-config and only pushing configuration commands that are not already configured. The config source can be a set of commands or a template. -deprecated: Deprecated in 2.2. Use eos_config instead +deprecated: Deprecated in 2.2. Use nxos_config instead extends_documentation_fragment: nxos options: src: diff --git a/network/openswitch/_ops_template.py b/network/openswitch/_ops_template.py index 2fb21c263ac..5bb77387aa3 100644 --- a/network/openswitch/_ops_template.py +++ b/network/openswitch/_ops_template.py @@ -28,7 +28,7 @@ against a provided candidate configuration. If there are changes, the candidate configuration is merged with the current configuration and pushed into OpenSwitch -deprecated: Deprecated in 2.2. Use eos_config instead +deprecated: Deprecated in 2.2. Use ops_config instead extends_documentation_fragment: openswitch options: src: From 32d7d31105f020bbf514298b7a8a0afa9ff74077 Mon Sep 17 00:00:00 2001 From: adejongh Date: Fri, 7 Oct 2016 22:14:56 +0200 Subject: [PATCH 500/770] Fixed incorrect usage of user_data variable (#5194) --- cloud/rackspace/rax.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/rackspace/rax.py b/cloud/rackspace/rax.py index 64105d32ed0..9ac0318c105 100644 --- a/cloud/rackspace/rax.py +++ b/cloud/rackspace/rax.py @@ -182,7 +182,7 @@ description: - how long before wait gives up, in seconds default: 300 -author: +author: - "Jesse Keating (@j2sol)" - "Matt Martz (@sivel)" notes: @@ -287,7 +287,7 @@ def create(module, names=[], flavor=None, image=None, meta={}, key_name=None, if user_data and os.path.isfile(os.path.expanduser(user_data)): try: - user_data = os.path.expanduser('user_data') + user_data = os.path.expanduser(user_data) f = open(user_data) user_data = f.read() f.close() From b4763c297bc349ace0905f9e931b3df8d706e4e5 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 7 Oct 2016 23:11:59 -0400 Subject: [PATCH 501/770] updates docstring for sros modules (#5197) --- network/sros/sros_command.py | 8 ++++---- network/sros/sros_config.py | 6 +++--- network/sros/sros_rollback.py | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/network/sros/sros_command.py b/network/sros/sros_command.py index e2e3acd6ccd..e8b6a654fc1 100644 --- a/network/sros/sros_command.py +++ b/network/sros/sros_command.py @@ -21,19 +21,19 @@ module: sros_command version_added: "2.2" author: "Peter Sprygada (@privateip)" -short_description: Run commands on remote devices running Nokia SROS +short_description: Run commands on remote devices running Nokia SR OS description: - - Sends arbitrary commands to an SROS node and returns the results + - Sends arbitrary commands to an SR OS node and returns the results read from the device. This module includes an argument that will cause the module to wait for a specific condition before returning or timing out if the condition is not met. - This module does not support running commands in configuration mode. - Please use M(sros_config) to configure SROS devices. + Please use M(sros_config) to configure SR OS devices. extends_documentation_fragment: sros options: commands: description: - - List of commands to send to the remote SROS device over the + - List of commands to send to the remote SR OS device over the configured provider. The resulting output from the command is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or diff --git a/network/sros/sros_config.py b/network/sros/sros_config.py index 186c7215e42..f1fd84a8bb2 100644 --- a/network/sros/sros_config.py +++ b/network/sros/sros_config.py @@ -21,11 +21,11 @@ module: sros_config version_added: "2.2" author: "Peter Sprygada (@privateip)" -short_description: Manage Nokia SROS device configuration +short_description: Manage Nokia SR OS device configuration description: - - Nokia SROS configurations use a simple block indent file syntax + - Nokia SR OS configurations use a simple block indent file syntax for segmenting configuration into sections. This module provides - an implementation for working with SROS configuration sections in + an implementation for working with SR OS configuration sections in a deterministic way. extends_documentation_fragment: sros options: diff --git a/network/sros/sros_rollback.py b/network/sros/sros_rollback.py index d04437fb32f..8d9514c4039 100644 --- a/network/sros/sros_rollback.py +++ b/network/sros/sros_rollback.py @@ -21,10 +21,10 @@ module: sros_rollback version_added: "2.2" author: "Peter Sprygada (@privateip)" -short_description: Configure Nokia SROS rollback +short_description: Configure Nokia SR OS rollback description: - Configure the rollback feature on remote Nokia devices running - the SROS operating system. this module provides a stateful + the SR OS operating system. this module provides a stateful implementation for managing the configuration of the rollback feature extends_documentation_fragment: sros @@ -33,7 +33,7 @@ description: - The I(rollback_location) specifies the location and filename of the rollback checkpoint files. This argument supports any - valid local or remote URL as specified in SROS + valid local or remote URL as specified in SR OS required: false default: null remote_max_checkpoints: @@ -56,7 +56,7 @@ description: - The I(rescue_location) specifies the location of the rescue file. This argument supports any valid local - or remote URL as specified in SROS + or remote URL as specified in SR OS required: false default: null state: From 745b1857d675ecbf7e7ada6a6e42350ca5669e61 Mon Sep 17 00:00:00 2001 From: jjshoe Date: Sat, 8 Oct 2016 07:09:44 -0500 Subject: [PATCH 502/770] Catch the rare condition where ami creation failed, this is critical when you have a 10-15 minute wait on ami creation. This rarely happens, and is tough to reproduce, but it does happen. (#5106) --- cloud/amazon/ec2_ami.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_ami.py b/cloud/amazon/ec2_ami.py index 1c1bd8354b1..684a51d31d9 100644 --- a/cloud/amazon/ec2_ami.py +++ b/cloud/amazon/ec2_ami.py @@ -395,6 +395,8 @@ def create_image(module, ec2): if img.state == 'available': break + elif img.state == 'failed': + module.fail_json(msg="AMI creation failed, please see the AWS console for more details") except boto.exception.EC2ResponseError as e: if ('InvalidAMIID.NotFound' not in e.error_code and 'InvalidAMIID.Unavailable' not in e.error_code) and wait and i == wait_timeout - 1: module.fail_json(msg="Error while trying to find the new image. Using wait=yes and/or a longer wait_timeout may help. %s: %s" % (e.error_code, e.error_message)) From f5c204005f5463f91eac0e8a800a453fd027f461 Mon Sep 17 00:00:00 2001 From: Nathaniel Case Date: Sat, 8 Oct 2016 15:29:24 -0400 Subject: [PATCH 503/770] nxos module cleanup (#5065) * Fix imports on nxos_bgp* modules * Fix imports on nxos_evpn* modules * Cleanup issues for nxos_facts * Shuffle imports for nxos_template * Fix imports on nxos_ospf* modules * Fix nxos_hsrp As get_hsrp_groups_in_devices is not actually called anywhere, I presume this change is reasonable. * Fix imports on nxos_interface* modules * Update nxos_static_route imports * update nxos_vrf * Update nxos_config imports --- network/nxos/_nxos_template.py | 3 ++- network/nxos/nxos_bgp.py | 8 +++----- network/nxos/nxos_bgp_af.py | 7 +++---- network/nxos/nxos_bgp_neighbor.py | 8 +++----- network/nxos/nxos_bgp_neighbor_af.py | 8 +++----- network/nxos/nxos_config.py | 6 +++--- network/nxos/nxos_evpn_global.py | 8 +++----- network/nxos/nxos_evpn_vni.py | 7 ++----- network/nxos/nxos_facts.py | 11 ++++++++--- network/nxos/nxos_hsrp.py | 11 +++-------- network/nxos/nxos_interface.py | 11 +++-------- network/nxos/nxos_interface_ospf.py | 7 ++----- network/nxos/nxos_ospf.py | 8 +++----- network/nxos/nxos_ospf_vrf.py | 8 +++----- network/nxos/nxos_static_route.py | 13 +++++-------- network/nxos/nxos_vrf.py | 17 ++++++----------- 16 files changed, 55 insertions(+), 86 deletions(-) diff --git a/network/nxos/_nxos_template.py b/network/nxos/_nxos_template.py index 371b93b0077..fe379629394 100644 --- a/network/nxos/_nxos_template.py +++ b/network/nxos/_nxos_template.py @@ -109,8 +109,9 @@ type: list sample: ['...', '...'] """ +import ansible.module_utils.nxos from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.nxos import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule def get_config(module): config = module.params['config'] or dict() diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 594e48aa9d3..9f7dd16a537 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -360,13 +360,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.shell import ShellError def to_list(val): diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 728515f70d0..38e9ba0350d 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -299,13 +299,12 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.network import NetworkModule +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): diff --git a/network/nxos/nxos_bgp_neighbor.py b/network/nxos/nxos_bgp_neighbor.py index 56a6c2923ff..a9080ae2f28 100644 --- a/network/nxos/nxos_bgp_neighbor.py +++ b/network/nxos/nxos_bgp_neighbor.py @@ -244,13 +244,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.shell import ShellError def to_list(val): diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py index b6406674f3c..06ced09e7ce 100644 --- a/network/nxos/nxos_bgp_neighbor_af.py +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -321,13 +321,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.shell import ShellError def to_list(val): diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index bd15e5d8d2f..19b15d06ef9 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -208,11 +208,11 @@ type: path sample: /playbooks/ansible/backup/nxos_config.2016-07-16@22:28:34 """ -import time -from ansible.module_utils.netcfg import NetworkConfig, dumps -from ansible.module_utils.nxos import NetworkModule, NetworkError +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.netcfg import NetworkConfig, dumps def check_args(module, warnings): if module.params['force']: diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index c2bbe4fdce6..650a19370e1 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -72,13 +72,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.shell import ShellError def to_list(val): diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py index 4ffcfeefcdb..a2de61d0620 100644 --- a/network/nxos/nxos_evpn_vni.py +++ b/network/nxos/nxos_evpn_vni.py @@ -126,14 +126,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos 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 +from ansible.module_utils.network import NetworkModule def to_list(val): diff --git a/network/nxos/nxos_facts.py b/network/nxos/nxos_facts.py index ff9c454aaa6..9d5defb9a8c 100644 --- a/network/nxos/nxos_facts.py +++ b/network/nxos/nxos_facts.py @@ -172,9 +172,11 @@ """ import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcli import CommandRunner, AddCommandError -from ansible.module_utils.nxos import NetworkModule, NetworkError +from ansible.module_utils.network import NetworkModule, NetworkError +from ansible.module_utils.six import iteritems def add_command(runner, command, output=None): @@ -193,6 +195,9 @@ def __init__(self, module, runner): self.facts = dict() self.commands() + def commands(self): + raise NotImplementedError + def transform_dict(self, data, keymap): transform = dict() for key, fact in keymap: @@ -255,7 +260,7 @@ class Interfaces(FactsBase): ('state', 'state'), ('desc', 'description'), ('eth_bw', 'bandwidth'), - ('eth_duplex','duplex'), + ('eth_duplex', 'duplex'), ('eth_speed', 'speed'), ('eth_mode', 'mode'), ('eth_hw_addr', 'macaddress'), @@ -511,7 +516,7 @@ def main(): module.exit_json(out=module.from_json(runner.items)) ansible_facts = dict() - for key, value in facts.iteritems(): + for key, value in iteritems(facts): # this is to maintain capability with nxos_facts 2.1 if key.startswith('_'): ansible_facts[key[1:]] = value diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index b218d295910..d21fcaa6dd1 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -118,20 +118,15 @@ sample: true ''' -import collections import json # COMMON CODE FOR MIGRATION -import re +import ansible.module_utils.nxos 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 +from ansible.module_utils.network import NetworkModule def to_list(val): @@ -406,7 +401,7 @@ def get_interface_mode(interface, intf_type, module): return mode -def get_hsrp_groups_on_interfaces(device): +def get_hsrp_groups_on_interfaces(device, module): command = 'show hsrp all' body = execute_show_command(command, module) hsrp = {} diff --git a/network/nxos/nxos_interface.py b/network/nxos/nxos_interface.py index 927e93d3a88..7c1186ee924 100644 --- a/network/nxos/nxos_interface.py +++ b/network/nxos/nxos_interface.py @@ -135,20 +135,15 @@ ''' import json -import collections # COMMON CODE FOR MIGRATION -import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.network import NetworkModule 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)): @@ -308,7 +303,7 @@ def is_default_interface(interface, module): body = execute_show_command(command, module, command_type='cli_show_ascii')[0] except IndexError: - body = [] + body = '' if body: raw_list = body.split('\n') diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py index 34ec566cf4d..5cf050e85cc 100644 --- a/network/nxos/nxos_interface_ospf.py +++ b/network/nxos/nxos_interface_ospf.py @@ -166,15 +166,12 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine +from ansible.module_utils.network import NetworkModule 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)): diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index ea566da000f..5942785476a 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -80,13 +80,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.shell import ShellError def to_list(val): diff --git a/network/nxos/nxos_ospf_vrf.py b/network/nxos/nxos_ospf_vrf.py index 32fd7cfcba8..8b4d330c995 100644 --- a/network/nxos/nxos_ospf_vrf.py +++ b/network/nxos/nxos_ospf_vrf.py @@ -171,13 +171,11 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.network import NetworkModule +from ansible.module_utils.shell import ShellError def to_list(val): diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index eb0555067f1..10a1f849ada 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -111,13 +111,10 @@ # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos from ansible.module_utils.basic import get_exception -from ansible.module_utils.netcfg import NetworkConfig, ConfigLine - -try: - from ansible.module_utils.nxos import get_module -except ImportError: - from ansible.module_utils.nxos import NetworkModule +from ansible.module_utils.netcfg import NetworkConfig, ConfigLine, dumps +from ansible.module_utils.network import NetworkModule def to_list(val): @@ -155,7 +152,7 @@ def get_section(self, path): try: section = self.get_section_objects(path) if self._device_os == 'junos': - return self.to_lines(section) + return dumps(section, output='lines') return self.to_block(section) except ValueError: return list() @@ -398,10 +395,10 @@ def network_from_string(address, mask, module): def normalize_prefix(module, prefix): splitted_prefix = prefix.split('/') + address = splitted_prefix[0] if len(splitted_prefix) > 2: module.fail_json(msg='Incorrect address format.', address=address) elif len(splitted_prefix) == 2: - address = splitted_prefix[0] mask = splitted_prefix[1] network = network_from_string(address, mask, module) diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index 40e11a70b9f..8c8c2c17925 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -111,19 +111,15 @@ ''' import json -import collections # COMMON CODE FOR MIGRATION import re +import ansible.module_utils.nxos 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 +from ansible.module_utils.network import NetworkModule def to_list(val): @@ -385,11 +381,10 @@ def get_commands_to_config_vrf(delta, vrf): def get_vrf_description(vrf, module): command_type = 'cli_show_ascii' - command = ('show run section vrf | begin ^vrf\scontext\s{0} ' - '| end ^vrf.*'.format(vrf)) + command = (r'show run section vrf | begin ^vrf\scontext\s{0} | end ^vrf.*'.format(vrf)) description = '' - descr_regex = ".*description\s(?P[\S+\s]+).*" + descr_regex = r".*description\s(?P[\S+\s]+).*" body = execute_show_command(command, module, command_type) try: @@ -507,7 +502,7 @@ def main(): if existing.get('vni') and existing.get('vni') != '': commands.insert(1, 'no vni {0}'.format(existing['vni'])) if module.check_mode: - module.exit_json(changed=True, commands=cmds) + module.exit_json(changed=True, commands=commands) else: execute_config_command(commands, module) changed = True @@ -526,4 +521,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() From e6c039872f553c6cca9e6fc77716f2dda3670215 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 10 Oct 2016 12:37:21 -0400 Subject: [PATCH 504/770] fixes bug with junos_config module not properly loading config (#5213) This fixes two issues. First, it fixes an issue with the junos_config module not properly recognizing a file with set commands. The second bug would cause the diff_config() function to raise an exception due to a blank line when splitting the config --- network/junos/junos_config.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 80cfc39a4f9..5e2ead6a1d2 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -214,18 +214,19 @@ def diff_commands(commands, config): updates = list() visited = set() - for item in commands: - if not item.startswith('set') and not item.startswith('delete'): - raise ValueError('line must start with either `set` or `delete`') - - elif item.startswith('set') and item[4:] not in config: - updates.append(item) - - elif item.startswith('delete'): - for entry in config: - if entry.startswith(item[7:]) and item not in visited: - updates.append(item) - visited.add(item) + for item in commands.split('\n'): + if len(item) > 0: + if not item.startswith('set') and not item.startswith('delete'): + raise ValueError('line must start with either `set` or `delete`') + + elif item.startswith('set') and item[4:] not in config: + updates.append(item) + + elif item.startswith('delete'): + for entry in config: + if entry.startswith(item[7:]) and item not in visited: + updates.append(item) + visited.add(item) return updates From 275fa3f055aacf2286eed54c13bde17db5082035 Mon Sep 17 00:00:00 2001 From: John Barker Date: Sun, 9 Oct 2016 18:58:06 +0100 Subject: [PATCH 505/770] Correct functional typos --- cloud/amazon/ec2_elb.py | 2 +- cloud/azure/azure_rm_virtualmachine.py | 2 +- cloud/docker/docker_container.py | 2 +- network/nxos/nxos_acl.py | 2 +- network/nxos/nxos_bgp_af.py | 2 +- network/sros/sros_rollback.py | 2 +- packaging/os/yum.py | 2 +- 7 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/ec2_elb.py b/cloud/amazon/ec2_elb.py index 7e741117745..2a128017c07 100644 --- a/cloud/amazon/ec2_elb.py +++ b/cloud/amazon/ec2_elb.py @@ -327,7 +327,7 @@ def main(): ec2_elbs={'default': None, 'required': False, 'type':'list'}, enable_availability_zone={'default': True, 'required': False, 'type': 'bool'}, wait={'required': False, 'default': True, 'type': 'bool'}, - wait_timeout={'requred': False, 'default': 0, 'type': 'int'} + wait_timeout={'required': False, 'default': 0, 'type': 'int'} ) ) diff --git a/cloud/azure/azure_rm_virtualmachine.py b/cloud/azure/azure_rm_virtualmachine.py index 868e4ed7a2c..ef6fbdfa46a 100644 --- a/cloud/azure/azure_rm_virtualmachine.py +++ b/cloud/azure/azure_rm_virtualmachine.py @@ -573,7 +573,7 @@ def exec_module(self, **kwargs): # Verify parameters and resolve any defaults if self.vm_size and not self.vm_size_is_valid(): - self.fail("Parameter error: vm_size {0} is not valid for your subscription and location.".foramt( + self.fail("Parameter error: vm_size {0} is not valid for your subscription and location.".format( self.vm_size )) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index 6af74a617c4..3a0aedd5171 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -1965,7 +1965,7 @@ def main(): privileged=dict(type='bool', default=False), published_ports=dict(type='list', aliases=['ports']), pull=dict(type='bool', default=False), - purge_networks=dict(type='bool', deault=False), + purge_networks=dict(type='bool', default=False), read_only=dict(type='bool', default=False), recreate=dict(type='bool', default=False), restart=dict(type='bool', default=False), diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index b0e3223eb64..eff27bb509c 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -640,7 +640,7 @@ def main(): seq=dict(required=False, type='str'), name=dict(required=True, type='str'), action=dict(required=False, choices=['remark', 'permit', 'deny']), - remark=dict(requried=False, type='str'), + remark=dict(required=False, type='str'), proto=dict(required=False, type='str'), src=dict(required=False, type='str'), src_port_op=dict(required=False), diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 38e9ba0350d..0eb920f3c72 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -958,7 +958,7 @@ def main(): additional_paths_install=dict(required=False, type='bool'), additional_paths_receive=dict(required=False, type='bool'), additional_paths_selection=dict(required=False, type='str'), - additional_paths_send=dict(required=False, ype='bool'), + additional_paths_send=dict(required=False, type='bool'), advertise_l2vpn_evpn=dict(required=False, type='bool'), client_to_client=dict(required=False, type='bool'), dampen_igp_metric=dict(required=False, type='str'), diff --git a/network/sros/sros_rollback.py b/network/sros/sros_rollback.py index 8d9514c4039..f952b092124 100644 --- a/network/sros/sros_rollback.py +++ b/network/sros/sros_rollback.py @@ -188,7 +188,7 @@ def main(): rollback_location=dict(), local_max_checkpoints=dict(type='int'), - remote_max_checkpionts=dict(type='int'), + remote_max_checkpoints=dict(type='int'), rescue_location=dict(), diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 45bfd2d5775..6a9de912c1e 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -986,7 +986,7 @@ def ensure(module, state, pkgs, conf_file, enablerepo, disablerepo, else: # should be caught by AnsibleModule argument_spec module.fail_json(msg="we should never get here unless this all" - " failed", changed=False, results='', errors='unepected state') + " failed", changed=False, results='', errors='unexpected state') return res From 9c25a7f97ebd77953def9e47fa79031997bc0ebe Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 12 Oct 2016 20:16:34 -0400 Subject: [PATCH 506/770] fixes issue with pushing config to versions that do not support sessions (#5236) earlier versions of eos do not support configuration sessions. this change will now check if sessions are supported and if not will fallback to not using config sessions fixes #4909 --- network/eos/eos_config.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index 6531a602d90..04d0c9f0fba 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -223,6 +223,11 @@ def check_args(module, warnings): 'match=none instead. This argument will be ' 'removed in the future') + if not module.connection.supports_sessions(): + warnings.append('The current version of EOS on the remote device does ' + 'not support configuration sessions. The commit ' + 'argument will be ignored') + def get_candidate(module): candidate = NetworkConfig(indent=3) if module.params['src']: @@ -245,9 +250,11 @@ def load_config(module, commands, result): diff = module.config.load_config(commands, replace=replace, commit=commit) - if diff: + if diff and module.connection.supports_sessions(): result['diff'] = dict(prepared=diff) result['changed'] = True + elif diff: + result['changed'] = True def run(module, result): match = module.params['match'] @@ -273,6 +280,7 @@ def run(module, result): result['updates'] = commands + module.log('commands: %s' % commands) load_config(module, commands, result) if module.params['save']: @@ -314,7 +322,6 @@ def main(): ('replace', 'config', ['src'])] module = NetworkModule(argument_spec=argument_spec, - connect_on_load=False, mutually_exclusive=mutually_exclusive, required_if=required_if, supports_check_mode=True) From d8d9ab8f6d17967fdd6519d28765bfd5c9381c57 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 12 Oct 2016 21:00:27 -0400 Subject: [PATCH 507/770] removes automated backup of ios to flash due to errors (#5245) The feature is extremely unstable right now and decision to pull it out for 2.2. Workaround is to do the same in the playbook --- network/ios/ios_config.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 7ca12716d42..cff8ebd4401 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -230,17 +230,6 @@ def get_candidate(module): candidate.add(module.params['lines'], parents=parents) return candidate -def load_backup(module): - try: - module.cli(['exit', 'config replace flash:/ansible-rollback force']) - except NetworkError: - module.fail_json(msg='unable to load backup configuration') - -def backup_config(module): - cmd = 'copy running-config flash:/ansible-rollback' - cmd = Command(cmd, prompt=re.compile('\? $'), response='\n') - module.cli(cmd) - def run(module, result): match = module.params['match'] replace = module.params['replace'] @@ -268,20 +257,12 @@ def run(module, result): result['updates'] = commands - # create a backup copy of the current running-config on - # device flash drive - backup_config(module) - # send the configuration commands to the device and merge # them with the current running config if not module.check_mode: module.config(commands) result['changed'] = True - # remove the backup copy of the running-config since its - # no longer needed - module.cli('delete /force flash:/ansible-rollback') - if module.params['save']: if not module.check_mode: module.config.save_config() @@ -340,7 +321,6 @@ def main(): try: run(module, result) except NetworkError: - load_backup(module) exc = get_exception() module.fail_json(msg=str(exc)) From 0ffd423b07b3cd5f55dacb4a4d4d831e9f5fb9ed Mon Sep 17 00:00:00 2001 From: amitsi Date: Wed, 12 Oct 2016 18:18:46 -0700 Subject: [PATCH 508/770] Update pn_vlan (#5223) removed name from an older file which got left out --- network/netvisor/pn_vlan.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/network/netvisor/pn_vlan.py b/network/netvisor/pn_vlan.py index 05438aa3eda..cf94c3bc753 100644 --- a/network/netvisor/pn_vlan.py +++ b/network/netvisor/pn_vlan.py @@ -222,8 +222,6 @@ def get_command_from_state(state): def main(): """ This section is for arguments parsing """ - arguement_spec = pn_arguement_spec - module = AnsibleModule( argument_spec=dict( pn_cliusername=dict(required=False, type='str'), From 528ced6d12c143ac1c7db374711719efc9f37321 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 12 Oct 2016 21:37:08 -0400 Subject: [PATCH 509/770] ios_config will now explicitly disconnect from remote host (#5247) The ios_config module will now explicitly send a disconnect to the remote host at the conclusion of the module run ref #5181 --- network/ios/ios_config.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index cff8ebd4401..1022419a8ab 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -322,8 +322,10 @@ def main(): run(module, result) except NetworkError: exc = get_exception() + module.disconnect() module.fail_json(msg=str(exc)) + module.disconnect() module.exit_json(**result) From 04b214852d13b24207c02d9c58b658d71bec0cfb Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 13 Oct 2016 08:21:33 -0400 Subject: [PATCH 510/770] fixes issue with collecting all filesystems in ios (#5248) earlier versions of ios do not provide the all-filesystems argument. This fix will now only report on the flash filesystem for ios_facts fixes #4712 --- network/ios/ios_facts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py index 699637d9c02..c1516cec4e9 100644 --- a/network/ios/ios_facts.py +++ b/network/ios/ios_facts.py @@ -195,12 +195,12 @@ def parse_serialnum(self, data): class Hardware(FactsBase): def commands(self): - add_command(self.runner, 'dir all-filesystems | include Directory') + add_command(self.runner, 'dir | include Directory') add_command(self.runner, 'show version') add_command(self.runner, 'show memory statistics | include Processor') def populate(self): - data = self.runner.get_command('dir all-filesystems | include Directory') + data = self.runner.get_command('dir | include Directory') self.facts['filesystems'] = self.parse_filesystems(data) data = self.runner.get_command('show memory statistics | include Processor') From 454835622bafc5d44926ab32716b440446f1c7ba Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Thu, 13 Oct 2016 05:28:02 -0700 Subject: [PATCH 511/770] GCE module examples update. Correct syntax, demonstrate other options and creation of multiple instances. (#5192) --- cloud/google/gce.py | 180 +++++++++++++++++++++++--------------------- 1 file changed, 96 insertions(+), 84 deletions(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index 96f058df6d1..10e0153f3c7 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -23,7 +23,7 @@ short_description: create or terminate GCE instances description: - Creates or terminates Google Compute Engine (GCE) instances. See - U(https://cloud.google.com/products/compute-engine) for an overview. + U(https://cloud.google.com/compute) for an overview. Full install/configuration instructions for the gce* modules can be found in the comments of ansible/test/gce_tests.py. options: @@ -89,7 +89,8 @@ default: null name: description: - - identifier when working with a single instance + - identifier when working with a single instance. Will be deprecated in a future release. + Please 'instance_names' instead. required: false network: description: @@ -164,96 +165,107 @@ - "apache-libcloud >= 0.13.3, >= 0.17.0 if using JSON credentials, >= 0.20.0 if using preemptible option" notes: - - Either I(name) or I(instance_names) is required. -author: "Eric Johnson (@erjohnso) " + - Either I(instance_names) or I(name) is required. + - JSON credentials strongly preferred. +author: "Eric Johnson (@erjohnso) , Tom Melendez (@supertom) " ''' EXAMPLES = ''' -# Basic provisioning example. Create a single Debian 7 instance in the -# us-central1-a Zone of n1-standard-1 machine type. -- local_action: - module: gce - name: test-instance - zone: us-central1-a - machine_type: n1-standard-1 - image: debian-7 - -# Example using defaults and with metadata to create a single 'foo' instance -- local_action: - module: gce - name: foo - metadata: '{"db":"postgres", "group":"qa", "id":500}' - - -# Launch instances from a control node, runs some tasks on the new instances, -# and then terminate them -# This example uses JSON credentials with the credentials_file parameter -# rather than the deprecated pem_file option with PEM formatted credentials. - -- name: Create a sandbox instance +# Basic provisioning example. Create a single Debian 8 instance in the +# us-central1-a Zone of the n1-standard-1 machine type. +# Create multiple instances by specifying multiple names, seperated by +# commas in the instance_names field +# (e.g. my-test-instance1,my-test-instance2) + gce: + instance_names: my-test-instance1 + zone: us-central1-a + machine_type: n1-standard-1 + image: debian-8 + state: present + service_account_email: "your-sa@your-project-name.iam.gserviceaccount.com" + credentials_file: "/path/to/your-key.json" + project_id: "your-project-name" + +# Create a single Debian 8 instance in the us-central1-a Zone +# Use existing disks, custom network/subnetwork, set service account permissions +# add tags and metadata. + gce: + instance_names: my-test-instance + zone: us-central1-a + machine_type: n1-standard-1 + state: present + metadata: '{"db":"postgres", "group":"qa", "id":500}' + tags: + - http-server + - my-other-tag + disks: + - { 'name' : 'disk-2', 'mode': 'READ_WRITE' } + - { 'name' : 'disk-3', 'mode': 'READ_ONLY' } + disk_auto_delete: false + network: foobar-network + subnetwork: foobar-subnetwork-1 + preemptible: true + ip_forward: true + service_account_permissions: + - storage-full + - taskqueue + - bigquery + service_account_email: "your-sa@your-project-name.iam.gserviceaccount.com" + credentials_file: "/path/to/your-key.json" + project_id: "your-project-name" + +# Example Playbook +- name: Compute Engine Instance Examples hosts: localhost vars: - names: foo,bar - machine_type: n1-standard-1 - image: debian-6 - zone: us-central1-a - service_account_email: unique-email@developer.gserviceaccount.com - credentials_file: /path/to/json_file - project_id: project-id + service_account_email: "your-sa@your-project-name.iam.gserviceaccount.com" + credentials_file: "/path/to/your-key.json" + project_id: "your-project-name" tasks: - - name: Launch instances - local_action: gce instance_names={{names}} machine_type={{machine_type}} - image={{image}} zone={{zone}} - service_account_email={{ service_account_email }} - credentials_file={{ credentials_file }} - project_id={{ project_id }} + - name: create multiple instances + # Basic provisioning example. Create multiple Debian 8 instances in the + # us-central1-a Zone of n1-standard-1 machine type. + gce: + instance_names: test1,test2,test3 + zone: us-central1-a + machine_type: n1-standard-1 + image: debian-8 + state: present + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + metadata : '{ "startup-script" : "apt-get update" }' register: gce - - name: Wait for SSH to come up - local_action: wait_for host={{item.public_ip}} port=22 delay=10 - timeout=60 state=started - with_items: {{gce.instance_data}} - -- name: Configure instance(s) - hosts: launched - become: True - roles: - - my_awesome_role - - my_awesome_tasks - -- name: Terminate instances - hosts: localhost - connection: local - tasks: - - name: Terminate instances that were previously launched - local_action: - module: gce - state: 'absent' - instance_names: {{gce.instance_names}} - -# The deprecated PEM file credentials can be used as follows -- name: Create a sandbox instance with PEM credentials - hosts: localhost - vars: - names: foo,bar - machine_type: n1-standard-1 - image: debian-6 - zone: us-central1-a - service_account_email: unique-email@developer.gserviceaccount.com - pem_file: /path/to/pem_file - project_id: project-id - tasks: - - name: Launch instances - local_action: gce instance_names={{names}} machine_type={{machine_type}} - image={{image}} zone={{zone}} - service_account_email={{ service_account_email }} - pem_file={{ pem_file }} - project_id={{ project_id }} - register: gce - - name: Wait for SSH to come up - local_action: wait_for host={{item.public_ip}} port=22 delay=10 - timeout=60 state=started - with_items: {{gce.instance_data}} + - name: Save host data + add_host: hostname={{ item.public_ip }} groupname=gce_instances_ips + with_items: "{{ gce.instance_data }}" + + - name: Wait for SSH for instances + wait_for: delay=1 host={{ item.public_ip }} port=22 state=started timeout=30 + with_items: "{{ gce.instance_data }}" + + - name: Configure Hosts + hosts: gce_instances_ips + become: yes + become_method: sudo + roles: + - my-role-one + - my-role-two + tags: + - config + + - name: delete test-instances + # Basic termination of instance. + gce: + service_account_email: "{{ service_account_email }}" + credentials_file: "{{ credentials_file }}" + project_id: "{{ project_id }}" + instance_names: "{{ gce.instance_names }}" + zone: us-central1-a + state: absent + tags: + - delete ''' import socket From c6d8cb6cab9fdfd9cd382048bbf419dfbc2dc164 Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Thu, 13 Oct 2016 05:33:13 -0700 Subject: [PATCH 512/770] Added libcloud guard for Managed Instance Groups. (#4911) --- cloud/google/gce_mig.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/cloud/google/gce_mig.py b/cloud/google/gce_mig.py index 7762eaed23f..bb44be1f25b 100644 --- a/cloud/google/gce_mig.py +++ b/cloud/google/gce_mig.py @@ -596,7 +596,6 @@ def get_mig(gce, name, zone): def main(): - module = AnsibleModule(argument_spec=dict( name=dict(required=True), template=dict(), @@ -619,7 +618,13 @@ def main(): msg="GCE module requires python's 'ast' module, python v2.6+") if not HAS_LIBCLOUD: module.fail_json( - msg='libcloud with GCE Managed Instance Group support (1.1+) required for this module.') + msg='libcloud with GCE Managed Instance Group support (1.2+) required for this module.') + + gce = gce_connect(module) + if not hasattr(gce, 'ex_create_instancegroupmanager'): + module.fail_json( + msg='libcloud with GCE Managed Instance Group support (1.2+) required for this module.', + changed=False) params = {} params['state'] = module.params.get('state') @@ -634,7 +639,6 @@ def main(): if not valid_autoscaling: module.fail_json(msg=as_msg, changed=False) - gce = gce_connect(module) changed = False json_output = {'state': params['state'], 'zone': params['zone']} mig = get_mig(gce, params['name'], params['zone']) From 312f578f93a13d21b6d5ab45c1dd0bdf8685ef54 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 13 Oct 2016 15:47:50 +0100 Subject: [PATCH 513/770] Bulk spelling improvement to modules-core (#5225) * Correct spelling mistakes * Correct more spelling issues * merge conflict * Revert typo in parms --- cloud/amazon/ec2.py | 4 ++-- cloud/amazon/ec2_ami.py | 2 +- cloud/amazon/ec2_ami_find.py | 2 +- cloud/amazon/ec2_asg.py | 10 +++++----- cloud/amazon/ec2_elb.py | 4 ++-- cloud/amazon/ec2_elb_lb.py | 4 ++-- cloud/amazon/ec2_vpc.py | 4 ++-- cloud/amazon/ec2_vpc_net.py | 2 +- cloud/amazon/iam.py | 8 ++++---- cloud/amazon/rds.py | 2 +- cloud/azure/azure.py | 4 ++-- cloud/azure/azure_rm_storageblob.py | 2 +- cloud/azure/azure_rm_virtualmachine.py | 6 +++--- cloud/docker/docker_container.py | 10 +++++----- cloud/docker/docker_network.py | 2 +- cloud/docker/docker_service.py | 2 +- cloud/openstack/_keystone_user.py | 2 +- cloud/openstack/_quantum_network.py | 2 +- cloud/openstack/_quantum_router_interface.py | 4 ++-- cloud/openstack/os_floating_ip.py | 2 +- cloud/openstack/os_ironic.py | 2 +- cloud/openstack/os_subnet.py | 2 +- cloud/vmware/vsphere_guest.py | 2 +- database/mysql/mysql_db.py | 2 +- database/mysql/mysql_user.py | 2 +- files/acl.py | 4 ++-- files/find.py | 2 +- files/ini_file.py | 2 +- files/stat.py | 2 +- files/synchronize.py | 4 ++-- network/basics/get_url.py | 4 ++-- network/basics/uri.py | 4 ++-- network/cumulus/cl_bond.py | 2 +- network/cumulus/cl_bridge.py | 2 +- network/cumulus/cl_interface.py | 2 +- network/cumulus/cl_interface_policy.py | 2 +- network/dellos10/dellos10_config.py | 2 +- network/dellos10/dellos10_facts.py | 4 ++-- network/dellos6/dellos6_command.py | 6 +++--- network/dellos6/dellos6_facts.py | 4 ++-- network/dellos9/dellos9_facts.py | 2 +- network/eos/_eos_template.py | 4 ++-- network/eos/eos_command.py | 6 +++--- network/eos/eos_facts.py | 2 +- network/ios/_ios_template.py | 2 +- network/ios/ios_facts.py | 4 ++-- network/iosxr/_iosxr_template.py | 4 ++-- network/iosxr/iosxr_facts.py | 2 +- network/junos/junos_command.py | 2 +- network/junos/junos_facts.py | 4 ++-- network/netvisor/pn_vrouter.py | 2 +- network/netvisor/pn_vrouterif.py | 2 +- network/nxos/_nxos_template.py | 4 ++-- network/nxos/nxos_acl.py | 2 +- network/nxos/nxos_bgp.py | 2 +- network/nxos/nxos_bgp_neighbor_af.py | 2 +- network/nxos/nxos_command.py | 6 +++--- network/nxos/nxos_evpn_vni.py | 2 +- network/nxos/nxos_facts.py | 2 +- network/nxos/nxos_file_copy.py | 2 +- network/nxos/nxos_gir_profile_management.py | 4 ++-- network/nxos/nxos_install_os.py | 6 +++--- network/nxos/nxos_ntp_auth.py | 6 +++--- network/nxos/nxos_nxapi.py | 2 +- network/nxos/nxos_smu.py | 2 +- network/nxos/nxos_snapshot.py | 2 +- network/nxos/nxos_snmp_user.py | 2 +- network/nxos/nxos_switchport.py | 4 ++-- network/nxos/nxos_udld.py | 2 +- network/nxos/nxos_udld_interface.py | 1 + network/nxos/nxos_vpc_interface.py | 2 +- network/nxos/nxos_vtp_domain.py | 2 +- network/nxos/nxos_vxlan_vtep_vni.py | 2 +- network/openswitch/_ops_template.py | 4 ++-- network/openswitch/ops_command.py | 8 ++++---- network/vyos/vyos_command.py | 2 +- network/vyos/vyos_facts.py | 4 ++-- packaging/os/apt.py | 8 ++++---- packaging/os/redhat_subscription.py | 2 +- packaging/os/rpm_key.py | 2 +- source_control/git.py | 6 +++--- source_control/subversion.py | 2 +- system/cron.py | 4 ++-- system/group.py | 2 +- system/service.py | 2 +- system/systemd.py | 2 +- system/user.py | 6 +++--- utilities/helper/_accelerate.py | 2 +- utilities/helper/meta.py | 2 +- utilities/logic/async_wrapper.py | 2 +- windows/win_lineinfile.py | 4 ++-- 91 files changed, 147 insertions(+), 146 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index bb0e9008e57..f2407c7aaaa 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -772,7 +772,7 @@ def create_block_device(module, ec2, volume): if int(volume['iops']) > MAX_IOPS_TO_SIZE_RATIO * size: module.fail_json(msg = 'IOPS must be at most %d times greater than size' % MAX_IOPS_TO_SIZE_RATIO) if 'encrypted' in volume: - module.fail_json(msg = 'You can not set encyrption when creating a volume from a snapshot') + module.fail_json(msg = 'You can not set encryption when creating a volume from a snapshot') if 'ephemeral' in volume: if 'snapshot' in volume: module.fail_json(msg = 'Cannot set both ephemeral and snapshot') @@ -1026,7 +1026,7 @@ def create_instances(module, ec2, vpc, override_count=None): if ebs_optimized: params['ebs_optimized'] = ebs_optimized - # 'tenancy' always has a default value, but it is not a valid parameter for spot instance resquest + # 'tenancy' always has a default value, but it is not a valid parameter for spot instance request if not spot_price: params['tenancy'] = tenancy diff --git a/cloud/amazon/ec2_ami.py b/cloud/amazon/ec2_ami.py index 684a51d31d9..506a370cbce 100644 --- a/cloud/amazon/ec2_ami.py +++ b/cloud/amazon/ec2_ami.py @@ -247,7 +247,7 @@ type: string sample: "435210894375" platform: - description: plaform of image + description: platform of image returned: when AMI is created or already exists type: string sample: null diff --git a/cloud/amazon/ec2_ami_find.py b/cloud/amazon/ec2_ami_find.py index bc7ad944aa5..5545766287a 100644 --- a/cloud/amazon/ec2_ami_find.py +++ b/cloud/amazon/ec2_ami_find.py @@ -228,7 +228,7 @@ type: string sample: "435210894375" platform: - description: plaform of image + description: platform of image returned: when AMI found type: string sample: null diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index c63ebdef4e8..0c22bc76943 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -77,7 +77,7 @@ default: None lc_check: description: - - Check to make sure instances that are being replaced with replace_instances do not aready have the current launch_config. + - Check to make sure instances that are being replaced with replace_instances do not already have the current launch_config. required: false version_added: "1.8" default: True @@ -113,7 +113,7 @@ version_added: "2.0" wait_timeout: description: - - how long before wait instances to become viable when replaced. Used in concjunction with instance_ids option. + - how long before wait instances to become viable when replaced. Used in conjunction with instance_ids option. default: 300 version_added: "1.8" wait_for_instances: @@ -125,7 +125,7 @@ termination_policies: description: - An ordered list of criteria used for selecting instances to be removed from the Auto Scaling group when reducing capacity. - - For 'Default', when used to create a new autoscaling group, the "Default" value is used. When used to change an existent autoscaling group, the current termination policies are mantained + - For 'Default', when used to create a new autoscaling group, the "Default"i value is used. When used to change an existent autoscaling group, the current termination policies are maintained. required: false default: Default choices: ['OldestInstance', 'NewestInstance', 'OldestLaunchConfiguration', 'ClosestToNextInstanceHour', 'Default'] @@ -369,7 +369,7 @@ def wait_for_elb(asg_connection, module, group_name): as_group = asg_connection.get_all_groups(names=[group_name])[0] if as_group.load_balancers and as_group.health_check_type == 'ELB': - log.debug("Waiting for ELB to consider intances healthy.") + log.debug("Waiting for ELB to consider instances healthy.") try: elb_connection = connect_to_aws(boto.ec2.elb, region, **aws_connect_params) except boto.exception.NoAuthHandlerFound as e: @@ -627,7 +627,7 @@ def replace(connection, module): if desired_capacity is None: desired_capacity = as_group.desired_capacity # set temporary settings and wait for them to be reached - # This should get overriden if the number of instances left is less than the batch size. + # This should get overwritten if the number of instances left is less than the batch size. as_group = connection.get_all_groups(names=[group_name])[0] update_size(as_group, max_size + batch_size, min_size + batch_size, desired_capacity + batch_size) diff --git a/cloud/amazon/ec2_elb.py b/cloud/amazon/ec2_elb.py index 2a128017c07..000bcf04b34 100644 --- a/cloud/amazon/ec2_elb.py +++ b/cloud/amazon/ec2_elb.py @@ -204,7 +204,7 @@ def _await_elb_instance_state(self, lb, awaited_state, initial_state, timeout): self.changed = True break elif self._is_instance_state_pending(instance_state): - # If it's pending, we'll skip further checks andd continue waiting + # If it's pending, we'll skip further checks and continue waiting pass elif (awaited_state == 'InService' and instance_state.reason_code == "Instance" @@ -212,7 +212,7 @@ def _await_elb_instance_state(self, lb, awaited_state, initial_state, timeout): # If the reason_code for the instance being out of service is # "Instance" this indicates a failure state, e.g. the instance # has failed a health check or the ELB does not have the - # instance's availabilty zone enabled. The exact reason why is + # instance's availability zone enabled. The exact reason why is # described in InstantState.description. msg = ("The instance %s could not be put in service on %s." " Reason: %s") diff --git a/cloud/amazon/ec2_elb_lb.py b/cloud/amazon/ec2_elb_lb.py index 7311a32a8fe..ec2bcd731c1 100644 --- a/cloud/amazon/ec2_elb_lb.py +++ b/cloud/amazon/ec2_elb_lb.py @@ -135,7 +135,7 @@ version_added: "1.8" stickiness: description: - - An associative array of stickness policy settings. Policy will be applied to all listeners ( see example ) + - An associative array of stickiness policy settings. Policy will be applied to all listeners ( see example ) required: false version_added: "2.0" wait: @@ -316,7 +316,7 @@ - load_balancer_port: 80 - instance_port: 80 -# Create an ELB with load balanacer stickiness enabled +# Create an ELB with load balancer stickiness enabled - local_action: module: ec2_elb_lb name: "New ELB" diff --git a/cloud/amazon/ec2_vpc.py b/cloud/amazon/ec2_vpc.py index 3df47c57b15..e17674e2d05 100644 --- a/cloud/amazon/ec2_vpc.py +++ b/cloud/amazon/ec2_vpc.py @@ -270,7 +270,7 @@ def rtb_changed(route_tables=None, vpc_conn=None, module=None, vpc=None, igw=Non Checks if the remote routes match the local routes. route_tables : Route_tables parameter in the module - vpc_conn : The VPC conection object + vpc_conn : The VPC connection object module : The module object vpc : The vpc object for this route table igw : The internet gateway object for this vpc @@ -503,7 +503,7 @@ def create_vpc(module, vpc_conn): # Handle route tables - this may be worth splitting into a # different module but should work fine here. The strategy to stay - # indempotent is to basically build all the route tables as + # idempotent is to basically build all the route tables as # defined, track the route table ids, and then run through the # remote list of route tables and delete any that we didn't # create. This shouldn't interrupt traffic in theory, but is the diff --git a/cloud/amazon/ec2_vpc_net.py b/cloud/amazon/ec2_vpc_net.py index 54c4307b23f..54be37028a5 100644 --- a/cloud/amazon/ec2_vpc_net.py +++ b/cloud/amazon/ec2_vpc_net.py @@ -25,7 +25,7 @@ options: name: description: - - The name to give your VPC. This is used in combination with the cidr_block paramater to determine if a VPC already exists. + - The name to give your VPC. This is used in combination with the cidr_block parameter to determine if a VPC already exists. required: yes cidr_block: description: diff --git a/cloud/amazon/iam.py b/cloud/amazon/iam.py index 73fd6101576..9f4e119bf18 100644 --- a/cloud/amazon/iam.py +++ b/cloud/amazon/iam.py @@ -78,7 +78,7 @@ default: '1' access_key_ids: description: - - A list of the keys that you want impacted by the access_key_state paramter. + - A list of the keys that you want impacted by the access_key_state parameter. groups: description: - A list of groups the user should belong to. When update, will gracefully remove groups not listed. @@ -334,7 +334,7 @@ def update_user(module, iam, name, new_name, new_path, key_state, key_count, key except boto.exception.BotoServerError as err: error_msg = boto_exception(str(err)) if 'Password does not conform to the account password policy' in error_msg: - module.fail_json(changed=False, msg="Passsword doesn't conform to policy") + module.fail_json(changed=False, msg="Password doesn't conform to policy") else: module.fail_json(msg=error_msg) @@ -391,7 +391,7 @@ def update_user(module, iam, name, new_name, new_path, key_state, key_count, key def set_users_groups(module, iam, name, groups, updated=None, new_name=None): - """ Sets groups for a user, will purge groups not explictly passed, while + """ Sets groups for a user, will purge groups not explicitly passed, while retaining pre-existing groups that also are in the new list. """ changed = False @@ -624,7 +624,7 @@ def main(): if iam_type == 'role' and state == 'update': module.fail_json(changed=False, msg="iam_type: role, cannot currently be updated, " - "please specificy present or absent") + "please specify present or absent") # check if trust_policy is present -- it can be inline JSON or a file path to a JSON file if trust_policy_filepath: diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py index a54863b7d15..15ec55e8305 100644 --- a/cloud/amazon/rds.py +++ b/cloud/amazon/rds.py @@ -115,7 +115,7 @@ description: - Port number that the DB instance uses for connections. Used only when command=create or command=replicate. - Prior to 2.0 it always defaults to null and the API would use 3306, it had to be set to other DB default values when not using MySql. - Starting at 2.0 it auotmaticaly defaults to what is expected for each c(db_engine). + Starting at 2.0 it automatically defaults to what is expected for each c(db_engine). required: false default: 3306 for mysql, 1521 for Oracle, 1433 for SQL Server, 5432 for PostgreSQL. upgrade: diff --git a/cloud/azure/azure.py b/cloud/azure/azure.py index 89ab576ea14..e5d6fc58f5d 100644 --- a/cloud/azure/azure.py +++ b/cloud/azure/azure.py @@ -485,9 +485,9 @@ def terminate_virtual_machine(module, azure): except AzureException as e: module.fail_json(msg="failed to delete the deployment %s, error was: %s" % (deployment.name, str(e))) - # It's unclear when disks associated with terminated deployment get detatched. + # It's unclear when disks associated with terminated deployment get detached. # Thus, until the wait_timeout is reached, we continue to delete disks as they - # become detatched by polling the list of remaining disks and examining the state. + # become detached by polling the list of remaining disks and examining the state. try: _delete_disks_when_detached(azure, wait_timeout, disk_names) except (AzureException, TimeoutError) as e: diff --git a/cloud/azure/azure_rm_storageblob.py b/cloud/azure/azure_rm_storageblob.py index 3e5bd85ee6d..4b5a2e078c7 100644 --- a/cloud/azure/azure_rm_storageblob.py +++ b/cloud/azure/azure_rm_storageblob.py @@ -185,7 +185,7 @@ "type": "BlockBlob" } container: - description: Facts about the current state of the selcted container. + description: Facts about the current state of the selected container. returned: always type: dict sample: { diff --git a/cloud/azure/azure_rm_virtualmachine.py b/cloud/azure/azure_rm_virtualmachine.py index ef6fbdfa46a..a237a8f88ce 100644 --- a/cloud/azure/azure_rm_virtualmachine.py +++ b/cloud/azure/azure_rm_virtualmachine.py @@ -158,7 +158,7 @@ required: false public_ip_allocation_method: description: - - If a public IP address is created when creating the VM (beacuse a Network Interface was not provided), + - If a public IP address is created when creating the VM (because a Network Interface was not provided), determines if the public IP address remains permanently associated with the Network Interface. If set to 'Dynamic' the public IP address may change any time the VM is rebooted or power cycled. choices: @@ -300,7 +300,7 @@ type: list example: ["testvm1001"] deleted_public_ips: - description: List of deleted publid IP addrees names. + description: List of deleted public IP address names. returned: 'on delete' type: list example: ["testvm1001"] @@ -913,7 +913,7 @@ def serialize_vm(self, vm): interface_dict['name'] = int_dict['networkInterfaces'] interface_dict['properties'] = nic_dict['properties'] - # Expand public IPs to include config porperties + # Expand public IPs to include config properties for interface in result['properties']['networkProfile']['networkInterfaces']: for config in interface['properties']['ipConfigurations']: if config['properties'].get('publicIPAddress'): diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index 3a0aedd5171..5be14176fd2 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -289,7 +289,7 @@ required: false pid_mode: description: - - Set the PID namespace mode for the container. Currenly only supports 'host'. + - Set the PID namespace mode for the container. Currently only supports 'host'. default: null required: false privileged: @@ -361,7 +361,7 @@ description: - Size of `/dev/shm`. The format is ``. `number` must be greater than `0`. Unit is optional and can be `b` (bytes), `k` (kilobytes), `m` (megabytes), or `g` (gigabytes). - - Ommitting the unit defaults to bytes. If you omit the size entirely, the system uses `64m`. + - Omitting the unit defaults to bytes. If you omit the size entirely, the system uses `64m`. default: null required: false security_opts: @@ -411,7 +411,7 @@ description: - If true, skip image verification. default: false - requried: false + required: false tty: description: - Allocate a psuedo-TTY. @@ -514,7 +514,7 @@ image: ubuntu:14.04 command: sleep infinity -- name: Stop a contianer +- name: Stop a container docker_container: name: mycontainer state: stopped @@ -1799,7 +1799,7 @@ def _add_networks(self, container, differences): self.results['actions'].append(dict(added_to_network=diff['parameter']['name'], network_parameters=params)) if not self.check_mode: try: - self.log("Connecting conainer to network %s" % diff['parameter']['id']) + self.log("Connecting container to network %s" % diff['parameter']['id']) self.log(params, pretty_print=True) self.client.connect_container_to_network(container.Id, diff['parameter']['id'], **params) except Exception as exc: diff --git a/cloud/docker/docker_network.py b/cloud/docker/docker_network.py index f06f7d5b09e..6b8056aafce 100644 --- a/cloud/docker/docker_network.py +++ b/cloud/docker/docker_network.py @@ -69,7 +69,7 @@ ipam_driver: description: - - Specifiy an IPAM driver. + - Specify an IPAM driver. default: null ipam_options: diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index c4fa36c9af6..ad231607c09 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -423,7 +423,7 @@ returned: always type: string id: - desription: image hash + description: image hash returned: always type: string diff --git a/cloud/openstack/_keystone_user.py b/cloud/openstack/_keystone_user.py index 72eba989862..e7be3a1d038 100644 --- a/cloud/openstack/_keystone_user.py +++ b/cloud/openstack/_keystone_user.py @@ -229,7 +229,7 @@ def ensure_user_exists(keystone, user_name, password, email, tenant_name, check_mode): """ Check if user exists - Return (True, id) if a new user was created, (False, id) user alrady + Return (True, id) if a new user was created, (False, id) user already exists """ diff --git a/cloud/openstack/_quantum_network.py b/cloud/openstack/_quantum_network.py index 09de5e4702d..83c80438f0a 100644 --- a/cloud/openstack/_quantum_network.py +++ b/cloud/openstack/_quantum_network.py @@ -72,7 +72,7 @@ default: present name: description: - - Name to be assigned to the nework + - Name to be assigned to the network required: true default: None provider_network_type: diff --git a/cloud/openstack/_quantum_router_interface.py b/cloud/openstack/_quantum_router_interface.py index e97740d9b55..abff97e0045 100644 --- a/cloud/openstack/_quantum_router_interface.py +++ b/cloud/openstack/_quantum_router_interface.py @@ -32,9 +32,9 @@ version_added: "1.2" author: "Benno Joy (@bennojoy)" deprecated: Deprecated in 2.0. Use os_router instead -short_description: Attach/Dettach a subnet's interface to a router +short_description: Attach/Detach a subnet's interface to a router description: - - Attach/Dettach a subnet interface to a router, to provide a gateway for the subnet. + - Attach/Detach a subnet interface to a router, to provide a gateway for the subnet. options: login_username: description: diff --git a/cloud/openstack/os_floating_ip.py b/cloud/openstack/os_floating_ip.py index f20812a5e57..75d76061fd2 100644 --- a/cloud/openstack/os_floating_ip.py +++ b/cloud/openstack/os_floating_ip.py @@ -187,7 +187,7 @@ def main(): # Requirements are met module.exit_json(changed=False, floating_ip=f_ip) - # Requirments are vague enough to ignore exisitng f_ip and try + # Requirements are vague enough to ignore existing f_ip and try # to create a new f_ip to the server. server = cloud.add_ips_to_server( diff --git a/cloud/openstack/os_ironic.py b/cloud/openstack/os_ironic.py index 79751347615..8537a378c66 100644 --- a/cloud/openstack/os_ironic.py +++ b/cloud/openstack/os_ironic.py @@ -188,7 +188,7 @@ def _choose_id_value(module): def _choose_if_password_only(module, patch): if len(patch) is 1: if 'password' in patch[0]['path'] and module.params['skip_update_of_masked_password']: - # Return false to aabort update as the password appears + # Return false to abort update as the password appears # to be the only element in the patch. return False return True diff --git a/cloud/openstack/os_subnet.py b/cloud/openstack/os_subnet.py index d4efe8727f2..3330af2a267 100644 --- a/cloud/openstack/os_subnet.py +++ b/cloud/openstack/os_subnet.py @@ -42,7 +42,7 @@ network_name: description: - Name of the network to which the subnet should be attached - - requried when I(state) is 'present' + - Required when I(state) is 'present' required: false name: description: diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index 3e8426e90f7..e6c72289c1f 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -262,7 +262,7 @@ vm_extra_config: folder: MyFolder -# Task to gather facts from a vSphere cluster only if the system is a VMWare guest +# Task to gather facts from a vSphere cluster only if the system is a VMware guest - vsphere_guest: vcenter_hostname: vcenter.mydomain.local diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index a63b0d66ca0..f98547b905b 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -88,7 +88,7 @@ # Dumps all databases to hostname.sql - mysql_db: state=dump name=all target=/tmp/{{ inventory_hostname }}.sql -# Imports file.sql similiar to mysql -u -p < hostname.sql +# Imports file.sql similar to mysql -u -p < hostname.sql - mysql_db: state=import name=all target=/tmp/{{ inventory_hostname }}.sql ''' diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 495ccdf6b96..010cdce6ae1 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -103,7 +103,7 @@ without providing any login_user/login_password details. The second must drop a ~/.my.cnf file containing the new root credentials. Subsequent runs of the playbook will then succeed by reading the new credentials from the file." - - Currently, there is only support for the `mysql_native_password` encryted password hash module. + - Currently, there is only support for the `mysql_native_password` encrypted password hash module. author: "Jonathan Mainguy (@Jmainguy)" extends_documentation_fragment: mysql diff --git a/files/acl.py b/files/acl.py index e74646f7ee3..2b898452592 100644 --- a/files/acl.py +++ b/files/acl.py @@ -78,7 +78,7 @@ required: false default: null description: - - DEPRECATED. The acl to set or remove. This must always be quoted in the form of '::'. The qualifier may be empty for some types, but the type and perms are always requried. '-' can be used as placeholder when you do not care about permissions. This is now superseded by entity, type and permissions fields. + - DEPRECATED. The acl to set or remove. This must always be quoted in the form of '::'. The qualifier may be empty for some types, but the type and perms are always required. '-' can be used as placeholder when you do not care about permissions. This is now superseded by entity, type and permissions fields. recursive: version_added: "2.0" @@ -202,7 +202,7 @@ def acl_changed(module, cmd): if get_platform().lower() == 'freebsd': return True - cmd = cmd[:] # lists are mutables so cmd would be overriden without this + cmd = cmd[:] # lists are mutables so cmd would be overwritten without this cmd.insert(1, '--test') lines = run_acl(module, cmd) diff --git a/files/find.py b/files/find.py index 88dc8d382ae..d48e52ed05a 100644 --- a/files/find.py +++ b/files/find.py @@ -48,7 +48,7 @@ required: false default: '*' description: - - One or more (shell or regex) patterns, which type is controled by C(use_regex) option. + - One or more (shell or regex) patterns, which type is controlled by C(use_regex) option. - The patterns restrict the list of files to be returned to those whose basenames match at least one of the patterns specified. Multiple patterns can be specified using a list. aliases: ['pattern'] diff --git a/files/ini_file.py b/files/ini_file.py index e26949a923f..b66acd7e355 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -175,7 +175,7 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese changed = ini_lines[index] != newline ini_lines[index] = newline if changed: - # remove all possible option occurences from the rest of the section + # remove all possible option occurrences from the rest of the section index = index + 1 while index < len(ini_lines): line = ini_lines[index] diff --git a/files/stat.py b/files/stat.py index 48979d73791..10ed4e0ebe2 100644 --- a/files/stat.py +++ b/files/stat.py @@ -172,7 +172,7 @@ type: int sample: 1003 size: - description: Size in bytes for a plain file, ammount of data for some special files + description: Size in bytes for a plain file, amount of data for some special files returned: success, path exists and user can read stats type: int sample: 203 diff --git a/files/synchronize.py b/files/synchronize.py index 9a053c74d75..ff69a2a67c6 100644 --- a/files/synchronize.py +++ b/files/synchronize.py @@ -248,7 +248,7 @@ # Synchronize passing in extra rsync options synchronize: src: /tmp/helloworld - dest: /var/www/helloword + dest: /var/www/helloworld rsync_opts: - "--no-motd" - "--exclude=.git" @@ -413,7 +413,7 @@ def main(): if not source.startswith('"rsync://') and not dest.startswith('"rsync://'): # If the user specified a port value # Note: The action plugin takes care of setting this to a port from - # inventory if the user didn't specify an explict dest_port + # inventory if the user didn't specify an explicit dest_port if dest_port is not None: cmd += " --rsh 'ssh %s %s -o Port=%s'" % (private_key, ssh_opts, dest_port) else: diff --git a/network/basics/get_url.py b/network/basics/get_url.py index 54bcedff05f..d2c8da127f4 100644 --- a/network/basics/get_url.py +++ b/network/basics/get_url.py @@ -101,7 +101,7 @@ If you worry about portability, only the sha1 algorithm is available on all platforms and python versions. The third party hashlib library can be installed for access to additional algorithms. - Additionaly, if a checksum is passed to this parameter, and the file exist under + Additionally, if a checksum is passed to this parameter, and the file exist under the C(dest) location, the destination_checksum would be calculated, and if checksum equals destination_checksum, the file download would be skipped (unless C(force) is true). ' @@ -238,7 +238,7 @@ def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10, head if os.path.exists(tmp_dest): module.fail_json(msg="%s is a file but should be a directory." % tmp_dest) else: - module.fail_json(msg="%s directoy does not exist." % tmp_dest) + module.fail_json(msg="%s directory does not exist." % tmp_dest) fd, tempname = tempfile.mkstemp(dir=tmp_dest) else: diff --git a/network/basics/uri.py b/network/basics/uri.py index e42bb068e64..3ecc85ea592 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -66,7 +66,7 @@ body: description: - The body of the http request/response to the web service. If C(body_format) is set - to 'json' it will take an already formated JSON string or convert a data structure + to 'json' it will take an already formatted JSON string or convert a data structure into JSON. required: false default: null @@ -384,7 +384,7 @@ def main(): dict_headers = module.params['headers'] if body_format == 'json': - # Encode the body unless its a string, then assume it is preformatted JSON + # Encode the body unless its a string, then assume it is pre-formatted JSON if not isinstance(body, basestring): body = json.dumps(body) dict_headers['Content-Type'] = 'application/json' diff --git a/network/cumulus/cl_bond.py b/network/cumulus/cl_bond.py index 5221699b58d..52da0bed3a9 100644 --- a/network/cumulus/cl_bond.py +++ b/network/cumulus/cl_bond.py @@ -154,7 +154,7 @@ notify: reload networking # define cl_bond once in tasks file -# then write inteface config in variables file +# then write interface config in variables file # with just the options you want. cl_bond: name: "{{ item.key }}" diff --git a/network/cumulus/cl_bridge.py b/network/cumulus/cl_bridge.py index 44822b9453e..256fa427d06 100644 --- a/network/cumulus/cl_bridge.py +++ b/network/cumulus/cl_bridge.py @@ -109,7 +109,7 @@ notify: reload networking # define cl_bridge once in tasks file -# then write inteface config in variables file +# then write interface config in variables file # with just the options you want. cl_bridge: name: "{{ item.key }}" diff --git a/network/cumulus/cl_interface.py b/network/cumulus/cl_interface.py index f5b84c127a9..3fd87bf9195 100644 --- a/network/cumulus/cl_interface.py +++ b/network/cumulus/cl_interface.py @@ -131,7 +131,7 @@ notify: reload networking # define cl_interfaces once in tasks -# then write intefaces in variables file +# then write interfaces in variables file # with just the options you want. cl_interface: name: "{{ item.key }}" diff --git a/network/cumulus/cl_interface_policy.py b/network/cumulus/cl_interface_policy.py index ae99cb1bf02..a8392c570e0 100644 --- a/network/cumulus/cl_interface_policy.py +++ b/network/cumulus/cl_interface_policy.py @@ -73,7 +73,7 @@ def read_current_int_dir(module): module.custom_currentportlist = os.listdir(module.params.get('location')) -# take the allowed list and conver it to into a list +# take the allowed list and convert it to into a list # of ports. def convert_allowed_list_to_port_range(module): allowedlist = module.params.get('allowed') diff --git a/network/dellos10/dellos10_config.py b/network/dellos10/dellos10_config.py index 1d976f011f1..b164acec391 100644 --- a/network/dellos10/dellos10_config.py +++ b/network/dellos10/dellos10_config.py @@ -123,7 +123,7 @@ choices: ['yes', 'no'] config: description: - - The C(config) argument allows the playbook desginer to supply + - The C(config) argument allows the playbook designer to supply the base configuration to be used to validate configuration changes necessary. If this argument is provided, the module will not download the running-config from the remote node. diff --git a/network/dellos10/dellos10_facts.py b/network/dellos10/dellos10_facts.py index bbccd20a34c..2d516f55739 100644 --- a/network/dellos10/dellos10_facts.py +++ b/network/dellos10/dellos10_facts.py @@ -36,10 +36,10 @@ gather_subset: description: - When supplied, this argument will restrict the facts collected - to a given subset. Possible values for this argument inlcude + to a given subset. Possible values for this argument include all, hardware, config, and interfaces. Can specify a list of values to include a larger subset. Values can also be used - with an initial M(!) to specify that a specific subset should + with an initial C(M(!)) to specify that a specific subset should not be collected. required: false default: '!config' diff --git a/network/dellos6/dellos6_command.py b/network/dellos6/dellos6_command.py index 3cc8137043c..a9d442f45bc 100644 --- a/network/dellos6/dellos6_command.py +++ b/network/dellos6/dellos6_command.py @@ -38,7 +38,7 @@ description: - List of commands to send to the remote dellos6 device over the configured provider. The resulting output from the command - is returned. If the I(waitfor) argument is provided, the + is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or the number of I(retries) as expired. required: true @@ -56,7 +56,7 @@ - Specifies the number of retries a command should be tried before it is considered failed. The command is run on the target device every retry and evaluated against the - I(waitfor) conditions. + I(wait_for) conditions. required: false default: 10 interval: @@ -80,7 +80,7 @@ transport: cli tasks: - - name: run show verion on remote devices + - name: run show version on remote devices dellos6_command: commands: show version provider "{{ cli }}" diff --git a/network/dellos6/dellos6_facts.py b/network/dellos6/dellos6_facts.py index 4ab90f39214..224ff2e3b82 100644 --- a/network/dellos6/dellos6_facts.py +++ b/network/dellos6/dellos6_facts.py @@ -36,10 +36,10 @@ gather_subset: description: - When supplied, this argument will restrict the facts collected - to a given subset. Possible values for this argument inlcude + to a given subset. Possible values for this argument include all, hardware, config, and interfaces. Can specify a list of values to include a larger subset. Values can also be used - with an initial M(!) to specify that a specific subset should + with an initial C(M(!)) to specify that a specific subset should not be collected. required: false default: '!config' diff --git a/network/dellos9/dellos9_facts.py b/network/dellos9/dellos9_facts.py index c1946a773fc..2e033acfa2e 100644 --- a/network/dellos9/dellos9_facts.py +++ b/network/dellos9/dellos9_facts.py @@ -39,7 +39,7 @@ to a given subset. Possible values for this argument include all, hardware, config, and interfaces. Can specify a list of values to include a larger subset. Values can also be used - with an initial M(!) to specify that a specific subset should + with an initial C(M(!)) to specify that a specific subset should not be collected. required: false default: '!config' diff --git a/network/eos/_eos_template.py b/network/eos/_eos_template.py index d546b0c98ff..99a1a5f51fe 100644 --- a/network/eos/_eos_template.py +++ b/network/eos/_eos_template.py @@ -19,7 +19,7 @@ --- module: eos_template version_added: "2.1" -author: "Peter sprygada (@privateip)" +author: "Peter Sprygada (@privateip)" short_description: Manage Arista EOS device configurations description: - Manages network device configurations over SSH or eAPI. This module @@ -112,7 +112,7 @@ responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] """ diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index e1a0f806df7..24cb6f3d794 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -88,7 +88,7 @@ password: admin transport: cli -- name: run show verion on remote devices +- name: run show version on remote devices eos_command: commands: show version provider: "{{ cli }}" @@ -106,7 +106,7 @@ - show interfaces provider: "{{ cli }}" -- name: run multiple commands and evalute the output +- name: run multiple commands and evaluate the output eos_command: commands: - show version @@ -139,7 +139,7 @@ failed_conditions: description: the conditionals that failed - retured: failed + returned: failed type: list sample: ['...', '...'] """ diff --git a/network/eos/eos_facts.py b/network/eos/eos_facts.py index aa9145367ef..7c53479c8ca 100644 --- a/network/eos/eos_facts.py +++ b/network/eos/eos_facts.py @@ -103,7 +103,7 @@ # hardware ansible_net_filesystems: - description: All file system names availabe on the device + description: All file system names available on the device returned: when hardware is configured type: list ansible_net_memfree_mb: diff --git a/network/ios/_ios_template.py b/network/ios/_ios_template.py index 59076983515..aa69c8f2e85 100644 --- a/network/ios/_ios_template.py +++ b/network/ios/_ios_template.py @@ -94,7 +94,7 @@ src: config.j2 force: yes -- name: provide the base configuration for comparision +- name: provide the base configuration for comparison ios_template: host: hostname username: foo diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py index c1516cec4e9..503d394b41f 100644 --- a/network/ios/ios_facts.py +++ b/network/ios/ios_facts.py @@ -35,7 +35,7 @@ to a given subset. Possible values for this argument include all, hardware, config, and interfaces. Can specify a list of values to include a larger subset. Values can also be used - with an initial M(!) to specify that a specific subset should + with an initial C(M(!)) to specify that a specific subset should not be collected. required: false default: '!config' @@ -87,7 +87,7 @@ # hardware ansible_net_filesystems: - description: All file system names availabe on the device + description: All file system names available on the device returned: when hardware is configured type: list ansible_net_memfree_mb: diff --git a/network/iosxr/_iosxr_template.py b/network/iosxr/_iosxr_template.py index f39ca4d116c..2f1ba8d02d8 100644 --- a/network/iosxr/_iosxr_template.py +++ b/network/iosxr/_iosxr_template.py @@ -19,7 +19,7 @@ --- module: iosxr_template version_added: "2.1" -author: "Peter sprygada (@privateip)" +author: "Peter Sprygada (@privateip)" short_description: Manage Cisco IOSXR device configurations over SSH description: - Manages network device configurations over SSH. This module @@ -97,7 +97,7 @@ responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] """ diff --git a/network/iosxr/iosxr_facts.py b/network/iosxr/iosxr_facts.py index 7af1023ffd6..d8d0e5d93b7 100644 --- a/network/iosxr/iosxr_facts.py +++ b/network/iosxr/iosxr_facts.py @@ -79,7 +79,7 @@ # hardware ansible_net_filesystems: - description: All file system names availabe on the device + description: All file system names available on the device returned: when hardware is configured type: list ansible_net_memfree_mb: diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index 6fc2b0a4ef3..49fa23da853 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -148,7 +148,7 @@ failed_conditionals: description: the conditionals that failed - retured: failed + returned: failed type: list sample: ['...', '...'] """ diff --git a/network/junos/junos_facts.py b/network/junos/junos_facts.py index f22b3bb0000..1e6973ddd23 100644 --- a/network/junos/junos_facts.py +++ b/network/junos/junos_facts.py @@ -45,7 +45,7 @@ format of the configuration file. Devices support three configuration file formats. By default, the configuration from the device is returned as text. The other options include - set and xml. If the xml option is choosen, the configuration file + set and xml. If the xml option is chosen, the configuration file is returned as both xml and json. required: false default: text @@ -81,7 +81,7 @@ RETURN = """ ansible_facts: - descrption: Returns the facts collect from the device + description: Returns the facts collect from the device returned: always type: dict """ diff --git a/network/netvisor/pn_vrouter.py b/network/netvisor/pn_vrouter.py index 0e1a6d37fba..6036daf74b8 100644 --- a/network/netvisor/pn_vrouter.py +++ b/network/netvisor/pn_vrouter.py @@ -97,7 +97,7 @@ 1 and 255 or 0 to unset. pn_bgp_options: description: - - Specify other BGP options as a whitespaces separted string within + - Specify other BGP options as a whitespaces separated string within single quotes ''. pn_rip_redistribute: description: diff --git a/network/netvisor/pn_vrouterif.py b/network/netvisor/pn_vrouterif.py index dd3c60c3a67..8e1a5255a0a 100644 --- a/network/netvisor/pn_vrouterif.py +++ b/network/netvisor/pn_vrouterif.py @@ -32,7 +32,7 @@ - Execute vrouter-interface-add, vrouter-interface-remove, vrouter-interface-modify command. - You configure interfaces to vRouter services on a fabric, cluster, - standalone switch or virtula network(VNET). + standalone switch or virtual network(VNET). options: pn_cliusername: description: diff --git a/network/nxos/_nxos_template.py b/network/nxos/_nxos_template.py index fe379629394..d7b50059e10 100644 --- a/network/nxos/_nxos_template.py +++ b/network/nxos/_nxos_template.py @@ -90,7 +90,7 @@ src: config.j2 force: yes -- name: provide the base configuration for comparision +- name: provide the base configuration for comparison nxos_template: src: candidate_config.txt config: current_config.txt @@ -105,7 +105,7 @@ responses: description: The set of responses from issuing the commands on the device - retured: when not check_mode + returned: when not check_mode type: list sample: ['...', '...'] """ diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index eff27bb509c..0b3ea06dc3d 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -38,7 +38,7 @@ If there is any difference, what is in Ansible will be pushed (configured options will be overridden). This is to improve security, but at the same time remember an ACE is removed, then re-added, so if there is a - change, the new ACE will be exactly what paramaters you are sending to + change, the new ACE will be exactly what parameters you are sending to the module. options: seq: diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 9f7dd16a537..5ec5135b03c 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -949,7 +949,7 @@ def main(): if module.params['vrf'] != 'default': for param, inserted_value in module.params.iteritems(): if param in GLOBAL_PARAMS and inserted_value: - module.fail_json(msg='Global params can be modifed only' + module.fail_json(msg='Global params can be modified only' ' under "default" VRF.', vrf=module.params['vrf'], global_param=param) diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py index 06ced09e7ce..1062ed214ce 100644 --- a/network/nxos/nxos_bgp_neighbor_af.py +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -50,7 +50,7 @@ required: true afi: description: - - Address Family Identifie. + - Address Family Identifier. required: true choices: ['ipv4','ipv6', 'vpnv4', 'vpnv6', 'l2vpn'] safi: diff --git a/network/nxos/nxos_command.py b/network/nxos/nxos_command.py index af302c2f752..c6e33c6e45c 100644 --- a/network/nxos/nxos_command.py +++ b/network/nxos/nxos_command.py @@ -93,7 +93,7 @@ password: admin transport: cli -- name: run show verion on remote devices +- name: run show version on remote devices nxos_command: commands: show version provider: "{{ cli }}" @@ -111,7 +111,7 @@ - show interfaces provider: "{{ cli }}" -- name: run multiple commands and evalute the output +- name: run multiple commands and evaluate the output nxos_command: commands: - show version @@ -144,7 +144,7 @@ failed_conditions: description: the conditionals that failed - retured: failed + returned: failed type: list sample: ['...', '...'] """ diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py index a2de61d0620..48cddf9456a 100644 --- a/network/nxos/nxos_evpn_vni.py +++ b/network/nxos/nxos_evpn_vni.py @@ -28,7 +28,7 @@ extends_documentation_fragment: nxos notes: - default, where supported, restores params default value. - - RD override is not permitted. You should set it to the defalt values + - RD override is not permitted. You should set it to the default values first and then reconfigure it. - C(route_target_both), C(route_target_import) and C(route_target_export valid) values are a list of extended communities, diff --git a/network/nxos/nxos_facts.py b/network/nxos/nxos_facts.py index 9d5defb9a8c..25dd9f4095d 100644 --- a/network/nxos/nxos_facts.py +++ b/network/nxos/nxos_facts.py @@ -100,7 +100,7 @@ # hardware ansible_net_filesystems: - description: All file system names availabe on the device + description: All file system names available on the device returned: when hardware is configured type: list ansible_net_memfree_mb: diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index a5111a23e6b..2c01cad1383 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -63,7 +63,7 @@ RETURN = ''' transfer_status: - description: Whether a file was transfered. "No Transfer" or "Sent". + description: Whether a file was transferred. "No Transfer" or "Sent". returned: success type: string sample: 'Sent' diff --git a/network/nxos/nxos_gir_profile_management.py b/network/nxos/nxos_gir_profile_management.py index ef99c708757..0b9e795b566 100644 --- a/network/nxos/nxos_gir_profile_management.py +++ b/network/nxos/nxos_gir_profile_management.py @@ -39,7 +39,7 @@ default: null mode: description: - - Configure the profile as Maintenance or Normale mode. + - Configure the profile as Maintenance or Normal mode. required: true choices: ['maintenance', 'normal'] state: @@ -376,4 +376,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_install_os.py b/network/nxos/nxos_install_os.py index 705e13b316d..be11cc2e8c6 100644 --- a/network/nxos/nxos_install_os.py +++ b/network/nxos/nxos_install_os.py @@ -36,7 +36,7 @@ - You must know if your platform supports taking a kickstart image as a parameter. If supplied but not supported, errors may occur. - This module attempts to install the software immediately, - wich may trigger a reboot. + which may trigger a reboot. - In check mode, the module tells you if the current boot images are set to the desired images. author: @@ -364,7 +364,7 @@ def set_boot_options(module, image_name, kickstart=None): Args: The main system image file name. Keyword Args: many implementors may choose - to supply a kickstart parameter to specicify a kickstart image. + to supply a kickstart parameter to specify a kickstart image. """ commands = ['terminal dont-ask'] if kickstart is None: @@ -413,4 +413,4 @@ def main(): if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py index 1161709970f..9790389b9c9 100644 --- a/network/nxos/nxos_ntp_auth.py +++ b/network/nxos/nxos_ntp_auth.py @@ -28,7 +28,7 @@ author: - Jason Edelman (@jedelman8) notes: - - If C(state=absent), the moudle will attempt to remove the given key configuration. + - If C(state=absent), the module will attempt to remove the given key configuration. If a matching key configuration isn't found on the device, the module will fail. - If C(state=absent) and C(authentication=on), authentication will be turned off. - If C(state=absent) and C(authentication=off), authentication will be turned on. @@ -59,7 +59,7 @@ choices: ['true', 'false'] authentication: description: - - Turns NTP authenication on or off. + - Turns NTP authentication on or off. required: false default: null choices: ['on', 'off'] @@ -96,7 +96,7 @@ type: dict sample: {"authentication": "off", "trusted_key": "false"} end_state: - description: k/v pairs of ntp autherntication after module execution + description: k/v pairs of ntp authentication after module execution returned: always type: dict sample: {"authentication": "off", "key_id": "32", diff --git a/network/nxos/nxos_nxapi.py b/network/nxos/nxos_nxapi.py index 3fb2a5c6d88..3324130b178 100644 --- a/network/nxos/nxos_nxapi.py +++ b/network/nxos/nxos_nxapi.py @@ -277,7 +277,7 @@ def main(): sandbox=dict(aliases=['enable_sandbox'], default=False, type='bool'), - # Only allow configuration of NXAPI using cli transpsort + # Only allow configuration of NXAPI using cli transport transport=dict(required=True, choices=['cli']), config=dict(), diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index f89aeb37b96..776430b86d8 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -355,7 +355,7 @@ def main(): remote_exists = remote_file_exists(module, pkg, file_system=file_system) if not remote_exists: - module.fail_json(msg="The requested package does't exist " + module.fail_json(msg="The requested package doesn't exist " "on the device") commands = get_commands(module, pkg, file_system) diff --git a/network/nxos/nxos_snapshot.py b/network/nxos/nxos_snapshot.py index f99c2ed294d..f7923a10fa1 100644 --- a/network/nxos/nxos_snapshot.py +++ b/network/nxos/nxos_snapshot.py @@ -29,7 +29,7 @@ author: - Gabriele Gerbino (@GGabriele) notes: - - C(transpot=cli) may cause timeout errors. + - C(transport=cli) may cause timeout errors. - The C(element_key1) and C(element_key2) parameter specify the tags used to distinguish among row entries. In most cases, only the element_key1 parameter needs to specified to be able to distinguish among row entries. diff --git a/network/nxos/nxos_snmp_user.py b/network/nxos/nxos_snmp_user.py index db10d51f4d5..d3add9b7614 100644 --- a/network/nxos/nxos_snmp_user.py +++ b/network/nxos/nxos_snmp_user.py @@ -473,7 +473,7 @@ def main(): if privacy and encrypt: if not pwd and authentication: - module.fail_json(msg='pwd and authentication must be proviced ' + module.fail_json(msg='pwd and authentication must be provided ' 'when using privacy and encrypt') if group and group not in get_snmp_groups(module): diff --git a/network/nxos/nxos_switchport.py b/network/nxos/nxos_switchport.py index 1b6cbf70ae0..387c7c7cda6 100644 --- a/network/nxos/nxos_switchport.py +++ b/network/nxos/nxos_switchport.py @@ -487,7 +487,7 @@ def get_switchport_config_commands(interface, existing, proposed, module): def is_switchport_default(existing): """Determines if switchport has a default config based on mode Args: - existing (dict): existing switcport configuration from Ansible mod + existing (dict): existing switchport configuration from Ansible mod Returns: boolean: True if switchport has OOB Layer 2 config, i.e. vlan 1 and trunk all and mode is access @@ -802,4 +802,4 @@ def main(): module.exit_json(**results) if __name__ == '__main__': - main() \ No newline at end of file + main() diff --git a/network/nxos/nxos_udld.py b/network/nxos/nxos_udld.py index 7c903fcfc6f..db53461f921 100644 --- a/network/nxos/nxos_udld.py +++ b/network/nxos/nxos_udld.py @@ -28,7 +28,7 @@ author: - Jason Edelman (@jedelman8) notes: - - When C(state=absent), it unconfigures existing setings C(msg_time) and set it + - When C(state=absent), it unconfigures existing settings C(msg_time) and set it to its default value of 15. It is cleaner to always use C(state=present). - Module will fail if the udld feature has not been previously enabled. options: diff --git a/network/nxos/nxos_udld_interface.py b/network/nxos/nxos_udld_interface.py index 44bb3c5ebe5..e061dde4651 100644 --- a/network/nxos/nxos_udld_interface.py +++ b/network/nxos/nxos_udld_interface.py @@ -363,6 +363,7 @@ def get_udld_interface(module, interface): table = body['TABLE_interface']['ROW_interface'] status = str(table.get('mib-port-status', None)) + # Note: 'mib-aggresive-mode' is NOT a typo agg = str(table.get('mib-aggresive-mode', 'disabled')) if agg == 'enabled': diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index a621c1b1632..dcc1c213dbf 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -46,7 +46,7 @@ default: null peer_link: description: - - Set to true/false for peer link config on assoicated portchannel. + - Set to true/false for peer link config on associated portchannel. required: false default: null state: diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 806b01f79c7..54704913cd1 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -29,7 +29,7 @@ - VTP feature must be active on the device to use this module. - This module is used to manage only VTP domain names. - VTP domain names are case-sensible. - - If it's never been configured before, VTP version is setted to 1 by default. + - If it's never been configured before, VTP version is set to 1 by default. Otherwise, it leaves the previous configured version untouched. Use M(nxos_vtp_version) to change it. - Use this in combination with M(nxos_vtp_password) and M(nxos_vtp_version) diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py index cf522f63e8f..a50c7600a07 100644 --- a/network/nxos/nxos_vxlan_vtep_vni.py +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -80,7 +80,7 @@ choices: ['present','absent'] include_defaults: description: - - Specify to use or not the complete runnning configuration + - Specify to use or not the complete running configuration for module operations. required: false default: true diff --git a/network/openswitch/_ops_template.py b/network/openswitch/_ops_template.py index 5bb77387aa3..73af4c0a085 100644 --- a/network/openswitch/_ops_template.py +++ b/network/openswitch/_ops_template.py @@ -87,11 +87,11 @@ RETURN = """ updates: description: The list of configuration updates to be merged - retured: always + returned: always type: dict sample: {obj, obj} responses: - desription: returns the responses when configuring using cli + description: returns the responses when configuring using cli returned: when transport == cli type: list sample: [...] diff --git a/network/openswitch/ops_command.py b/network/openswitch/ops_command.py index 43dab36a839..9a3c0b2f2d8 100644 --- a/network/openswitch/ops_command.py +++ b/network/openswitch/ops_command.py @@ -20,7 +20,7 @@ --- module: ops_command version_added: "2.1" -author: "Peter sprygada (@privateip)" +author: "Peter Sprygada (@privateip)" short_description: Run arbitrary commands on OpenSwitch devices. description: - Sends arbitrary commands to an OpenSwitch node and returns the results @@ -33,7 +33,7 @@ description: - List of commands to send to the remote ops device over the configured provider. The resulting output from the command - is returned. If the I(waitfor) argument is provided, the + is returned. If the I(wait_for) argument is provided, the module is not returned until the condition is satisfied or the number of retires as expired. required: true @@ -65,7 +65,7 @@ - Specifies the number of retries a command should by tried before it is considered failed. The command is run on the target device every retry and evaluated against the - I(waitfor) conditions. + I(wait_for) conditions. required: false default: 10 interval: @@ -122,7 +122,7 @@ failed_conditions: description: the conditionals that failed - retured: failed + returned: failed type: list sample: ['...', '...'] """ diff --git a/network/vyos/vyos_command.py b/network/vyos/vyos_command.py index 0d09c4320f2..4c24e9e70d5 100644 --- a/network/vyos/vyos_command.py +++ b/network/vyos/vyos_command.py @@ -117,7 +117,7 @@ sample: [['...', '...'], ['...'], ['...']] failed_conditions: description: The conditionals that failed - retured: failed + returned: failed type: list sample: ['...', '...'] warnings: diff --git a/network/vyos/vyos_facts.py b/network/vyos/vyos_facts.py index 5be19a78e4d..3b0e5558538 100644 --- a/network/vyos/vyos_facts.py +++ b/network/vyos/vyos_facts.py @@ -35,7 +35,7 @@ to a given subset. Possible values for this argument include all, hardware, config, and interfaces. Can specify a list of values to include a larger subset. Values can also be used - with an initial M(!) to specify that a specific subset should + with an initial C(M(!)) to specify that a specific subset should not be collected. required: false default: "!config" @@ -70,7 +70,7 @@ returned: when config is configured type: str ansible_net_commits: - descrption: The set of available configuration revisions + description: The set of available configuration revisions returned: when present type: list ansible_net_hostname: diff --git a/packaging/os/apt.py b/packaging/os/apt.py index eeaf4aa61c3..72709420e94 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -266,8 +266,8 @@ def package_version_compare(version, other_version): def package_status(m, pkgname, version, cache, state): try: # get the package from the cache, as well as the - # the low-level apt_pkg.Package object which contains - # state fields not directly acccesible from the + # low-level apt_pkg.Package object which contains + # state fields not directly accessible from the # higher-level apt.package.Package object. pkg = cache[pkgname] ll_pkg = cache._cache[pkgname] # the low-level package object @@ -819,7 +819,7 @@ def main(): updated_cache = True mtimestamp, updated_cache_time = get_updated_cache_time() - # If theres nothing else to do exit. This will set state as + # If there is nothing else to do exit. This will set state as # changed based on if the cache was updated. if not p['package'] and not p['upgrade'] and not p['deb']: module.exit_json( @@ -879,7 +879,7 @@ def main(): # Store when the update time was last retvals['cache_update_time'] = updated_cache_time # If the cache was updated and the general state change was set to - # False make sure that the change in cache state is acurately + # False make sure that the change in cache state is accurately # updated by setting the general changed state to the same as # the cache state. if updated_cache and not retvals['changed']: diff --git a/packaging/os/redhat_subscription.py b/packaging/os/redhat_subscription.py index f1440772500..2bef4cbb56c 100644 --- a/packaging/os/redhat_subscription.py +++ b/packaging/os/redhat_subscription.py @@ -71,7 +71,7 @@ default: null org_id: description: - - Organisation ID to use in conjunction with activationkey + - Organization ID to use in conjunction with activationkey required: False default: null version_added: "2.0" diff --git a/packaging/os/rpm_key.py b/packaging/os/rpm_key.py index b4d359658eb..4899d5cddfd 100644 --- a/packaging/os/rpm_key.py +++ b/packaging/os/rpm_key.py @@ -39,7 +39,7 @@ default: "present" choices: [present, absent] description: - - Wheather the key will be imported or removed from the rpm db. + - If the key will be imported or removed from the rpm db. validate_certs: description: - If C(no) and the C(key) is a url starting with https, SSL certificates will not be validated. This should only be used diff --git a/source_control/git.py b/source_control/git.py index 8b85df27f44..1060ab91acd 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -551,7 +551,7 @@ def get_head_branch(git_path, module, dest, remote, bare=False): if os.path.isfile(repo_path): try: gitdir = yaml.safe_load(open(repo_path)).get('gitdir') - # There is a posibility the .git file to have an absolute path. + # There is a possibility the .git file to have an absolute path. if os.path.isabs(gitdir): repo_path = gitdir else: @@ -590,7 +590,7 @@ def set_remote_url(git_path, module, repo, dest, remote): label = "set a new url %s for %s" % (repo, remote) module.fail_json(msg="Failed to %s: %s %s" % (label, out, err)) - # Return False if remote_url is None to maintain previous bevhavior + # Return False if remote_url is None to maintain previous behavior # for Git versions prior to 1.7.5 that lack required functionality. return remote_url is not None @@ -614,7 +614,7 @@ def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, g refspecs.append(currenthead) elif is_remote_branch(git_path, module, dest, repo, version): if currenthead != version: - # this workaroung is only needed for older git versions + # this workaround is only needed for older git versions # 1.8.3 is broken, 1.9.x works # ensure that remote branch is available as both local and remote ref refspecs.append('+refs/heads/%s:refs/heads/%s' % (version, version)) diff --git a/source_control/subversion.py b/source_control/subversion.py index 1f52a2925ed..d92634828e7 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -174,7 +174,7 @@ def has_local_mods(self): # The --quiet option will return only modified files. # Match only revisioned files, i.e. ignore status '?'. regex = re.compile(r'^[^?X]') - # Has local mods if more than 0 modifed revisioned files. + # Has local mods if more than 0 modified revisioned files. return len(filter(regex.match, lines)) > 0 def needs_update(self): diff --git a/system/cron.py b/system/cron.py index 00ac3709b90..bfc2f2d2fbb 100644 --- a/system/cron.py +++ b/system/cron.py @@ -22,7 +22,7 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # -# Cron Plugin: The goal of this plugin is to provide an indempotent method for +# Cron Plugin: The goal of this plugin is to provide an idempotent method for # setting up cron jobs on a host. The script will play well with other manually # entered crons. Each cron job entered will be preceded with a comment # describing the job so that it can be found later, which is required to be @@ -139,7 +139,7 @@ env: description: - If set, manages a crontab's environment variable. New variables are added on top of crontab. - "name" and "value" paramenters are the name and the value of environment variable. + "name" and "value" parameters are the name and the value of environment variable. version_added: "2.1" required: false default: "no" diff --git a/system/group.py b/system/group.py index 8edb93a1d0a..f6628727808 100644 --- a/system/group.py +++ b/system/group.py @@ -258,7 +258,7 @@ class DarwinGroup(Group): - group_add() - group_mod() - group manupulation are done using dseditgroup(1). + group manipulation are done using dseditgroup(1). """ platform = 'Darwin' diff --git a/system/service.py b/system/service.py index baac4fed1a6..18b76d32c6c 100644 --- a/system/service.py +++ b/system/service.py @@ -1350,7 +1350,7 @@ def service_enable(self): def service_control(self): status = self.get_sunos_svcs_status() - # if starting or reloading, clear maintenace states + # if starting or reloading, clear maintenance states if self.action in ['start', 'reload', 'restart'] and status in ['maintenance', 'degraded']: rc, stdout, stderr = self.execute_command("%s clear %s" % (self.svcadm_cmd, self.name)) if rc != 0: diff --git a/system/systemd.py b/system/systemd.py index a92c776a038..fdaeae64708 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -75,7 +75,7 @@ - systemd: state=started name=httpd # Example action to stop service cron on debian, if running - systemd: name=cron state=stopped -# Example action to restart service cron on centos, in all cases, also issue deamon-reload to pick up config changes +# Example action to restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes - systemd: state=restarted daemon_reload=yes name=crond # Example action to reload service httpd, in all cases - systemd: name=httpd state=reloaded diff --git a/system/user.py b/system/user.py index 9c1c9adaa83..ab2c4be27a1 100644 --- a/system/user.py +++ b/system/user.py @@ -1528,7 +1528,7 @@ def _get_next_uid(self): def _change_user_password(self): '''Change password for SELF.NAME against SELF.PASSWORD. - Please note that password must be cleatext. + Please note that password must be cleartext. ''' # some documentation on how is stored passwords on OSX: # http://blog.lostpassword.com/2012/07/cracking-mac-os-x-lion-accounts-passwords/ @@ -1560,7 +1560,7 @@ def _make_group_numerical(self): def __modify_group(self, group, action): '''Add or remove SELF.NAME to or from GROUP depending on ACTION. - ACTION can be 'add' or 'remove' otherwhise 'remove' is assumed. ''' + ACTION can be 'add' or 'remove' otherwise 'remove' is assumed. ''' if action == 'add': option = '-a' else: @@ -1574,7 +1574,7 @@ def __modify_group(self, group, action): def _modify_group(self): '''Add or remove SELF.NAME to or from GROUP depending on ACTION. - ACTION can be 'add' or 'remove' otherwhise 'remove' is assumed. ''' + ACTION can be 'add' or 'remove' otherwise 'remove' is assumed. ''' rc = 0 out = '' diff --git a/utilities/helper/_accelerate.py b/utilities/helper/_accelerate.py index 13d0510f0ce..3bb79122191 100644 --- a/utilities/helper/_accelerate.py +++ b/utilities/helper/_accelerate.py @@ -255,7 +255,7 @@ def run(self): conn.sendall("EXISTS\n") # update the last event time so the server doesn't - # shutdown sooner than expected for new cliets + # shutdown sooner than expected for new clients try: self.server.last_event_lock.acquire() self.server.last_event = datetime.datetime.now() diff --git a/utilities/helper/meta.py b/utilities/helper/meta.py index 6e039123c38..7e2863ffddf 100644 --- a/utilities/helper/meta.py +++ b/utilities/helper/meta.py @@ -40,7 +40,7 @@ required: true default: null notes: - - meta is not really a module nor action_plugin as such it cannot be overriden. + - meta is not really a module nor action_plugin as such it cannot be overwritten. author: - "Ansible Core Team" ''' diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 05054504b46..b2af4067f2b 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -222,7 +222,7 @@ def _run_module(wrapped_cmd, jid, job_path): if pid: # Notify the overlord that the async process started - # we need to not return immmediately such that the launched command has an attempt + # we need to not return immediately such that the launched command has an attempt # to initialize PRIOR to ansible trying to clean up the launch directory (and argsfile) # this probably could be done with some IPC later. Modules should always read # the argsfile at the very first start of their execution anyway diff --git a/windows/win_lineinfile.py b/windows/win_lineinfile.py index c6761591619..bc378f4910c 100644 --- a/windows/win_lineinfile.py +++ b/windows/win_lineinfile.py @@ -94,7 +94,7 @@ newline: required: false description: - - "Specifies the line separator style to use for the modified file. This defaults to the windows line separator (\r\n). Note that the indicated line separator will be used for file output regardless of the original line seperator that appears in the input file." + - "Specifies the line separator style to use for the modified file. This defaults to the windows line separator (\r\n). Note that the indicated line separator will be used for file output regardless of the original line separator that appears in the input file." choices: [ "windows", "unix" ] default: "windows" """ @@ -110,7 +110,7 @@ - win_lineinfile: dest=C:\\temp\\services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default" -# Create file if it doesnt exist with a specific encoding +# Create file if it doesn't exist with a specific encoding - win_lineinfile: dest=C:\\temp\\utf16.txt create="yes" encoding="utf-16" line="This is a utf-16 encoded file" # Add a line to a file and ensure the resulting file uses unix line separators From 6795954dc80c444e608e3b26a3c5666fb28847a3 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 13 Oct 2016 15:06:50 -0400 Subject: [PATCH 514/770] fixes bug introduced in 3670215 in junos_config (#5251) The previous fix created a new bug that this PR resolves --- network/junos/junos_config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index 5e2ead6a1d2..f0814a0b56b 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -214,7 +214,7 @@ def diff_commands(commands, config): updates = list() visited = set() - for item in commands.split('\n'): + for item in commands: if len(item) > 0: if not item.startswith('set') and not item.startswith('delete'): raise ValueError('line must start with either `set` or `delete`') @@ -232,6 +232,8 @@ def diff_commands(commands, config): def load_config(module, result): candidate = module.params['lines'] or module.params['src'] + if isinstance(candidate, basestring): + candidate = candidate.split('\n') kwargs = dict() kwargs['comment'] = module.params['comment'] From 9718a58be4773e3efa4e3b5fef2b7bca35c1acc9 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:30:02 +0200 Subject: [PATCH 515/770] Do not leak api_key or root password in log (#5201) --- cloud/linode/linode.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/linode/linode.py b/cloud/linode/linode.py index 022dc231bf4..079feea39f1 100644 --- a/cloud/linode/linode.py +++ b/cloud/linode/linode.py @@ -443,14 +443,14 @@ def main(): state = dict(default='present', choices=['active', 'present', 'started', 'deleted', 'absent', 'stopped', 'restarted']), - api_key = dict(), + api_key = dict(no_log=True), name = dict(type='str'), plan = dict(type='int'), distribution = dict(type='int'), datacenter = dict(type='int'), linode_id = dict(type='int', aliases=['lid']), payment_term = dict(type='int', default=1, choices=[1, 12, 24]), - password = dict(type='str'), + password = dict(type='str', no_log=True), ssh_pub_key = dict(type='str'), swap = dict(type='int', default=512), wait = dict(type='bool', default=True), From 8853f6b6a41c420332189e329fdcd36eccda8950 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:31:08 +0200 Subject: [PATCH 516/770] Do not leak the password in log (#5203) --- cloud/azure/azure.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/azure/azure.py b/cloud/azure/azure.py index e5d6fc58f5d..38e9c2fc7c6 100644 --- a/cloud/azure/azure.py +++ b/cloud/azure/azure.py @@ -535,7 +535,7 @@ def main(): management_cert_path=dict(), endpoints=dict(default='22'), user=dict(), - password=dict(), + password=dict(no_log=True), image=dict(), virtual_network_name=dict(default=None), state=dict(default='present'), From 586353ed02a60000146e240519dcdf31d231d9ea Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:31:54 +0200 Subject: [PATCH 517/770] Do not leak various passwords in log (#5202) --- database/postgresql/postgresql_user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index 7a3bb559936..d8b0b8bb20a 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -544,11 +544,11 @@ def main(): module = AnsibleModule( argument_spec=dict( login_user=dict(default="postgres"), - login_password=dict(default=""), + login_password=dict(default="", no_log=True), login_host=dict(default=""), login_unix_socket=dict(default=""), user=dict(required=True, aliases=['name']), - password=dict(default=None), + password=dict(default=None, no_log=True), state=dict(default="present", choices=["absent", "present"]), priv=dict(default=None), db=dict(default=''), From 528092fd2f2c041e7cd1e10d9f46ef9b384d4dff Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:32:43 +0200 Subject: [PATCH 518/770] Do not leak the subversion password in log (#5200) --- source_control/subversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/subversion.py b/source_control/subversion.py index d92634828e7..8940154c334 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -199,7 +199,7 @@ def main(): revision=dict(default='HEAD', aliases=['rev', 'version']), force=dict(default='no', type='bool'), username=dict(required=False), - password=dict(required=False), + password=dict(required=False, no_log=True), executable=dict(default=None, type='path'), export=dict(default=False, required=False, type='bool'), switch=dict(default=True, required=False, type='bool'), From df4ab73e151313954c961750a35bd93cf3315835 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:33:37 +0200 Subject: [PATCH 519/770] Do not leak the vtp_password in log (#5199) --- network/nxos/nxos_vtp_password.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index 1f78c8e267e..fee217a8ff0 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -404,7 +404,7 @@ def get_vtp_password(module): def main(): argument_spec = dict( - vtp_password=dict(type='str'), + vtp_password=dict(type='str', no_log=True), state=dict(choices=['absent', 'present'], default='present'), ) From 1b6a71db92e859f475ee1ace6e01923d718a960b Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Thu, 13 Oct 2016 22:34:23 +0200 Subject: [PATCH 520/770] Do not leak the password in log (#5189) --- database/postgresql/postgresql_privs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/postgresql/postgresql_privs.py b/database/postgresql/postgresql_privs.py index 6944b54fcc6..74b2630b747 100644 --- a/database/postgresql/postgresql_privs.py +++ b/database/postgresql/postgresql_privs.py @@ -537,7 +537,7 @@ def main(): port=dict(type='int', default=5432), unix_socket=dict(default='', aliases=['login_unix_socket']), login=dict(default='postgres', aliases=['login_user']), - password=dict(default='', aliases=['login_password']) + password=dict(default='', aliases=['login_password'], no_log=True) ), supports_check_mode = True ) From 2fdd869fd6f38efcef8dc7ebbd6042607f9be742 Mon Sep 17 00:00:00 2001 From: Davis Phillips Date: Thu, 13 Oct 2016 14:30:47 -0700 Subject: [PATCH 521/770] vsphere_guest: Set extra config and powerstate after template deploy (#4266) * Fixes #1381 * Fixes #2971 * Fixes #3056 --- cloud/vmware/vsphere_guest.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index e6c72289c1f..b3d186eb98f 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -736,7 +736,7 @@ def deploy_template(vsphere_client, guest, resource_pool, template_src, esxi, mo try: if not vmTarget: - cloneArgs = dict(resourcepool=rpmor, power_on=power_on_after_clone) + cloneArgs = dict(resourcepool=rpmor, power_on=False) if snapshot_to_clone is not None: #check if snapshot_to_clone is specified, Create a Linked Clone instead of a full clone. @@ -748,6 +748,18 @@ def deploy_template(vsphere_client, guest, resource_pool, template_src, esxi, mo cloneArgs["folder"] = vm_extra_config.get("folder") vmTemplate.clone(guest, **cloneArgs) + + vm = vsphere_client.get_vm_by_name(guest) + + # VM was created. If there is any extra config options specified, set + if vm_extra_config: + vm.set_extra_config(vm_extra_config) + + # Power on if asked + if power_on_after_clone == True: + state = 'powered_on' + power_state(vm, state, True) + changed = True else: changed = False From a35b77a10c8b5e6cf380c01607fbef9d71702d9f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 14 Oct 2016 02:11:25 +0200 Subject: [PATCH 522/770] Convert name to bytes to compare it to bools On python 3, bools is a list of bytes: >>> rc,bools = selinux.security_get_boolean_names() >>> 'virt_use_nfs' in bools False >>> bools [b'abrt_anon_write', b'abrt_handle_event', ...] --- system/seboolean.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/seboolean.py b/system/seboolean.py index 1fc9ac2579b..4581c1333ef 100644 --- a/system/seboolean.py +++ b/system/seboolean.py @@ -71,7 +71,7 @@ def has_boolean_value(module, name): rc, bools = selinux.security_get_boolean_names() except OSError: module.fail_json(msg="Failed to get list of boolean names") - if name in bools: + if to_bytes(name) in bools: return True else: return False @@ -215,4 +215,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * +from ansible.module_utils._text import to_bytes main() From 15f8c6835bb5e452e352db8d169f2991a2a5706a Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Fri, 14 Oct 2016 01:50:24 +0200 Subject: [PATCH 523/770] Fix unarchive on python3 Since handler.files_in_archive is a list of files coming from various executables output, that's a bytes list, and we use it with dest who is a str. So we need to convert that to native type. --- files/unarchive.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index 8af1c4106ce..b51d3979c66 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -251,7 +251,7 @@ def files_in_archive(self, force_refresh=False): try: for member in archive.namelist(): if member not in self.excludes: - self._files_in_archive.append(member) + self._files_in_archive.append(to_native(member)) except: archive.close() raise UnarchiveError('Unable to list files in the archive') @@ -623,7 +623,7 @@ def files_in_archive(self, force_refresh=False): # filename = filename.decode('string_escape') filename = codecs.escape_decode(filename)[0] if filename and filename not in self.excludes: - self._files_in_archive.append(filename) + self._files_in_archive.append(to_native(filename)) return self._files_in_archive def is_unarchived(self): @@ -863,5 +863,7 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.urls import * +from ansible.module_utils._text import to_native + if __name__ == '__main__': main() From 845cc1a531610f2db9639393acc7c1663235716e Mon Sep 17 00:00:00 2001 From: Alfredo Solano Date: Mon, 17 Oct 2016 15:17:32 +0900 Subject: [PATCH 524/770] apt: doc: use yaml syntax in examples (#5070) --- packaging/os/apt.py | 102 +++++++++++++++++++++++++++----------------- 1 file changed, 64 insertions(+), 38 deletions(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 72709420e94..b58af6a7c4e 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -127,44 +127,70 @@ ''' EXAMPLES = ''' -# Update repositories cache and install "foo" package -- apt: name=foo update_cache=yes - -# Remove "foo" package -- apt: name=foo state=absent - -# Install the package "foo" -- apt: name=foo state=present - -# Install the version '1.00' of package "foo" -- apt: name=foo=1.00 state=present - -# Update the repository cache and update package "nginx" to latest version using default release squeeze-backport -- apt: name=nginx state=latest default_release=squeeze-backports update_cache=yes - -# Install latest version of "openjdk-6-jdk" ignoring "install-recommends" -- apt: name=openjdk-6-jdk state=latest install_recommends=no - -# Update all packages to the latest version -- apt: upgrade=dist - -# Run the equivalent of "apt-get update" as a separate step -- apt: update_cache=yes - -# Only run "update_cache=yes" if the last one is more than 3600 seconds ago -- apt: update_cache=yes cache_valid_time=3600 - -# Pass options to dpkg on run -- apt: upgrade=dist update_cache=yes dpkg_options='force-confold,force-confdef' - -# Install a .deb package -- apt: deb=/tmp/mypackage.deb - -# Install the build dependencies for package "foo" -- apt: pkg=foo state=build-dep - -# Install a .deb package from the internet. -- apt: deb=https://example.com/python-ppq_0.1-1_all.deb +- name: Update repositories cache and install "foo" package + apt: + name: foo + update_cache: yes + +- name: Remove "foo" package + apt: + name: foo + state: absent + +- name: Install the package "foo" + apt: + name: foo + state: present + +- name: Install the version '1.00' of package "foo" + apt: + name: foo=1.00 + state: present + +- name: Update the repository cache and update package "nginx" to latest version using default release squeeze-backport + apt: + name: nginx + state: latest + default_release: squeeze-backports + update_cache: yes + +- name: Install latest version of "openjdk-6-jdk" ignoring "install-recommends" + apt: + name: openjdk-6-jdk + state: latest + install_recommends: no + +- name: Update all packages to the latest version + apt: + upgrade: dist + +- name: Run the equivalent of "apt-get update" as a separate step + apt: + update_cache: yes + +- name: Only run "update_cache=yes" if the last one is more than 3600 seconds ago + apt: + update_cache: yes + cache_valid_time: 3600 + +- name: Pass options to dpkg on run + apt: + upgrade: dist + update_cache: yes + dpkg_options: force-confold,force-confdef + +- name: Install a .deb package + apt: + deb: /tmp/mypackage.deb + +- name: Install the build dependencies for package "foo" + apt: + pkg: foo + state: build-dep + +- name: Install a .deb package from the internet. + apt: + deb: https://example.com/python-ppq_0.1-1_all.deb ''' RETURN = ''' From 500736d3013ba36fcbddcefafc83732d3d1876bf Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Mon, 17 Oct 2016 15:18:49 +0200 Subject: [PATCH 525/770] Refactor domain/project handling on os_user module (#5212) The keys returned by user objects for default domain and default project are respectively default_domain_id and default_project_id. We need to gather those IDs in case the user passed names, so we can then compare with the user object on the needs_update helper function. --- cloud/openstack/os_user.py | 65 +++++++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/cloud/openstack/os_user.py b/cloud/openstack/os_user.py index 19c984992ac..9bc66ecd52f 100644 --- a/cloud/openstack/os_user.py +++ b/cloud/openstack/os_user.py @@ -120,19 +120,40 @@ sample: "demouser" ''' -def _needs_update(module, user): - keys = ('email', 'default_project', 'domain', 'enabled') - for key in keys: - if module.params[key] is not None and module.params[key] != user.get(key): +def _needs_update(params_dict, user): + for k, v in params_dict.items(): + if k != 'password' and user[k] != v: return True # We don't get password back in the user object, so assume any supplied # password is a change. - if module.params['password'] is not None: + if params_dict['password'] is not None: return True return False +def _get_domain_id(cloud, domain): + try: + # We assume admin is passing domain id + domain_id = cloud.get_domain(domain)['id'] + except: + # If we fail, maybe admin is passing a domain name. + # Note that domains have unique names, just like id. + try: + domain_id = cloud.search_domains(filters={'name': domain})[0]['id'] + except: + # Ok, let's hope the user is non-admin and passing a sane id + domain_id = domain + + return domain_id + +def _get_default_project_id(cloud, default_project): + project = cloud.get_project(default_project) + if not project: + module.fail_json(msg='Default project %s is not valid' % default_project) + + return project['id'] + def main(): argument_spec = openstack_full_argument_spec( @@ -165,41 +186,33 @@ def main(): cloud = shade.openstack_cloud(**module.params) user = cloud.get_user(name) + domain_id = None if domain: opcloud = shade.operator_cloud(**module.params) - try: - # We assume admin is passing domain id - dom = opcloud.get_domain(domain)['id'] - domain = dom - except: - # If we fail, maybe admin is passing a domain name. - # Note that domains have unique names, just like id. - try: - dom = opcloud.search_domains(filters={'name': domain})[0]['id'] - domain = dom - except: - # Ok, let's hope the user is non-admin and passing a sane id - pass + domain_id = _get_domain_id(opcloud, domain) if state == 'present': - project_id = None + default_project_id = None if default_project: - project = cloud.get_project(default_project) - if not project: - module.fail_json(msg='Default project %s is not valid' % default_project) - project_id = project['id'] + default_project_id = _get_default_project_id(cloud, default_project) if user is None: user = cloud.create_user( name=name, password=password, email=email, - default_project=default_project, domain_id=domain, + default_project=default_project_id, domain_id=domain_id, enabled=enabled) changed = True else: - if _needs_update(module, user): + params_dict = {'email': email, 'enabled': enabled, 'password': password} + if domain_id is not None: + params_dict['domain_id'] = domain_id + if default_project_id is not None: + params_dict['default_project_id'] = default_project_id + + if _needs_update(params_dict, user): user = cloud.update_user( user['id'], password=password, email=email, - default_project=project_id, domain_id=domain, + default_project=default_project_id, domain_id=domain_id, enabled=enabled) changed = True else: From 4900632342b981c6b04e934a148e4109b2666c23 Mon Sep 17 00:00:00 2001 From: Steven de Vries Date: Mon, 17 Oct 2016 17:53:07 +0200 Subject: [PATCH 526/770] Move job parameter to meet expected requirements (#5151) closes #5273 --- system/cron.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/cron.py b/system/cron.py index bfc2f2d2fbb..654ca410bb6 100644 --- a/system/cron.py +++ b/system/cron.py @@ -645,10 +645,10 @@ def main(): crontab.remove_env(name) changed = True else: - job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time, disabled) old_job = crontab.find_job(name) if do_install: + job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time, disabled) if len(old_job) == 0: crontab.add_job(name, job) changed = True From d018a86f4f1851351b9c2eb2456849c600ce059c Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Mon, 17 Oct 2016 14:52:28 -0400 Subject: [PATCH 527/770] Fix `fail_json` invocation in `cloudformation` module --- cloud/amazon/cloudformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index d3da86413ac..acaa849bc86 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -263,7 +263,7 @@ def main(): if module.params['template'] is None and module.params['template_url'] is None: if state == 'present': - module.fail_json('Module parameter "template" or "template_url" is required if "state" is "present"') + module.fail_json(msg='Module parameter "template" or "template_url" is required if "state" is "present"') if module.params['template'] is not None: template_body = open(module.params['template'], 'r').read() From b0159fe7d364d2268d32a8da00795b44ffac58c4 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 16:40:45 +0200 Subject: [PATCH 528/770] Remove the wide try/expect clause This doesn't catch anything precise, and none of the methods should throw a expection for anything. This also hide python 3 errors. --- system/authorized_key.py | 33 +++++++++++++++------------------ 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index 4b0420708d1..09e4bf30231 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -243,25 +243,22 @@ def parseoptions(module, options): ''' options_dict = keydict() #ordered dict if options: - try: - # the following regex will split on commas while - # ignoring those commas that fall within quotes - regex = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''') - parts = regex.split(options)[1:-1] - for part in parts: - if "=" in part: - (key, value) = part.split("=", 1) - if options_dict.has_key(key): - if isinstance(options_dict[key], list): - options_dict[key].append(value) - else: - options_dict[key] = [options_dict[key], value] + # the following regex will split on commas while + # ignoring those commas that fall within quotes + regex = re.compile(r'''((?:[^,"']|"[^"]*"|'[^']*')+)''') + parts = regex.split(options)[1:-1] + for part in parts: + if "=" in part: + (key, value) = part.split("=", 1) + if options_dict.has_key(key): + if isinstance(options_dict[key], list): + options_dict[key].append(value) else: - options_dict[key] = value - elif part != ",": - options_dict[part] = None - except: - module.fail_json(msg="invalid option string: %s" % options) + options_dict[key] = [options_dict[key], value] + else: + options_dict[key] = value + elif part != ",": + options_dict[part] = None return options_dict From 4a0042b1f0b2ef0df715282365b48ac747f0fe38 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 16 Oct 2016 23:39:36 +0200 Subject: [PATCH 529/770] Make subversion module work on python 3 In python 3, filter return a iterator and so result in this traceback: Traceback (most recent call last): File \"/tmp/ansible_kzu72kz5/ansible_module_subversion.py\", line 264, in main() File \"/tmp/ansible_kzu72kz5/ansible_module_subversion.py\", line 243, in main local_mods = svn.has_local_mods() File \"/tmp/ansible_kzu72kz5/ansible_module_subversion.py\", line 178, in has_local_mods return len(filter(regex.match, lines)) > 0 TypeError: object of type 'filter' has no len() --- source_control/subversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/subversion.py b/source_control/subversion.py index 8940154c334..470779a7f2f 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -175,7 +175,7 @@ def has_local_mods(self): # Match only revisioned files, i.e. ignore status '?'. regex = re.compile(r'^[^?X]') # Has local mods if more than 0 modified revisioned files. - return len(filter(regex.match, lines)) > 0 + return len(list(filter(regex.match, lines))) > 0 def needs_update(self): curr, url = self.get_revision() From 6f15dfd464549cbaecc99fd50dd61d013af5d74f Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 17 Oct 2016 01:04:57 +0200 Subject: [PATCH 530/770] Make pip module use pip3 on python 3 --- packaging/language/pip.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 1fe039509eb..405c2308b9e 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -114,6 +114,8 @@ example C(pip-3.3), if there are both Python 2.7 and 3.3 installations in the system and you want to run pip for the Python 3.3 installation. It cannot be specified together with the 'virtualenv' parameter (added in 2.1). + By default, it will take the appropriate version for the python interpreter + use by ansible, e.g. pip3 on python 3, and pip2 or pip on python 2. version_added: "1.3" required: false default: null @@ -132,6 +134,8 @@ - Please note that virtualenv (U(http://www.virtualenv.org/)) must be installed on the remote host if the virtualenv parameter is specified and the virtualenv needs to be created. + - By default, this module will use the appropriate version of pip for the + interpreter used by ansible (e.g. pip3 when using python 3, pip2 otherwise) requirements: [ "virtualenv", "pip" ] author: "Matt Wright (@mattupstate)" ''' @@ -188,7 +192,7 @@ from ansible.module_utils.basic import AnsibleModule from ansible.module_utils._text import to_native - +from ansible.module_utils.six import PY3 #: Python one-liners to be run at the command line that will determine the # installed version for these special libraries. These are libraries that @@ -264,7 +268,13 @@ def _get_pip(module, env=None, executable=None): # On Fedora17 and below, CentOS and RedHat 6 and 5, pip is pip-python. # On Fedora, CentOS, and RedHat, the exception is in the virtualenv. # There, pip is just pip. - candidate_pip_basenames = ['pip', 'python-pip', 'pip-python'] + # On python 3.4, pip should be default, cf PEP 453 + # By default, it will try to use pip required for the current python + # interpreter, so people can use pip to install modules dependencies + candidate_pip_basenames = ['pip2', 'pip', 'python-pip', 'pip-python'] + if PY3: + candidate_pip_basenames = ['pip3'] + pip = None if executable is not None: executable = os.path.expanduser(executable) From 510214a03241ac8bc357077e9bdad72bcbd8190d Mon Sep 17 00:00:00 2001 From: John R Barker Date: Tue, 18 Oct 2016 13:53:57 +0100 Subject: [PATCH 531/770] Group "apt-get update" and "apt-get install" (#5283) * Group "apt-get update" and "apt-get install" Should speed up sanity * Run apt-get install in quiet mode --- test/utils/shippable/sanity.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index d9234cf0527..cc0830faec9 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -7,11 +7,11 @@ install_deps="${INSTALL_DEPS:-}" cd "${source_root}" if [ "${install_deps}" != "" ]; then - add-apt-repository ppa:fkrull/deadsnakes && apt-get update -qq && apt-get install python2.4 -qq - + add-apt-repository ppa:fkrull/deadsnakes apt-add-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports universe' apt-get update -qq - apt-get install shellcheck + + apt-get install -qq shellcheck python2.4 pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing From 3266efb02fcd80e60b8a9e4a783fc31e8249a523 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Tue, 18 Oct 2016 07:54:59 +0200 Subject: [PATCH 532/770] Make the code run on python 3 Test suite block on: Traceback (most recent call last): File "/tmp/ansible_fhootp1e/ansible_module_authorized_key.py", line 496, in main() File "/tmp/ansible_fhootp1e/ansible_module_authorized_key.py", line 490, in main results = enforce_state(module, module.params) File "/tmp/ansible_fhootp1e/ansible_module_authorized_key.py", line 410, in enforce_state parsed_new_key = parsekey(module, new_key) File "/tmp/ansible_fhootp1e/ansible_module_authorized_key.py", line 308, in parsekey options = parseoptions(module, options) File "/tmp/ansible_fhootp1e/ansible_module_authorized_key.py", line 253, in parseoptions if options_dict.has_key(key): AttributeError: 'keydict' object has no attribute 'has_key' With keydict being a subclass of dict. --- system/authorized_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index 09e4bf30231..3435cfc9d3c 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -250,7 +250,7 @@ def parseoptions(module, options): for part in parts: if "=" in part: (key, value) = part.split("=", 1) - if options_dict.has_key(key): + if key in options_dict: if isinstance(options_dict[key], list): options_dict[key].append(value) else: From 6e74aa9fbdea18ff0770dd5fb0a38fa0b556bc19 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 18 Oct 2016 11:17:39 -0400 Subject: [PATCH 533/770] added allow_duplicates to include_role docs --- utilities/logic/include_role.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 127313ec9f8..360d5f5a5d0 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -40,8 +40,11 @@ static: description: - Gives Ansible a hint if this is a 'static' include or not. If static it implies that it won't need templating nor loops nor conditionals and will show included tasks in the --list options. + allow_duplicates: + description: + - Overrides the role's metadata setting to allow using a role more than once with the same parameters. required: False - default: None + default: True private: description: - If True the variables from defaults/ and vars/ in a role will not be made available to the rest of the play. From 8c9a3175e2a0528c68ee6d8ea8946a93257083b6 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 18 Oct 2016 11:17:39 -0400 Subject: [PATCH 534/770] added allow_duplicates to include_role docs --- utilities/logic/include_role.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 360d5f5a5d0..761a5748030 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -40,6 +40,11 @@ static: description: - Gives Ansible a hint if this is a 'static' include or not. If static it implies that it won't need templating nor loops nor conditionals and will show included tasks in the --list options. + allow_duplicates: + description: + - Overrides the role's metadata setting to allow using a role more than once with the same parameters. + required: False + default: None allow_duplicates: description: - Overrides the role's metadata setting to allow using a role more than once with the same parameters. From ecc40297537e7cf3912710011a09529fa8b53560 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 18 Oct 2016 12:05:22 -0400 Subject: [PATCH 535/770] fixed doc typo --- utilities/logic/include_role.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 761a5748030..2ddbc62355c 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -40,9 +40,6 @@ static: description: - Gives Ansible a hint if this is a 'static' include or not. If static it implies that it won't need templating nor loops nor conditionals and will show included tasks in the --list options. - allow_duplicates: - description: - - Overrides the role's metadata setting to allow using a role more than once with the same parameters. required: False default: None allow_duplicates: From b59b5d36e0d770d88787b90d12dea35fbda70b4f Mon Sep 17 00:00:00 2001 From: Jamie Evans Date: Tue, 18 Oct 2016 11:33:51 -0700 Subject: [PATCH 536/770] verify both tags and commits (#2654) This fixes a bug where the module fails to verify tags. I added a conditional statement in `verify_commit_sign()` that checks if `version` argument is a tag, if so, use `git verify-tag` instead. --- source_control/git.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/source_control/git.py b/source_control/git.py index 1060ab91acd..41c29a4ca4c 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -786,7 +786,11 @@ def switch_version(git_path, module, dest, remote, version, verify_commit, depth def verify_commit_sign(git_path, module, dest, version): - cmd = "%s verify-commit %s" % (git_path, version) + if version in get_tags(git_path, module, dest): + git_sub = "verify-tag" + else: + git_sub = "verify-commit" + cmd = "%s %s %s" % (git_path, git_sub, version) (rc, out, err) = module.run_command(cmd, cwd=dest) if rc != 0: module.fail_json(msg='Failed to verify GPG signature of commit/tag "%s"' % version, stdout=out, stderr=err, rc=rc) From 20726b94be64b17829f5afb712b4b5f542515229 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Wed, 31 Aug 2016 15:58:19 +0200 Subject: [PATCH 537/770] Ensure yum failures in with-loop result into a failed task The implementation is fairly simple, we force the rc= parameter to not be zero so that the check in _executor/task_result.py_ correctly determines that it failed. Without this change Ansible would report the task to be ok (despite failed=True and msg=Some_error_message) although Ansible stops and the summary output reports a failed task. This fixes #4214, #4384 and also relates to ansible/ansible#12070, ansible/ansible#16006, ansible/ansible##16597, ansible/ansible#17208 and ansible/ansible#17252 --- packaging/os/yum.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 6a9de912c1e..7602b248a65 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -583,7 +583,9 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): if spec.endswith('.rpm') and '://' not in spec: # get the pkg name-v-r.arch if not os.path.exists(spec): - res['msg'] += "No Package file matching '%s' found on system" % spec + res['msg'] += "No RPM file matching '%s' found on system" % spec + res['results'].append("No RPM file matching '%s' found on system" % spec) + res['rc'] = 127 # Ensure the task fails in with-loop module.fail_json(**res) nvra = local_nvra(module, spec) @@ -619,11 +621,13 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): if installed_pkgs: res['results'].append('%s providing %s is already installed' % (installed_pkgs[0], spec)) continue - + # look up what pkgs provide this pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos) if not pkglist: - res['msg'] += "No Package matching '%s' found available, installed or updated" % spec + res['msg'] += "No package matching '%s' found available, installed or updated" % spec + res['results'].append("No package matching '%s' found available, installed or updated" % spec) + res['rc'] = 126 # Ensure the task fails in with-loop module.fail_json(**res) # if any of the packages are involved in a transaction, fail now @@ -631,6 +635,7 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): conflicts = transaction_exists(pkglist) if len(conflicts) > 0: res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) + res['rc'] = 125 # Ensure the task fails in with-loop module.fail_json(**res) # if any of them are installed @@ -685,8 +690,7 @@ def install(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): for spec in items: # Fail on invalid urls: if ('://' in spec and ('No package %s available.' % spec in out or 'Cannot open: %s. Skipping.' % spec in err)): - err = 'Package at %s could not be installed' % spec - module.fail_json(changed=False,msg=err,rc=1) + module.fail_json(msg='Package at %s could not be installed' % spec, rc=1, changed=False) if (rc != 0 and 'Nothing to do' in err) or 'Nothing to do' in out: # avoid failing in the 'Nothing To Do' case # this may happen with an URL spec. @@ -758,7 +762,7 @@ def remove(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): # of the process # at this point we should check to see if the pkg is no longer present - + for pkg in pkgs: if not pkg.startswith('@'): # we can't sensibly check for a group being uninstalled reliably # look to see if the pkg shows up from is_installed. If it doesn't @@ -834,7 +838,9 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): pkglist = what_provides(module, repoq, spec, conf_file, en_repos=en_repos, dis_repos=dis_repos) # FIXME..? may not be desirable to throw an exception here if a single package is missing if not pkglist: - res['msg'] += "No Package matching '%s' found available, installed or updated" % spec + res['msg'] += "No package matching '%s' found available, installed or updated" % spec + res['results'].append("No package matching '%s' found available, installed or updated" % spec) + res['rc'] = 126 # Ensure the task fails in with-loop module.fail_json(**res) nothing_to_do = True @@ -866,6 +872,8 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): conflicts = transaction_exists(pkglist) if len(conflicts) > 0: res['msg'] += "The following packages have pending transactions: %s" % ", ".join(conflicts) + res['results'].append("The following packages have pending transactions: %s" % ", ".join(conflicts)) + res['rc'] = 128 # Ensure the task fails in with-loop module.fail_json(**res) # check_mode output From 953cd915bb567564d951de79167229eec7bc5aa7 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 13:07:04 +0200 Subject: [PATCH 538/770] Cleanup imports of xattr Since the module use re and os, we need to import them. And rather than importing '*', we should limit to the only object/function needed, so we can more easily refactor later. --- files/xattr.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/files/xattr.py b/files/xattr.py index 378665477cf..8f12c853a28 100644 --- a/files/xattr.py +++ b/files/xattr.py @@ -73,6 +73,8 @@ ''' import operator +import re +import os def get_xattr_keys(module,path,follow): cmd = [ module.get_bin_path('getfattr', True) ] @@ -202,7 +204,7 @@ def main(): module.exit_json(changed=changed, msg=msg, xattr=res) # import module snippets -from ansible.module_utils.basic import * - +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception if __name__ == '__main__': main() From f040d63403f6c459a278918fa48fa8cb87754506 Mon Sep 17 00:00:00 2001 From: tedder Date: Fri, 23 Sep 2016 20:59:25 -0700 Subject: [PATCH 539/770] Boto3 rewrite of cloudformation module - removed star-imports, which wasn't possible in Ansible 1.x - boto doesn't have any of the modern features (most notably, changesets), so this rewrite goes all-in on boto3. - tags are updateable, at least in boto3. Fix documentation. - staying with "ansible yaml to json conversion" because I'm trying to keep this scoped properly. The next PR will have AWS-native yaml support. - documented the output. Tried to leave it backwards-compatible but the changes to 'events' might break someone's flow. However, the existing data wasn't terribly useful so I don't assume it will hurt. - split up the code into functions. This should make unit testing possible. - added forward-facing code: 'six' for iterating, started using AWSRetry, common tag conversion. - add todo list - Pass `exception` parameter to fail_json --- cloud/amazon/cloudformation.py | 358 ++++++++++++++++++++------------- 1 file changed, 216 insertions(+), 142 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index acaa849bc86..6b924b434a9 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -14,12 +14,22 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +# upcoming features: +# - AWS-native YAML support +# - Ted's multifile YAML concatenation +# - changesets (and blocking/waiting for them) +# - finish AWSRetry conversion +# - move create/update code out of main +# - unit tests + DOCUMENTATION = ''' --- module: cloudformation short_description: Create or delete an AWS CloudFormation stack description: - Launches an AWS CloudFormation stack and waits for it complete. +notes: + - As of version 2.3, migrated to boto3 to enable new features. To match existing behavior, YAML parsing is done in the module, not given to AWS as YAML. This will change (in fact, it may change before 2.3 is out). version_added: "1.1" options: stack_name: @@ -56,23 +66,16 @@ version_added: "2.0" stack_policy: description: - - the path of the cloudformation stack policy + - the path of the cloudformation stack policy. A policy cannot be removed once placed, but it can be modified. (for instance, [allow all updates](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/protect-stack-resources.html#d0e9051) required: false default: null version_added: "1.9" tags: description: - - Dictionary of tags to associate with stack and it's resources during stack creation. Cannot be updated later. - Requires at least Boto version 2.6.0. + - Dictionary of tags to associate with stack and its resources during stack creation. Can be updated later, updating tags removes previous entries. required: false default: null version_added: "1.4" - region: - description: - - The AWS region to use. If not specified then the value of the AWS_REGION or EC2_REGION environment variable, if any, is used. - required: true - aliases: ['aws_region', 'ec2_region'] - version_added: "1.5" template_url: description: - Location of file containing the template body. The URL must point to a template (max size 307,200 bytes) located in an S3 bucket in the same region as the stack. This parameter is mutually exclusive with 'template'. Either one of them is required if "state" parameter is "present" @@ -87,16 +90,18 @@ version_added: "2.0" author: "James S. Martin (@jsmartin)" -extends_documentation_fragment: aws +extends_documentation_fragment: +- aws +- ec2 ''' EXAMPLES = ''' # Basic task example - name: launch ansible cloudformation example cloudformation: - stack_name: "ansible-cloudformation" + stack_name: "ansible-cloudformation" state: "present" - region: "us-east-1" + region: "us-east-1" disable_rollback: true template: "files/cloudformation-example.json" template_parameters: @@ -110,9 +115,9 @@ # Basic role example - name: launch ansible cloudformation example cloudformation: - stack_name: "ansible-cloudformation" + stack_name: "ansible-cloudformation" state: "present" - region: "us-east-1" + region: "us-east-1" disable_rollback: true template: "roles/cloudformation/files/cloudformation-example.json" template_parameters: @@ -145,16 +150,46 @@ Stack: ansible-cloudformation ''' +RETURN = ''' +events: + type: list + description: Most recent events in Cloudformation's event log. This may be from a previous run in some cases. + returned: always + sample: ["StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE", "StackEvent AWS::CloudFormation::Stack stackname UPDATE_COMPLETE_CLEANUP_IN_PROGRESS"] +log: + description: Debugging logs. Useful when modifying or finding an error. + returned: always + type: list + sample: ["updating stack"] + stack_resources: + description: AWS stack resources and their status. List of dictionaries, one dict per resource. + type: list + sample: [ + { + "last_updated_time": "2016-10-11T19:40:14.979000+00:00", + "logical_resource_id": "CFTestSg", + "physical_resource_id": "cloudformation2-CFTestSg-16UQ4CYQ57O9F", + "resource_type": "AWS::EC2::SecurityGroup", + "status": "UPDATE_COMPLETE", + "status_reason": null + } + ] + +''' + import json import time import yaml +import sys +import traceback + try: - import boto - import boto.cloudformation.connection - HAS_BOTO = True + import boto3 + import botocore + HAS_BOTO3 = True except ImportError: - HAS_BOTO = False + HAS_BOTO3 = False def boto_exception(err): @@ -162,7 +197,7 @@ def boto_exception(err): if hasattr(err, 'error_message'): error = err.error_message elif hasattr(err, 'message'): - error = err.message + error = err.message + ' ' + str(err) + ' - ' + str(type(err)) else: error = '%s: %s' % (Exception, err) @@ -170,7 +205,7 @@ def boto_exception(err): def boto_version_required(version_tuple): - parts = boto.Version.split('.') + parts = boto3.__version__.split('.') boto_version = [] try: for part in parts: @@ -179,64 +214,117 @@ def boto_version_required(version_tuple): boto_version.append(-1) return tuple(boto_version) >= tuple(version_tuple) +def get_stack_events(cfn, stack_name): + '''This event data was never correct, it worked as a side effect. So the v2.3 format is different.''' + ret = { 'events':[], 'log':[] } + + try: + events = cfn.describe_stack_events(StackName=stack_name) + except (botocore.exceptions.ValidationError,botocore.exceptions.ClientError) as err: + error_msg = boto_exception(err) + if 'does not exist'.format(stack_name) in error_msg: + # missing stack, don't bail. + ret['log'].append('Stack does not exist.') + return ret + ret['log'].append('Unknown error: ' + str(error_msg)) + return ret + + for e in events.get('StackEvents', []): + eventline = 'StackEvent {} {} {}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus']) + ret['events'].append(eventline) + + if e['ResourceStatus'].endswith('FAILED'): + failline = '{} {} {}: {}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus'], e['ResourceStatusReason']) + ret['log'].append(failline) + + return ret def stack_operation(cfn, stack_name, operation): '''gets the status of a stack while it is created/updated/deleted''' existed = [] - result = {} operation_complete = False while operation_complete == False: try: - stack = invoke_with_throttling_retries(cfn.describe_stacks, stack_name)[0] + stack = get_stack_facts(cfn, stack_name) existed.append('yes') except: - if 'yes' in existed: - result = dict(changed=True, - output='Stack Deleted', - events=list(map(str, list(stack.describe_events())))) + if 'yes' in existed or operation=='DELETE': # stacks may delete fast, look in a few ways. + ret = get_stack_events(cfn, stack_name) + ret.update({ 'changed': True, 'output': 'Stack Deleted'}) + return ret else: - result = dict(changed= True, output='Stack Not Found') - break - if '%s_COMPLETE' % operation == stack.stack_status: - result = dict(changed=True, - events = list(map(str, list(stack.describe_events()))), - output = 'Stack %s complete' % operation) - break - if 'ROLLBACK_COMPLETE' == stack.stack_status or '%s_ROLLBACK_COMPLETE' % operation == stack.stack_status: - result = dict(changed=True, failed=True, - events = list(map(str, list(stack.describe_events()))), - output = 'Problem with %s. Rollback complete' % operation) - break - elif '%s_FAILED' % operation == stack.stack_status: - result = dict(changed=True, failed=True, - events = list(map(str, list(stack.describe_events()))), - output = 'Stack %s failed' % operation) - break - elif '%s_ROLLBACK_FAILED' % operation == stack.stack_status: - result = dict(changed=True, failed=True, - events = list(map(str, list(stack.describe_events()))), - output = 'Stack %s rollback failed' % operation) - break + return {'changed': True, 'failed': True, 'output': 'Stack Not Found', 'exception': traceback.format_exc()} + ret = get_stack_events(cfn, stack_name) + if not stack: + if 'yes' in existed or operation=='DELETE': # stacks may delete fast, look in a few ways. + ret = get_stack_events(cfn, stack_name) + ret.update({ 'changed': True, 'output': 'Stack Deleted'}) + return ret + else: + ret.update({'changed':False, 'failed':True, 'output' : 'Stack not found.'}) + return ret + elif stack['StackStatus'].endswith('_ROLLBACK_COMPLETE'): + ret.update({'changed':True, 'failed':True, 'output' : 'Problem with %s. Rollback complete' % operation}) + return ret + # note the ordering of ROLLBACK_COMPLETE and COMPLETE, because otherwise COMPLETE will match both cases. + elif stack['StackStatus'].endswith('_COMPLETE'): + ret.update({'changed':True, 'output' : 'Stack %s complete' % operation }) + return ret + elif stack['StackStatus'].endswith('_ROLLBACK_FAILED'): + ret.update({'changed':True, 'failed':True, 'output' : 'Stack %s rollback failed' % operation}) + return ret + # note the ordering of ROLLBACK_FAILED and FAILED, because otherwise FAILED will match both cases. + elif stack['StackStatus'].endswith('_FAILED'): + ret.update({'changed':True, 'failed':True, 'output': 'Stack %s failed' % operation}) + return ret else: + # this can loop forever :/ + #return dict(changed=True, failed=True, output = str(stack), operation=operation) time.sleep(5) - return result + return {'failed': True, 'output':'Failed for unknown reasons.'} + +@AWSRetry.backoff(tries=3, delay=5) +def describe_stacks(cfn, stack_name): + return cfn.describe_stacks(StackName=stack_name) + +def get_stack_facts(cfn, stack_name): + try: + stack_response = describe_stacks(cfn, stack_name) + stack_info = stack_response['Stacks'][0] + #except AmazonCloudFormationException as e: + except (botocore.exceptions.ValidationError,botocore.exceptions.ClientError) as err: + error_msg = boto_exception(err) + if 'does not exist'.format(stack_name) in error_msg: + # missing stack, don't bail. + return None + + # other error, bail. + raise err + + if stack_response and stack_response.get('Stacks', None): + stacks = stack_response['Stacks'] + if len(stacks): + stack_info = stacks[0] + + return stack_info IGNORE_CODE = 'Throttling' MAX_RETRIES=3 -def invoke_with_throttling_retries(function_ref, *argv): +def invoke_with_throttling_retries(function_ref, *argv, **kwargs): retries=0 while True: try: - retval=function_ref(*argv) + retval=function_ref(*argv, **kwargs) return retval - except boto.exception.BotoServerError as e: - if e.code != IGNORE_CODE or retries==MAX_RETRIES: - raise e + except Exception as e: + # boto way of looking for retries + #if e.code != IGNORE_CODE or retries==MAX_RETRIES: + raise e time.sleep(5 * (2**retries)) retries += 1 def main(): - argument_spec = ec2_argument_spec() + argument_spec = ansible.module_utils.ec2.ec2_argument_spec() argument_spec.update(dict( stack_name=dict(required=True), template_parameters=dict(required=False, type='dict', default={}), @@ -255,123 +343,107 @@ def main(): argument_spec=argument_spec, mutually_exclusive=[['template_url', 'template']], ) - if not HAS_BOTO: - module.fail_json(msg='boto required for this module') + if not HAS_BOTO3: + module.fail_json(msg='boto3 required for this module') + # collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around. + stack_params = { + 'Capabilities':['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], + } state = module.params['state'] - stack_name = module.params['stack_name'] + stack_params['StackName'] = module.params['stack_name'] if module.params['template'] is None and module.params['template_url'] is None: if state == 'present': module.fail_json(msg='Module parameter "template" or "template_url" is required if "state" is "present"') if module.params['template'] is not None: - template_body = open(module.params['template'], 'r').read() - else: - template_body = None + stack_params['TemplateBody'] = open(module.params['template'], 'r').read() if module.params['template_format'] == 'yaml': - if template_body is None: + if not stack_params.get('TemplateBody'): module.fail_json(msg='yaml format only supported for local templates') else: - template_body = json.dumps(yaml.load(template_body), indent=2) + stack_params['TemplateBody'] = json.dumps(yaml.load(stack_params['TemplateBody']), indent=2) - notification_arns = module.params['notification_arns'] + if module.params.get('notification_arns'): + stack_params['NotificationARNs'] = module.params['notification_arns'].split(',') + else: + stack_params['NotificationARNs'] = [] if module.params['stack_policy'] is not None: - stack_policy_body = open(module.params['stack_policy'], 'r').read() - else: - stack_policy_body = None + stack_params['StackPolicyBody'] = open(module.params['stack_policy'], 'r').read() - disable_rollback = module.params['disable_rollback'] template_parameters = module.params['template_parameters'] - tags = module.params['tags'] - template_url = module.params['template_url'] - - region, ec2_url, aws_connect_kwargs = get_aws_connection_info(module) - - kwargs = dict() - if tags is not None: - if not boto_version_required((2,6,0)): - module.fail_json(msg='Module parameter "tags" requires at least Boto version 2.6.0') - kwargs['tags'] = tags + stack_params['Parameters'] = [{'ParameterKey':k, 'ParameterValue':v} for k, v in template_parameters.items()] + if isinstance(module.params.get('tags'), dict): + stack_params['Tags'] = ansible.module_utils.ec2.ansible_dict_to_boto3_tag_list(module.params['tags']) - # convert the template parameters ansible passes into a tuple for boto - template_parameters_tup = [(k, v) for k, v in template_parameters.items()] - stack_outputs = {} + if module.params.get('template_url'): + stack_params['TemplateURL'] = module.params['template_url'] - try: - cfn = connect_to_aws(boto.cloudformation, region, **aws_connect_kwargs) - except boto.exception.NoAuthHandlerFound as e: - module.fail_json(msg=str(e)) update = False result = {} - operation = None + + try: + region, ec2_url, aws_connect_kwargs = ansible.module_utils.ec2.get_aws_connection_info(module, boto3=True) + cfn = ansible.module_utils.ec2.boto3_conn(module, conn_type='client', resource='cloudformation', region=region, endpoint=ec2_url, **aws_connect_kwargs) + except botocore.exceptions.NoCredentialsError as e: + module.fail_json(msg=boto_exception(e)) + + stack_info = get_stack_facts(cfn, stack_params['StackName']) # if state is present we are going to ensure that the stack is either # created or updated - if state == 'present': + if state == 'present' and not stack_info: try: - cfn.create_stack(stack_name, parameters=template_parameters_tup, - template_body=template_body, - notification_arns=notification_arns, - stack_policy_body=stack_policy_body, - template_url=template_url, - disable_rollback=disable_rollback, - capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], - **kwargs) - operation = 'CREATE' + # 'disablerollback' only applies on creation, not update. + stack_params['DisableRollback'] = module.params['disable_rollback'] + + cfn.create_stack(**stack_params) except Exception as err: error_msg = boto_exception(err) - if 'AlreadyExistsException' in error_msg or 'already exists' in error_msg: - update = True - else: - module.fail_json(msg=error_msg) - if not update: - result = stack_operation(cfn, stack_name, operation) - - # if the state is present and the stack already exists, we try to update it - # AWS will tell us if the stack template and parameters are the same and - # don't need to be updated. - if update: + #return {'error': error_msg} + module.fail_json(msg=error_msg) + result = stack_operation(cfn, stack_params['StackName'], 'CREATE') + if not result: module.fail_json(msg="empty result") + + if state == 'present' and stack_info: + # if the state is present and the stack already exists, we try to update it. + # AWS will tell us if the stack template and parameters are the same and + # don't need to be updated. try: - cfn.update_stack(stack_name, parameters=template_parameters_tup, - template_body=template_body, - notification_arns=notification_arns, - stack_policy_body=stack_policy_body, - disable_rollback=disable_rollback, - template_url=template_url, - capabilities=['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM'], - **kwargs) - operation = 'UPDATE' + cfn.update_stack(**stack_params) except Exception as err: error_msg = boto_exception(err) if 'No updates are to be performed.' in error_msg: result = dict(changed=False, output='Stack is already up-to-date.') else: module.fail_json(msg=error_msg) - - if operation == 'UPDATE': - result = stack_operation(cfn, stack_name, operation) + #return {'error': error_msg} + #module.fail_json(msg=error_msg) + result = stack_operation(cfn, stack_params['StackName'], 'UPDATE') + if not result: module.fail_json(msg="empty result") # check the status of the stack while we are creating/updating it. # and get the outputs of the stack if state == 'present' or update: - stack = invoke_with_throttling_retries(cfn.describe_stacks,stack_name)[0] - for output in stack.outputs: - stack_outputs[output.key] = output.value - result['stack_outputs'] = stack_outputs - stack_resources = [] - for res in cfn.list_stack_resources(stack_name): + stack = get_stack_facts(cfn, stack_params['StackName']) + for output in stack.get('Outputs', []): + result['stack_outputs'][output['OutputKey']] = output['OutputValue'] + stack_resources = [] + reslist = cfn.list_stack_resources(StackName=stack_params['StackName']) + for res in reslist.get('StackResourceSummaries', []): stack_resources.append({ - "last_updated_time": res.last_updated_time, - "logical_resource_id": res.logical_resource_id, - "physical_resource_id": res.physical_resource_id, - "status": res.resource_status, - "status_reason": res.resource_status_reason, - "resource_type": res.resource_type }) + "logical_resource_id": res['LogicalResourceId'], + "physical_resource_id": res['PhysicalResourceId'], + "resource_type": res['ResourceType'], + "last_updated_time": res['LastUpdatedTimestamp'], + "status": res['ResourceStatus'], + "status_reason": res.get('ResourceStatusReason') # can be blank, apparently + }) result['stack_resources'] = stack_resources # absent state is different because of the way delete_stack works. @@ -379,24 +451,26 @@ def main(): # so must describe the stack first if state == 'absent': + #result = {} try: - invoke_with_throttling_retries(cfn.describe_stacks,stack_name) - operation = 'DELETE' - except Exception as err: - error_msg = boto_exception(err) - if 'Stack:%s does not exist' % stack_name in error_msg: + stack = get_stack_facts(cfn, stack_params['StackName']) + if not stack: result = dict(changed=False, output='Stack not found.') else: - module.fail_json(msg=error_msg) - if operation == 'DELETE': - cfn.delete_stack(stack_name) - result = stack_operation(cfn, stack_name, operation) + cfn.delete_stack(StackName=stack_params['StackName']) + result = stack_operation(cfn, stack_params['StackName'], 'DELETE') + except Exception as err: + module.fail_json(msg=boto_exception(err), exception=traceback.format_exc()) module.exit_json(**result) + # import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.ec2 import * +from ansible.module_utils.basic import AnsibleModule +import ansible.module_utils.ec2 + +# import a class, otherwise we'll use a fully qualified path +from ansible.module_utils.ec2 import AWSRetry if __name__ == '__main__': main() From d8b015d9c8607323a70c4fe7f026e0927376b0cd Mon Sep 17 00:00:00 2001 From: tedder Date: Mon, 17 Oct 2016 16:29:54 -0700 Subject: [PATCH 540/770] Cloudformation module fix unintentional changed=true - Don't rewrite the result; this is causing 'changed=true' on update - Move AWSRetry import to top since it's a decorator, and is needed at definition-time --- cloud/amazon/cloudformation.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 6b924b434a9..2d1890c4c94 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -191,6 +191,8 @@ except ImportError: HAS_BOTO3 = False +# import a class, otherwise we'll use a fully qualified path +from ansible.module_utils.ec2 import AWSRetry def boto_exception(err): '''generic error message handler''' @@ -210,7 +212,7 @@ def boto_version_required(version_tuple): try: for part in parts: boto_version.append(int(part)) - except: + except ValueError: boto_version.append(-1) return tuple(boto_version) >= tuple(version_tuple) @@ -242,13 +244,14 @@ def get_stack_events(cfn, stack_name): def stack_operation(cfn, stack_name, operation): '''gets the status of a stack while it is created/updated/deleted''' existed = [] - operation_complete = False - while operation_complete == False: + while True: try: stack = get_stack_facts(cfn, stack_name) existed.append('yes') except: - if 'yes' in existed or operation=='DELETE': # stacks may delete fast, look in a few ways. + # If the stack previously existed, and now can't be found then it's + # been deleted successfully. + if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. ret = get_stack_events(cfn, stack_name) ret.update({ 'changed': True, 'output': 'Stack Deleted'}) return ret @@ -415,6 +418,7 @@ def main(): # don't need to be updated. try: cfn.update_stack(**stack_params) + result = stack_operation(cfn, stack_params['StackName'], 'UPDATE') except Exception as err: error_msg = boto_exception(err) if 'No updates are to be performed.' in error_msg: @@ -423,7 +427,6 @@ def main(): module.fail_json(msg=error_msg) #return {'error': error_msg} #module.fail_json(msg=error_msg) - result = stack_operation(cfn, stack_params['StackName'], 'UPDATE') if not result: module.fail_json(msg="empty result") # check the status of the stack while we are creating/updating it. @@ -469,8 +472,6 @@ def main(): from ansible.module_utils.basic import AnsibleModule import ansible.module_utils.ec2 -# import a class, otherwise we'll use a fully qualified path -from ansible.module_utils.ec2 import AWSRetry if __name__ == '__main__': main() From cd9e39420bc1676ff7ed84f933e8e4e78c485bef Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Tue, 18 Oct 2016 10:58:09 -0400 Subject: [PATCH 541/770] Fix cloudformation module return parameter documentation Always return stack outputs, even if only an empty dict --- cloud/amazon/cloudformation.py | 62 +++++++++++++++++++--------------- 1 file changed, 34 insertions(+), 28 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 2d1890c4c94..5e7a5066486 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -161,20 +161,24 @@ returned: always type: list sample: ["updating stack"] - stack_resources: - description: AWS stack resources and their status. List of dictionaries, one dict per resource. - type: list - sample: [ - { - "last_updated_time": "2016-10-11T19:40:14.979000+00:00", - "logical_resource_id": "CFTestSg", - "physical_resource_id": "cloudformation2-CFTestSg-16UQ4CYQ57O9F", - "resource_type": "AWS::EC2::SecurityGroup", - "status": "UPDATE_COMPLETE", - "status_reason": null - } - ] - +stack_resources: + description: AWS stack resources and their status. List of dictionaries, one dict per resource. + type: list + sample: [ + { + "last_updated_time": "2016-10-11T19:40:14.979000+00:00", + "logical_resource_id": "CFTestSg", + "physical_resource_id": "cloudformation2-CFTestSg-16UQ4CYQ57O9F", + "resource_type": "AWS::EC2::SecurityGroup", + "status": "UPDATE_COMPLETE", + "status_reason": null + } + ] +stack_outputs: + type: dict + description: A key:value dictionary of all the stack outputs currently defined. If there are no stack outputs, it is an empty dictionary. + returned: always + sample: {"MySg": "AnsibleModuleTestYAML-CFTestSg-C8UVS567B6NS"} ''' import json @@ -218,13 +222,13 @@ def boto_version_required(version_tuple): def get_stack_events(cfn, stack_name): '''This event data was never correct, it worked as a side effect. So the v2.3 format is different.''' - ret = { 'events':[], 'log':[] } + ret = {'events':[], 'log':[]} try: events = cfn.describe_stack_events(StackName=stack_name) - except (botocore.exceptions.ValidationError,botocore.exceptions.ClientError) as err: + except (botocore.exceptions.ValidationError, botocore.exceptions.ClientError) as err: error_msg = boto_exception(err) - if 'does not exist'.format(stack_name) in error_msg: + if 'does not exist' in error_msg: # missing stack, don't bail. ret['log'].append('Stack does not exist.') return ret @@ -253,36 +257,35 @@ def stack_operation(cfn, stack_name, operation): # been deleted successfully. if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. ret = get_stack_events(cfn, stack_name) - ret.update({ 'changed': True, 'output': 'Stack Deleted'}) + ret.update({'changed': True, 'output': 'Stack Deleted'}) return ret else: return {'changed': True, 'failed': True, 'output': 'Stack Not Found', 'exception': traceback.format_exc()} ret = get_stack_events(cfn, stack_name) if not stack: - if 'yes' in existed or operation=='DELETE': # stacks may delete fast, look in a few ways. + if 'yes' in existed or operation == 'DELETE': # stacks may delete fast, look in a few ways. ret = get_stack_events(cfn, stack_name) - ret.update({ 'changed': True, 'output': 'Stack Deleted'}) + ret.update({'changed': True, 'output': 'Stack Deleted'}) return ret else: - ret.update({'changed':False, 'failed':True, 'output' : 'Stack not found.'}) + ret.update({'changed': False, 'failed': True, 'output' : 'Stack not found.'}) return ret elif stack['StackStatus'].endswith('_ROLLBACK_COMPLETE'): - ret.update({'changed':True, 'failed':True, 'output' : 'Problem with %s. Rollback complete' % operation}) + ret.update({'changed': True, 'failed' :True, 'output': 'Problem with %s. Rollback complete' % operation}) return ret # note the ordering of ROLLBACK_COMPLETE and COMPLETE, because otherwise COMPLETE will match both cases. elif stack['StackStatus'].endswith('_COMPLETE'): - ret.update({'changed':True, 'output' : 'Stack %s complete' % operation }) + ret.update({'changed': True, 'output' : 'Stack %s complete' % operation }) return ret elif stack['StackStatus'].endswith('_ROLLBACK_FAILED'): - ret.update({'changed':True, 'failed':True, 'output' : 'Stack %s rollback failed' % operation}) + ret.update({'changed': True, 'failed': True, 'output': 'Stack %s rollback failed' % operation}) return ret # note the ordering of ROLLBACK_FAILED and FAILED, because otherwise FAILED will match both cases. elif stack['StackStatus'].endswith('_FAILED'): - ret.update({'changed':True, 'failed':True, 'output': 'Stack %s failed' % operation}) + ret.update({'changed': True, 'failed': True, 'output': 'Stack %s failed' % operation}) return ret else: # this can loop forever :/ - #return dict(changed=True, failed=True, output = str(stack), operation=operation) time.sleep(5) return {'failed': True, 'output':'Failed for unknown reasons.'} @@ -347,7 +350,7 @@ def main(): mutually_exclusive=[['template_url', 'template']], ) if not HAS_BOTO3: - module.fail_json(msg='boto3 required for this module') + module.fail_json(msg='boto3 and botocore are required for this module') # collect the parameters that are passed to boto3. Keeps us from having so many scalars floating around. stack_params = { @@ -434,6 +437,9 @@ def main(): if state == 'present' or update: stack = get_stack_facts(cfn, stack_params['StackName']) + if result.get('stack_outputs') is None: + # always define stack_outputs, but it may be empty + result['stack_outputs'] = {} for output in stack.get('Outputs', []): result['stack_outputs'][output['OutputKey']] = output['OutputValue'] stack_resources = [] @@ -458,7 +464,7 @@ def main(): try: stack = get_stack_facts(cfn, stack_params['StackName']) if not stack: - result = dict(changed=False, output='Stack not found.') + result = {'changed': False, 'output': 'Stack not found.'} else: cfn.delete_stack(StackName=stack_params['StackName']) result = stack_operation(cfn, stack_params['StackName'], 'DELETE') From 2511500a912e927755227ffc428482028a506cca Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 13:25:43 +0200 Subject: [PATCH 542/770] Cleanup import for authorized_key Do not import '*', to ease future refactoring and cleanup of module_utils. --- system/authorized_key.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index 3435cfc9d3c..fabc7af3c1f 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -491,6 +491,7 @@ def main(): module.exit_json(**results) # import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.urls import fetch_url +from ansible.module_utils.pycompat24 import get_exception main() From 77c635f18bd6dffc3b3d1fe75eebf024673fa8de Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 13:16:17 +0200 Subject: [PATCH 543/770] cleanup import for htpassword module In order to ease future refactoring, we should avoid importing '*' from ansible.module_utils.basic. --- web_infrastructure/htpasswd.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_infrastructure/htpasswd.py b/web_infrastructure/htpasswd.py index 3ad9de6e60f..f1a4e482f3a 100644 --- a/web_infrastructure/htpasswd.py +++ b/web_infrastructure/htpasswd.py @@ -257,7 +257,8 @@ def main(): # import module snippets -from ansible.module_utils.basic import * +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception if __name__ == '__main__': main() From 6145e24ed4efb97e4e3a14d428f8cbe1712dbf5c Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Wed, 19 Oct 2016 00:07:28 +0200 Subject: [PATCH 544/770] Fix code for python 3 Since dict.keys return a dictkeys under python 3, we hav to cast it to a list to avoid traceback: Traceback (most recent call last): File "/tmp/ansible_sh16ejbd/ansible_module_authorized_key.py", line 496, in main() File "/tmp/ansible_sh16ejbd/ansible_module_authorized_key.py", line 490, in main results = enforce_state(module, module.params) File "/tmp/ansible_sh16ejbd/ansible_module_authorized_key.py", line 410, in enforce_state parsed_new_key = parsekey(module, new_key) File "/tmp/ansible_sh16ejbd/ansible_module_authorized_key.py", line 308, in parsekey options = parseoptions(module, options) File "/tmp/ansible_sh16ejbd/ansible_module_authorized_key.py", line 259, in parseoptions options_dict[key] = value File "/tmp/ansible_sh16ejbd/ansible_module_authorized_key.py", line 164, in __setitem__ self.itemlist.append(key) AttributeError: 'dict_keys' object has no attribute 'append' Yet another fix for https://github.com/ansible/ansible/pull/18053 --- system/authorized_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index fabc7af3c1f..f8bbbadce95 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -159,7 +159,7 @@ class keydict(dict): def __init__(self, *args, **kw): super(keydict,self).__init__(*args, **kw) - self.itemlist = super(keydict,self).keys() + self.itemlist = list(super(keydict,self).keys()) def __setitem__(self, key, value): self.itemlist.append(key) super(keydict,self).__setitem__(key, value) From ced24b7b03584afeb9fc0a5789ab866a933503be Mon Sep 17 00:00:00 2001 From: Daniel Andrei Minca Date: Wed, 19 Oct 2016 00:55:37 +0300 Subject: [PATCH 545/770] fix unclear documentation for docker container the docker container module's `exposed_ports` was slightly ambigous. Use the official Docker documentation to define what an `exposed port` is. Resolves: ansible/ansible-modules-core#5303 Signed-off-by: Daniel Andrei Minca --- cloud/docker/docker_container.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index 5be14176fd2..a1a595ed2b3 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -124,7 +124,8 @@ required: false exposed_ports: description: - - List of additional container ports to expose for port mappings or links. + - List of additional container ports which informs Docker that the container + listens on the specified network ports at runtime. If the port is already exposed using EXPOSE in a Dockerfile, it does not need to be exposed again. default: null From 53e452e88b1c920d0c941fec604f3c684d67b588 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 19 Oct 2016 17:38:56 +0100 Subject: [PATCH 546/770] Run validate-modules from ansible/ansible@devel (#5257) * Run validate-modules from devel Use a clean dir for checkout typo Correct path validate-modules requires mock and voluptuous==0.8.8 typo Ensure script is running Remove testing debug Install Ansible only once Install ansible and validate_modules requirements Now that we no longer pip install Ansible we need to manually install it's dependencies Debug Dependencies are listed in ansible/ansible debug submodules typo typo working * Matt's feedback * Use mktemp to checkout and delete directory after running * Single quotes --- test/utils/shippable/sanity.sh | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index cc0830faec9..84dab4c54be 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -6,6 +6,14 @@ install_deps="${INSTALL_DEPS:-}" cd "${source_root}" +# FIXME REPOMERGE: No need to checkout ansible +build_dir=$(mktemp -d) +trap 'rm -rf "{$build_dir}"' EXIT + +git clone "https://github.com/ansible/ansible.git" "${build_dir}" --recursive +source "${build_dir}/hacking/env-setup" +# REPOMERGE: END + if [ "${install_deps}" != "" ]; then add-apt-repository ppa:fkrull/deadsnakes apt-add-repository 'deb http://archive.ubuntu.com/ubuntu trusty-backports universe' @@ -13,10 +21,14 @@ if [ "${install_deps}" != "" ]; then apt-get install -qq shellcheck python2.4 - pip install git+https://github.com/ansible/ansible.git@devel#egg=ansible - pip install git+https://github.com/sivel/ansible-testing.git#egg=ansible_testing + # Install dependencies for ansible and validate_modules + pip install -r "${build_dir}/test/utils/shippable/sanity-requirements.txt" --upgrade + pip list + fi +validate_modules="${build_dir}/test/sanity/validate-modules/validate-modules" + python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . python2.6 -m compileall -fq . @@ -24,7 +36,7 @@ python2.7 -m compileall -fq . python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" ANSIBLE_DEPRECATION_WARNINGS=false \ - ansible-validate-modules --exclude '/utilities/|/shippable(/|$)' . + "${validate_modules}" --exclude '/utilities/|/shippable(/|$)' . shellcheck \ test/utils/shippable/*.sh From 2d459b797f954ea9239e49bb0360f29be91eb3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filip=20Hub=C3=ADk?= Date: Wed, 5 Oct 2016 11:06:05 +0200 Subject: [PATCH 547/770] Fix incorrect line wrapping in output from yum check-updates https://github.com/ansible/ansible-modules-core/issues/4318#issuecomment-251416661 --- packaging/os/yum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 7602b248a65..131144d8535 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -800,6 +800,8 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): res['results'].append('Nothing to do here, all packages are up to date') return res elif rc == 100: + # remove incorrect new lines in longer columns in output from yum check-update + out=re.sub('\n\W+', ' ', out) available_updates = out.split('\n') # build update dictionary for line in available_updates: From 7a7ff3ebcacacbdbc71d3a89f3411734ee86ec0f Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 19 Oct 2016 20:57:50 +0100 Subject: [PATCH 548/770] Typo in cleanup (#5322) --- test/utils/shippable/sanity.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 84dab4c54be..3b754afa7b0 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -8,7 +8,7 @@ cd "${source_root}" # FIXME REPOMERGE: No need to checkout ansible build_dir=$(mktemp -d) -trap 'rm -rf "{$build_dir}"' EXIT +trap 'rm -rf "${build_dir}"' EXIT git clone "https://github.com/ansible/ansible.git" "${build_dir}" --recursive source "${build_dir}/hacking/env-setup" From 23da0fc2b85fa4f4c1fdb1185b14a0f626e8996a Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Wed, 19 Oct 2016 19:50:21 -0400 Subject: [PATCH 549/770] Support native YAML in CloudFormation module (#5327) Support the new native YAML format in the CloudFormation API. This means the existing `template_format` parameter is deprecated. This commit also adds a warning for the deprecated parameter. --- cloud/amazon/cloudformation.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 5e7a5066486..3549cc2963d 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -15,7 +15,6 @@ # along with Ansible. If not, see . # upcoming features: -# - AWS-native YAML support # - Ted's multifile YAML concatenation # - changesets (and blocking/waiting for them) # - finish AWSRetry conversion @@ -83,7 +82,7 @@ version_added: "2.0" template_format: description: - - For local templates, allows specification of json or yaml format + - (deprecated) For local templates, allows specification of json or yaml format. Templates are now passed raw to CloudFormation regardless of format. This parameter is ignored since Ansible 2.3. default: json choices: [ json, yaml ] required: false @@ -340,7 +339,7 @@ def main(): stack_policy=dict(default=None, required=False), disable_rollback=dict(default=False, type='bool'), template_url=dict(default=None, required=False), - template_format=dict(default='json', choices=['json', 'yaml'], required=False), + template_format=dict(default=None, choices=['json', 'yaml'], required=False), tags=dict(default=None, type='dict') ) ) @@ -366,12 +365,6 @@ def main(): if module.params['template'] is not None: stack_params['TemplateBody'] = open(module.params['template'], 'r').read() - if module.params['template_format'] == 'yaml': - if not stack_params.get('TemplateBody'): - module.fail_json(msg='yaml format only supported for local templates') - else: - stack_params['TemplateBody'] = json.dumps(yaml.load(stack_params['TemplateBody']), indent=2) - if module.params.get('notification_arns'): stack_params['NotificationARNs'] = module.params['notification_arns'].split(',') else: @@ -471,6 +464,10 @@ def main(): except Exception as err: module.fail_json(msg=boto_exception(err), exception=traceback.format_exc()) + if module.params['template_format'] is not None: + result['warnings'] = [('Argument `template_format` is deprecated ' + 'since Ansible 2.3, JSON and YAML templates are now passed ' + 'directly to the CloudFormation API.')] module.exit_json(**result) From 98f6019d86eb92bf1a68e830dacb1f81905c08fb Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Wed, 19 Oct 2016 19:55:10 -0400 Subject: [PATCH 550/770] Remove unused YAML import from cloudformation --- cloud/amazon/cloudformation.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 3549cc2963d..3ea1017d524 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -182,7 +182,6 @@ import json import time -import yaml import sys import traceback From bd51222cdf789a020205b7c112a1ec24f12bf0b9 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 20 Oct 2016 10:42:10 -0400 Subject: [PATCH 551/770] Add separate clone parameter (#5307) * Add separate clone parameter This brings the hg module in line with the git module for controlling individual update and checkout functionality based on whether the directory exists or not. It also allows specifying `no` for both to pull the remote revision without performing a checkout * Reflect the right added ver for the hg clone arg --- source_control/hg.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/source_control/hg.py b/source_control/hg.py index 5f279e1a9ed..e0e50e2c795 100644 --- a/source_control/hg.py +++ b/source_control/hg.py @@ -41,6 +41,7 @@ dest: description: - Absolute path of where the repository should be cloned to. + This parameter is required, unless clone and update are set to no required: true default: null revision: @@ -70,6 +71,13 @@ version_added: "2.0" description: - If C(no), do not retrieve new revisions from the origin repository + clone: + required: false + default: "yes" + choices: [ "yes", "no" ] + version_added: "2.3" + description: + - If C(no), do not clone the repository if it does not exist locally. executable: required: false default: null @@ -88,6 +96,10 @@ EXAMPLES = ''' # Ensure the current working copy is inside the stable branch and deletes untracked files if any. - hg: repo=https://bitbucket.org/user/repo1 dest=/home/user/repo1 revision=stable purge=yes + +# Example just get information about the repository whether or not it has +# already been cloned locally. +- hg: repo=git://bitbucket.org/user/repo dest=/srv/checkout clone=no update=no ''' import os @@ -129,6 +141,13 @@ def get_revision(self): else: return to_native(out).strip('\n') + def get_remote_revision(self): + (rc, out, err) = self._command(['id', self.repo]) + if rc != 0: + self.module_fail_json(msg=err) + else: + return to_native(out).strip('\n') + def has_local_mods(self): now = self.get_revision() if '+' in now: @@ -215,11 +234,12 @@ def main(): module = AnsibleModule( argument_spec = dict( repo = dict(required=True, aliases=['name']), - dest = dict(required=True, type='path'), + dest = dict(type='path'), revision = dict(default=None, aliases=['version']), force = dict(default='no', type='bool'), purge = dict(default='no', type='bool'), update = dict(default='yes', type='bool'), + clone = dict(default='yes', type='bool'), executable = dict(default=None), ), ) @@ -229,22 +249,33 @@ def main(): force = module.params['force'] purge = module.params['purge'] update = module.params['update'] + clone = module.params['clone'] hg_path = module.params['executable'] or module.get_bin_path('hg', True) - hgrc = os.path.join(dest, '.hg/hgrc') + if dest is not None: + hgrc = os.path.join(dest, '.hg/hgrc') # initial states before = '' changed = False cleaned = False + if not dest and (clone or update): + module.fail_json(msg="the destination directory must be specified unless clone=no and update=no") + hg = Hg(module, dest, repo, revision, hg_path) # If there is no hgrc file, then assume repo is absent # and perform clone. Otherwise, perform pull and update. + if not clone and not update: + out = hg.get_remote_revision() + module.exit_json(after=out, changed=False) if not os.path.exists(hgrc): - (rc, out, err) = hg.clone() - if rc != 0: - module.fail_json(msg=err) + if clone: + (rc, out, err) = hg.clone() + if rc != 0: + module.fail_json(msg=err) + else: + module.exit_json(changed=False) elif not update: # Just return having found a repo already in the dest path before = hg.get_revision() From 2508d5f5cbe398bceea64ccc9549b5955c53f7f2 Mon Sep 17 00:00:00 2001 From: Matthew Jones Date: Thu, 20 Oct 2016 10:42:58 -0400 Subject: [PATCH 552/770] Add separate checkout and update parameters (#5306) * Add separate checkout and update parameters This brings the svn module in line with the git module for controlling individual update and checkout functionality based on whether the directory exists or not. It also allows specifying `no` for both to pull the remote revision without performing a checkout * Update version-added for new parameters --- source_control/subversion.py | 41 +++++++++++++++++++++++++++++++++--- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/source_control/subversion.py b/source_control/subversion.py index 470779a7f2f..449cd3cdbe1 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -72,6 +72,20 @@ description: - Path to svn executable to use. If not supplied, the normal mechanism for resolving binary paths will be used. + checkout: + required: false + default: "yes" + choices: [ "yes", "no" ] + version_added: "2.3" + description: + - If no, do not check out the repository if it does not exist locally + update: + required: false + default: "yes" + choices: [ "yes", "no" ] + version_added: "2.3" + description: + - If no, do not retrieve new revisions from the origin repository export: required: false default: "no" @@ -94,6 +108,10 @@ # Export subversion directory to folder - subversion: repo=svn+ssh://an.example.org/path/to/repo dest=/src/export export=True + +# Example just get information about the repository whether or not it has +# already been cloned locally. +- subversion: repo=svn+ssh://an.example.org/path/to/repo dest=/srv/checkout checkout=no update=no ''' import re @@ -168,6 +186,12 @@ def get_revision(self): url = re.search(r'^URL:.*$', text, re.MULTILINE).group(0) return rev, url + def get_remote_revision(self): + '''Revision and URL of subversion working directory.''' + text = '\n'.join(self._exec(["info", self.repo])) + rev = re.search(r'^Revision:.*$', text, re.MULTILINE).group(0) + return rev + def has_local_mods(self): '''True if revisioned files have been added or modified. Unrevisioned files are ignored.''' lines = self._exec(["status", "--quiet", "--ignore-externals", self.dest]) @@ -194,7 +218,7 @@ def needs_update(self): def main(): module = AnsibleModule( argument_spec=dict( - dest=dict(required=True, type='path'), + dest=dict(type='path'), repo=dict(required=True, aliases=['name', 'repository']), revision=dict(default='HEAD', aliases=['rev', 'version']), force=dict(default='no', type='bool'), @@ -202,6 +226,8 @@ def main(): password=dict(required=False, no_log=True), executable=dict(default=None, type='path'), export=dict(default=False, required=False, type='bool'), + checkout=dict(default=True, required=False, type='bool'), + update=dict(default=True, required=False, type='bool'), switch=dict(default=True, required=False, type='bool'), ), supports_check_mode=True @@ -216,19 +242,28 @@ def main(): svn_path = module.params['executable'] or module.get_bin_path('svn', True) export = module.params['export'] switch = module.params['switch'] + checkout = module.params['checkout'] + update = module.params['update'] # We screenscrape a huge amount of svn commands so use C locale anytime we # call run_command() module.run_command_environ_update = dict(LANG='C', LC_MESSAGES='C') + if not dest and (checkout or update or export): + module.fail_json(msg="the destination directory must be specified unless checkout=no, update=no, and export=no") + svn = Subversion(module, dest, repo, revision, username, password, svn_path) + if not export and not update and not checkout: + module.exit_json(changed=False, after=svn.get_remote_revision()) if export or not os.path.exists(dest): before = None local_mods = False if module.check_mode: module.exit_json(changed=True) - if not export: + elif not export and not checkout: + module.exit_json(changed=False) + if not export and checkout: svn.checkout() else: svn.export(force=force) @@ -236,7 +271,7 @@ def main(): # Order matters. Need to get local mods before switch to avoid false # positives. Need to switch before revert to ensure we are reverting to # correct repo. - if module.check_mode: + if module.check_mode or not update: check, before, after = svn.needs_update() module.exit_json(changed=check, before=before, after=after) before = svn.get_revision() From d77027f49543cbf486a65eb6c845d71929b1e47e Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 19 Oct 2016 19:21:21 -0700 Subject: [PATCH 553/770] Fix authorized_key module to preserve the order of options The last fix allowing multiple definitions of the same option key (for permitopen support) introduced a set() which removed the guaranteed ordering of the options. This change restores ordering. The change is larger than simply removing the set because we do need to handle the non-dict semantics around keys not being unique in the data structure. The new code make use of __setitem__() and items() to do its work. Trying to use getitem() or keys() should be looked upon with suspicion as neither of those follow dictionary semantics and it is quite possible the coder doesn't realize this. The next time we need to touch or enhance the keydict code it should probably be rewritten to not pretend to extend the dictionary interface. --- system/authorized_key.py | 92 ++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 31 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index f8bbbadce95..fb82579b864 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -143,7 +143,6 @@ # # see example in examples/playbooks -import sys import os import pwd import os.path @@ -151,26 +150,74 @@ import re import shlex +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +from ansible.module_utils.urls import fetch_url + class keydict(dict): - """ a dictionary that maintains the order of keys as they are added """ + """ a dictionary that maintains the order of keys as they are added + + This has become an abuse of the dict interface. Probably should be + rewritten to be an entirely custom object with methods instead of + bracket-notation. + + Our requirements are for a data structure that: + * Preserves insertion order + * Can store multiple values for a single key. + + The present implementation has the following functions used by the rest of + the code: + + * __setitem__(): to add a key=value. The value can never be disassociated + with the key, only new values can be added in addition. + * items(): to retrieve the key, value pairs. + + Other dict methods should work but may be surprising. For instance, there + will be multiple keys that are the same in keys() and __getitem__() will + return a list of the values that have been set via __setitem__. + """ # http://stackoverflow.com/questions/2328235/pythonextend-the-dict-class def __init__(self, *args, **kw): super(keydict,self).__init__(*args, **kw) self.itemlist = list(super(keydict,self).keys()) + def __setitem__(self, key, value): self.itemlist.append(key) - super(keydict,self).__setitem__(key, value) + if key in self: + self[key].append(value) + else: + super(keydict, self).__setitem__(key, [value]) + def __iter__(self): return iter(self.itemlist) + def keys(self): - return list(set(self.itemlist)) - def values(self): - return [self[key] for key in self] + return self.itemlist + + def _item_generator(self): + indexes = {} + for key in self.itemlist: + if key in indexes: + indexes[key] += 1 + else: + indexes[key] = 0 + yield key, self[key][indexes[key]] + + def iteritems(self): + return self._item_generator() + + def items(self): + return list(self.iteritems()) + def itervalues(self): - return (self[key] for key in self) + return (item[1] for item in self.iteritems()) + + def values(self): + return list(self.itervalues()) + def keyfile(module, user, write=False, path=None, manage_dir=True): """ @@ -250,13 +297,7 @@ def parseoptions(module, options): for part in parts: if "=" in part: (key, value) = part.split("=", 1) - if key in options_dict: - if isinstance(options_dict[key], list): - options_dict[key].append(value) - else: - options_dict[key] = [options_dict[key], value] - else: - options_dict[key] = value + options_dict[key] = value elif part != ",": options_dict[part] = None @@ -346,15 +387,11 @@ def writekeys(module, filename, keys): option_str = "" if options: option_strings = [] - for option_key in options.keys(): - if options[option_key]: - if isinstance(options[option_key], list): - for value in options[option_key]: - option_strings.append("%s=%s" % (option_key, value)) - else: - option_strings.append("%s=%s" % (option_key, options[option_key])) - else: + for option_key, value in options.items(): + if value is None: option_strings.append("%s" % option_key) + else: + option_strings.append("%s=%s" % (option_key, value)) option_str = ",".join(option_strings) option_str += " " key_line = "%s%s %s %s\n" % (option_str, type, keyhash, comment) @@ -379,7 +416,6 @@ def enforce_state(module, params): state = params.get("state", "present") key_options = params.get("key_options", None) exclusive = params.get("exclusive", False) - validate_certs = params.get("validate_certs", True) error_msg = "Error getting key from: %s" # if the key is a url, request it and use it as key source @@ -416,12 +452,10 @@ def enforce_state(module, params): parsed_options = parseoptions(module, key_options) parsed_new_key = (parsed_new_key[0], parsed_new_key[1], parsed_options, parsed_new_key[3]) - present = False matched = False non_matching_keys = [] if parsed_new_key[0] in existing_keys: - present = True # Then we check if everything matches, including # the key type and options. If not, we append this # existing key to the non-matching list @@ -471,7 +505,6 @@ def enforce_state(module, params): return params def main(): - module = AnsibleModule( argument_spec = dict( user = dict(required=True, type='str'), @@ -490,8 +523,5 @@ def main(): results = enforce_state(module, module.params) module.exit_json(**results) -# import module snippets -from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils.urls import fetch_url -from ansible.module_utils.pycompat24 import get_exception -main() +if __name__ == '__main__': + main() From 653ec28a975182b91a7ef0308c327caa7be515ce Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 20 Oct 2016 13:09:58 -0700 Subject: [PATCH 554/770] On Ubuntu16, virtualenv always tries to use python2 even when python2 is not installed. Workaround that by mimicing the upstream virtualenv behaviour in code (use the python version that was used to invoke virtualenv/the ansible module) --- packaging/language/pip.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 405c2308b9e..95cd7caf5f6 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -79,7 +79,7 @@ description: - The Python executable used for creating the virtual environment. For example C(python3.5), C(python2.7). When not specified, the - system Python version is used. + Python version used to run the ansible module is used. required: false default: null state: @@ -415,6 +415,14 @@ def main(): if virtualenv_python: cmd += ' -p%s' % virtualenv_python + elif PY3: + # Ubuntu currently has a patch making virtualenv always + # try to use python2. Since Ubuntu16 works without + # python2 installed, this is a problem. This code mimics + # the upstream behaviour of using the python which invoked + # virtualenv to determine which python is used inside of + # the virtualenv (when none are specified). + cmd += ' -p%s' % sys.executable cmd = "%s %s" % (cmd, env) rc, out_venv, err_venv = module.run_command(cmd, cwd=chdir) From 7cfe6df92e8c73877b127f30ddff29b86ef74b8f Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Thu, 20 Oct 2016 17:18:14 -0400 Subject: [PATCH 555/770] GCE: Add support for 'number' parameter for manually provisioned Google Compute clusters (#4276) * Add option for number parameter to generate manually provisioned clusters from a base name * Refactor code to work with starting and stopped when number is specified * Update docs * Fix documentation error breaking Travis * Fixes for async gce operations * Fix documentation * base_name from parameter to alias for name and fixes for renaming variables * Fix breaking change on gce.py * Fix bugs with name parameter * Fix comments for Github build checks * Add logic to set changed appropriately for cluster provisioning --- cloud/google/gce.py | 177 +++++++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 66 deletions(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index 10e0153f3c7..9c18b71f7a7 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -89,9 +89,17 @@ default: null name: description: - - identifier when working with a single instance. Will be deprecated in a future release. - Please 'instance_names' instead. + - either a name of a single instance or when used with 'num_instances', + the base name of a cluster of nodes required: false + aliases: ['base_name'] + num_instances: + description: + - can be used with 'name', specifies + the number of nodes to provision using 'name' + as a base name + required: false + version_added: "2.3" network: description: - name of the network, 'default' will be used if not specified @@ -336,7 +344,7 @@ def get_instance_info(inst): }) -def create_instances(module, gce, instance_names): +def create_instances(module, gce, instance_names, number): """Creates new instances. Attributes other than instance_names are picked up from 'module' @@ -446,40 +454,62 @@ def create_instances(module, gce, instance_names): module.fail_json(msg='Missing required create instance variable', changed=False) - for name in instance_names: - pd = None - if lc_disks: - pd = lc_disks[0] - elif persistent_boot_disk: + gce_args = dict( + location=lc_zone, + ex_network=network, ex_tags=tags, ex_metadata=metadata, + ex_can_ip_forward=ip_forward, + external_ip=instance_external_ip, ex_disk_auto_delete=disk_auto_delete, + ex_service_accounts=ex_sa_perms + ) + if preemptible is not None: + gce_args['ex_preemptible'] = preemptible + if subnetwork is not None: + gce_args['ex_subnetwork'] = subnetwork + + if isinstance(instance_names, str) and not number: + instance_names = [instance_names] + + if isinstance(instance_names, str) and number: + instance_responses = gce.ex_create_multiple_nodes(instance_names, lc_machine_type, + lc_image(), number, **gce_args) + for resp in instance_responses: + n = resp + if isinstance(resp, libcloud.compute.drivers.gce.GCEFailedNode): + try: + n = gce.ex_get_node(n.name, lc_zone) + except ResourceNotFoundError: + pass + else: + # Assure that at least one node has been created to set changed=True + changed = True + new_instances.append(n) + else: + for instance in instance_names: + pd = None + if lc_disks: + pd = lc_disks[0] + elif persistent_boot_disk: + try: + pd = gce.ex_get_volume("%s" % instance, lc_zone) + except ResourceNotFoundError: + pd = gce.create_volume(None, "%s" % instance, image=lc_image()) + gce_args['ex_boot_disk'] = pd + + inst = None try: - pd = gce.ex_get_volume("%s" % name, lc_zone) + inst = gce.ex_get_node(instance, lc_zone) except ResourceNotFoundError: - pd = gce.create_volume(None, "%s" % name, image=lc_image()) - - gce_args = dict( - location=lc_zone, - ex_network=network, ex_tags=tags, ex_metadata=metadata, - ex_boot_disk=pd, ex_can_ip_forward=ip_forward, - external_ip=instance_external_ip, ex_disk_auto_delete=disk_auto_delete, - ex_service_accounts=ex_sa_perms - ) - if preemptible is not None: - gce_args['ex_preemptible'] = preemptible - if subnetwork is not None: - gce_args['ex_subnetwork'] = subnetwork - - inst = None - try: - inst = gce.ex_get_node(name, lc_zone) - except ResourceNotFoundError: - inst = gce.create_node( - name, lc_machine_type, lc_image(), **gce_args - ) - changed = True - except GoogleBaseError as e: - module.fail_json(msg='Unexpected error attempting to create ' + - 'instance %s, error: %s' % (name, e.value)) + inst = gce.create_node( + instance, lc_machine_type, lc_image(), **gce_args + ) + changed = True + except GoogleBaseError as e: + module.fail_json(msg='Unexpected error attempting to create ' + + 'instance %s, error: %s' % (instance, e.value)) + if inst: + new_instances.append(inst) + for inst in new_instances: for i, lc_disk in enumerate(lc_disks): # Check whether the disk is already attached if (len(inst.extra['disks']) > i): @@ -502,9 +532,6 @@ def create_instances(module, gce, instance_names): inst.extra['disks'].append( {'source': lc_disk.extra['selfLink'], 'index': i}) - if inst: - new_instances.append(inst) - instance_names = [] instance_json_data = [] for inst in new_instances: @@ -514,7 +541,7 @@ def create_instances(module, gce, instance_names): return (changed, instance_json_data, instance_names) -def change_instance_state(module, gce, instance_names, zone_name, state): +def change_instance_state(module, gce, instance_names, number, zone_name, state): """Changes the state of a list of instances. For example, change from started to stopped, or started to absent. @@ -528,31 +555,46 @@ def change_instance_state(module, gce, instance_names, zone_name, state): """ changed = False - changed_instance_names = [] - for name in instance_names: + nodes = [] + state_instance_names = [] + + if isinstance(instance_names, str) and number: + node_names = ['%s-%03d' % (instance_names, i) for i in range(number)] + elif isinstance(instance_names, str) and not number: + node_names = [instance_names] + else: + node_names = instance_names + + for name in node_names: inst = None try: inst = gce.ex_get_node(name, zone_name) except ResourceNotFoundError: - pass + state_instance_names.append(name) except Exception as e: module.fail_json(msg=unexpected_error_msg(e), changed=False) - if inst and state in ['absent', 'deleted']: - gce.destroy_node(inst) - changed_instance_names.append(inst.name) - changed = True - elif inst and state == 'started' and \ - inst.state == libcloud.compute.types.NodeState.STOPPED: - gce.ex_start_node(inst) - changed_instance_names.append(inst.name) - changed = True - elif inst and state in ['stopped', 'terminated'] and \ - inst.state == libcloud.compute.types.NodeState.RUNNING: - gce.ex_stop_node(inst) - changed_instance_names.append(inst.name) - changed = True - - return (changed, changed_instance_names) + else: + nodes.append(inst) + state_instance_names.append(name) + + if state in ['absent', 'deleted'] and number: + changed_nodes = gce.ex_destroy_multiple_nodes(nodes) or [False] + changed = reduce(lambda x, y: x or y, changed_nodes) + else: + for node in nodes: + if state in ['absent', 'deleted']: + gce.destroy_node(node) + changed = True + elif state == 'started' and \ + node.state == libcloud.compute.types.NodeState.STOPPED: + gce.ex_start_node(node) + changed = True + elif state in ['stopped', 'terminated'] and \ + node.state == libcloud.compute.types.NodeState.RUNNING: + gce.ex_stop_node(node) + changed = True + + return (changed, state_instance_names) def main(): module = AnsibleModule( @@ -561,7 +603,8 @@ def main(): instance_names = dict(), machine_type = dict(default='n1-standard-1'), metadata = dict(), - name = dict(), + name = dict(aliases=['base_name']), + num_instances = dict(type='int'), network = dict(default='default'), subnetwork = dict(), persistent_boot_disk = dict(type='bool', default=False), @@ -580,7 +623,8 @@ def main(): external_ip=dict(default='ephemeral'), disk_auto_delete = dict(type='bool', default=True), preemptible = dict(type='bool', default=None), - ) + ), + mutually_exclusive=[('instance_names', 'name')] ) if not HAS_PYTHON26: @@ -595,6 +639,7 @@ def main(): machine_type = module.params.get('machine_type') metadata = module.params.get('metadata') name = module.params.get('name') + number = module.params.get('num_instances') network = module.params.get('network') subnetwork = module.params.get('subnetwork') persistent_boot_disk = module.params.get('persistent_boot_disk') @@ -605,13 +650,13 @@ def main(): preemptible = module.params.get('preemptible') changed = False - inames = [] + inames = None if isinstance(instance_names, list): inames = instance_names elif isinstance(instance_names, str): inames = instance_names.split(',') if name: - inames.append(name) + inames = name if not inames: module.fail_json(msg='Must specify a "name" or "instance_names"', changed=False) @@ -629,20 +674,20 @@ def main(): json_output = {'zone': zone} if state in ['absent', 'deleted', 'started', 'stopped', 'terminated']: json_output['state'] = state - (changed, changed_instance_names) = change_instance_state( - module, gce, inames, zone, state) + (changed, state_instance_names) = change_instance_state( + module, gce, inames, number, zone, state) # based on what user specified, return the same variable, although # value could be different if an instance could not be destroyed - if instance_names: - json_output['instance_names'] = changed_instance_names + if instance_names or name and number: + json_output['instance_names'] = state_instance_names elif name: json_output['name'] = name elif state in ['active', 'present']: json_output['state'] = 'present' (changed, instance_data, instance_name_list) = create_instances( - module, gce, inames) + module, gce, inames, number) json_output['instance_data'] = instance_data if instance_names: json_output['instance_names'] = instance_name_list From 2cb41ce880f26df7eafe691c8c492df9c27b9e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Fri, 21 Oct 2016 08:46:59 +0200 Subject: [PATCH 556/770] doc: add_host: add example without deprecated vars (#5323) ansible_ssh_* are deprecated since 2.0 --- inventory/add_host.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/inventory/add_host.py b/inventory/add_host.py index ef01ed1051b..d2873e28f85 100644 --- a/inventory/add_host.py +++ b/inventory/add_host.py @@ -37,7 +37,7 @@ notes: - This module bypasses the play host loop and only runs once for all the hosts in the play, if you need it to iterate use a with\_ directive. -author: +author: - "Ansible Core Team" - "Seth Vidal" ''' @@ -49,8 +49,13 @@ # add a host with a non-standard port local to your machines - add_host: name={{ new_ip }}:{{ new_port }} -# add a host alias that we reach through a tunnel +# add a host alias that we reach through a tunnel (Ansible <= 1.9) - add_host: hostname={{ new_ip }} ansible_ssh_host={{ inventory_hostname }} ansible_ssh_port={{ new_port }} + +# add a host alias that we reach through a tunnel (Ansible >= 2.0) +- add_host: hostname={{ new_ip }} + ansible_host={{ inventory_hostname }} + ansible_port={{ new_port }} ''' From 14185067d7fd83050f5370cc571e27e27e2ad939 Mon Sep 17 00:00:00 2001 From: Robin Roth Date: Fri, 21 Oct 2016 16:48:21 +0200 Subject: [PATCH 557/770] Fix git failure for use of depth with version (#5135) * Fixes #5108 * before module fails with "fatal: A branch named 'STABLE' already exists." when depth is used on a fresh clone with a non-HEAD branch --- source_control/git.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index 41c29a4ca4c..42ee9393a60 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -756,11 +756,12 @@ def switch_version(git_path, module, dest, remote, version, verify_commit, depth else: # FIXME check for local_branch first, should have been fetched already if is_remote_branch(git_path, module, dest, remote, version): + if depth and not is_local_branch(git_path, module, dest, version): + # git clone --depth implies --single-branch, which makes + # the checkout fail if the version changes + # fetch the remote branch, to be able to check it out next + set_remote_branch(git_path, module, dest, remote, version, depth) if not is_local_branch(git_path, module, dest, version): - if depth: - # git clone --depth implies --single-branch, which makes - # the checkout fail if the version changes - set_remote_branch(git_path, module, dest, remote, version, depth) cmd = "%s checkout --track -b %s %s/%s" % (git_path, version, remote, version) else: (rc, out, err) = module.run_command("%s checkout --force %s" % (git_path, version), cwd=dest) From a3d4d74d528753b23b9a875040c94bf622d1eb5c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 21 Oct 2016 00:07:50 -0700 Subject: [PATCH 558/770] Fix git for py3 Comparing to the output of run_command() needs to use native strings Also fix imports: We were relying on them coming from the import of basic. A few (like yaml) weren't imported at all. --- source_control/git.py | 54 ++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 24 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index 42ee9393a60..15dda627cd8 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -21,7 +21,7 @@ DOCUMENTATION = ''' --- module: git -author: +author: - "Ansible Core Team" - "Michael DeHaan" version_added: "0.0.1" @@ -46,8 +46,8 @@ default: "HEAD" description: - What version of the repository to check out. This can be the - the literal string C(HEAD), a branch name, a tag name. - It can also be a I(SHA-1) hash, in which case C(refspec) needs + the literal string C(HEAD), a branch name, a tag name. + It can also be a I(SHA-1) hash, in which case C(refspec) needs to be specified if the given revision is not already available. accept_hostkey: required: false @@ -55,8 +55,8 @@ choices: [ "yes", "no" ] version_added: "1.5" description: - - if C(yes), adds the hostkey for the repo url if not already - added. If ssh_opts contains "-o StrictHostKeyChecking=no", + - if C(yes), adds the hostkey for the repo url if not already + added. If ssh_opts contains "-o StrictHostKeyChecking=no", this parameter is ignored. ssh_opts: required: false @@ -186,9 +186,9 @@ notes: - "If the task seems to be hanging, first verify remote host is in C(known_hosts). - SSH will prompt user to authorize the first contact with a remote host. To avoid this prompt, - one solution is to use the option accept_hostkey. Another solution is to - add the remote host public key in C(/etc/ssh/ssh_known_hosts) before calling + SSH will prompt user to authorize the first contact with a remote host. To avoid this prompt, + one solution is to use the option accept_hostkey. Another solution is to + add the remote host public key in C(/etc/ssh/ssh_known_hosts) before calling the git module, with the following command: ssh-keyscan -H remote_host.com >> /etc/ssh/ssh_known_hosts." ''' @@ -214,12 +214,18 @@ import os import re +import shlex +import stat +import sys import tempfile from distutils.version import LooseVersion +from ansible.module_utils.basic import AnsibleModule, get_module_path +from ansible.module_utils.known_hosts import add_git_host_key from ansible.module_utils.six import string_types from ansible.module_utils._text import to_bytes, to_native + def head_splitter(headfile, remote, module=None, fail_on_error=False): '''Extract the head reference''' # https://github.com/ansible/ansible-modules-core/pull/907 @@ -253,13 +259,13 @@ def unfrackgitpath(path): def get_submodule_update_params(module, git_path, cwd): - #or: git submodule [--quiet] update [--init] [-N|--no-fetch] - #[-f|--force] [--rebase] [--reference ] [--merge] + #or: git submodule [--quiet] update [--init] [-N|--no-fetch] + #[-f|--force] [--rebase] [--reference ] [--merge] #[--recursive] [--] [...] params = [] - # run a bad submodule command to get valid params + # run a bad submodule command to get valid params cmd = "%s submodule update --help" % (git_path) rc, stdout, stderr = module.run_command(cmd, cwd=cwd) lines = stderr.split('\n') @@ -272,7 +278,7 @@ def get_submodule_update_params(module, git_path, cwd): update_line = update_line.replace(']','') update_line = update_line.replace('|',' ') parts = shlex.split(update_line) - for part in parts: + for part in parts: if part.startswith('--'): part = part.replace('--', '') params.append(part) @@ -320,7 +326,7 @@ def set_git_ssh(ssh_wrapper, key_file, ssh_opts): del os.environ["GIT_KEY"] if key_file: - os.environ["GIT_KEY"] = key_file + os.environ["GIT_KEY"] = key_file if os.environ.get("GIT_SSH_OPTS"): del os.environ["GIT_SSH_OPTS"] @@ -420,7 +426,7 @@ def reset(git_path, module, dest): def get_diff(module, git_path, dest, repo, remote, depth, bare, before, after): ''' Return the difference between 2 versions ''' - if before == None: + if before is None: return { 'prepared': '>> Newly checked out %s' % after } elif before != after: # Ensure we have the object we are referring to during git diff ! @@ -469,7 +475,7 @@ def get_remote_head(git_path, module, dest, version, remote, bare): out = to_native(out) if tag: - # Find the dereferenced tag if this is an annotated tag. + # Find the dereferenced tag if this is an annotated tag. for tag in out.split('\n'): if tag.endswith(version + '^{}'): out = tag @@ -483,7 +489,7 @@ def get_remote_head(git_path, module, dest, version, remote, bare): def is_remote_tag(git_path, module, dest, remote, version): cmd = '%s ls-remote %s -t refs/tags/%s' % (git_path, remote, version) (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) - if to_bytes(version, errors='surrogate_or_strict') in out: + if to_native(version, errors='surrogate_or_strict') in out: return True else: return False @@ -550,6 +556,10 @@ def get_head_branch(git_path, module, dest, remote, bare=False): # Check if the .git is a file. If it is a file, it means that we are in a submodule structure. if os.path.isfile(repo_path): try: + ### FIXME: This introduces another dep that we don't want. We + # probably need to take a look at the format of this file and do + # our own parsing. + import yaml gitdir = yaml.safe_load(open(repo_path)).get('gitdir') # There is a possibility the .git file to have an absolute path. if os.path.isabs(gitdir): @@ -602,7 +612,6 @@ def fetch(git_path, module, repo, dest, version, remote, depth, bare, refspec, g fetch_str = 'download remote objects and refs' fetch_cmd = [git_path, 'fetch'] - refspecs = [] if depth: # try to find the minimal set of refs we need to fetch to get a @@ -674,7 +683,7 @@ def submodules_fetch(git_path, module, remote, track_submodules, dest): if line.strip().startswith('url'): repo = line.split('=', 1)[1].strip() if module.params['ssh_opts'] is not None: - if not "-o StrictHostKeyChecking=no" in module.params['ssh_opts']: + if "-o StrictHostKeyChecking=no" not in module.params['ssh_opts']: add_git_host_key(module, repo, accept_hostkey=module.params['accept_hostkey']) else: add_git_host_key(module, repo, accept_hostkey=module.params['accept_hostkey']) @@ -859,7 +868,7 @@ def main(): result = dict( warnings=list() ) # evaluate and set the umask before doing anything else - if umask != None: + if umask is not None: if not isinstance(umask, string_types): module.fail_json(msg="umask must be defined as a quoted octal integer") try: @@ -897,9 +906,9 @@ def main(): set_git_ssh(ssh_wrapper, key_file, ssh_opts) module.add_cleanup_file(path=ssh_wrapper) - # add the git repo's hostkey + # add the git repo's hostkey if module.params['ssh_opts'] is not None: - if not "-o StrictHostKeyChecking=no" in module.params['ssh_opts']: + if "-o StrictHostKeyChecking=no" not in module.params['ssh_opts']: add_git_host_key(module, repo, accept_hostkey=module.params['accept_hostkey']) else: add_git_host_key(module, repo, accept_hostkey=module.params['accept_hostkey']) @@ -1020,9 +1029,6 @@ def main(): module.exit_json(**result) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.known_hosts import * if __name__ == '__main__': main() From ce44f0a0ae6f3584ea392bbaa441433a3c8f7465 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 21 Oct 2016 08:30:43 -0700 Subject: [PATCH 559/770] Remove the yaml dep from the git module --- source_control/git.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index 15dda627cd8..a9952172272 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -222,7 +222,7 @@ from ansible.module_utils.basic import AnsibleModule, get_module_path from ansible.module_utils.known_hosts import add_git_host_key -from ansible.module_utils.six import string_types +from ansible.module_utils.six import b, string_types from ansible.module_utils._text import to_bytes, to_native @@ -536,8 +536,8 @@ def is_local_branch(git_path, module, dest, branch): def is_not_a_branch(git_path, module, dest): branches = get_branches(git_path, module, dest) - for b in branches: - if b.startswith('* ') and ('no branch' in b or 'detached from' in b): + for branch in branches: + if branch.startswith('* ') and ('no branch' in branch or 'detached from' in branch): return True return False @@ -556,17 +556,23 @@ def get_head_branch(git_path, module, dest, remote, bare=False): # Check if the .git is a file. If it is a file, it means that we are in a submodule structure. if os.path.isfile(repo_path): try: - ### FIXME: This introduces another dep that we don't want. We - # probably need to take a look at the format of this file and do - # our own parsing. - import yaml - gitdir = yaml.safe_load(open(repo_path)).get('gitdir') + git_conf = open(repo_path, 'rb') + for line in git_conf: + config_val = line.split(b(':'), 1) + if config_val[0].strip() == b('gitdir'): + gitdir = to_native(config_val[1].strip(), errors='surrogate_or_strict') + break + else: + # No repo path found + return '' + # There is a possibility the .git file to have an absolute path. if os.path.isabs(gitdir): repo_path = gitdir else: repo_path = os.path.join(repo_path.split('.git')[0], gitdir) except (IOError, AttributeError): + # No repo path found return '' # Read .git/HEAD for the name of the branch. # If we're in a detached HEAD state, look up the branch associated with From 8d3a34ced677e065b6aebcef91bcd4afc521555f Mon Sep 17 00:00:00 2001 From: Adrian Likins Date: Fri, 21 Oct 2016 12:28:28 -0400 Subject: [PATCH 560/770] Make authorized_key preserve key order (#5339) * Make authorized_key preserve key order Track the ordering of keys in the original file (rank) and try to preserve it when writing out updates. Fixes #4780 --- system/authorized_key.py | 70 +++++++++++++++++++++++++++++----------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index fb82579b864..084ce95f186 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -149,6 +149,7 @@ import tempfile import re import shlex +from operator import itemgetter from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.pycompat24 import get_exception @@ -303,10 +304,13 @@ def parseoptions(module, options): return options_dict -def parsekey(module, raw_key): +def parsekey(module, raw_key, rank=None): ''' parses a key, which may or may not contain a list of ssh-key options at the beginning + + rank indicates the keys original ordering, so that + it can be written out in the same order. ''' VALID_SSH2_KEY_TYPES = [ @@ -333,6 +337,10 @@ def parsekey(module, raw_key): lex.whitespace_split = True key_parts = list(lex) + if key_parts and key_parts[0] == '#': + # comment line, invalid line, etc. + return (raw_key, 'skipped', None, None, rank) + for i in range(0, len(key_parts)): if key_parts[i] in VALID_SSH2_KEY_TYPES: type_index = i @@ -355,7 +363,7 @@ def parsekey(module, raw_key): if len(key_parts) > (type_index + 1): comment = " ".join(key_parts[(type_index + 2):]) - return (key, key_type, options, comment) + return (key, key_type, options, comment, rank) def readkeys(module, filename): @@ -364,15 +372,15 @@ def readkeys(module, filename): keys = {} f = open(filename) - for line in f.readlines(): - key_data = parsekey(module, line) + for rank_index, line in enumerate(f.readlines()): + key_data = parsekey(module, line, rank=rank_index) if key_data: # use key as identifier keys[key_data[0]] = key_data else: - # for an invalid line, just append the line - # to the array so it will be re-output later - keys[line] = line + # for an invalid line, just set the line + # dict key to the line so it will be re-output later + keys[line] = (line, 'skipped', None, None, rank_index) f.close() return keys @@ -380,10 +388,17 @@ def writekeys(module, filename, keys): fd, tmp_path = tempfile.mkstemp('', 'tmp', os.path.dirname(filename)) f = open(tmp_path,"w") + + # FIXME: only the f.writelines() needs to be in try clause try: - for index, key in keys.items(): + new_keys = keys.values() + # order the new_keys by their original ordering, via the rank item in the tuple + ordered_new_keys = sorted(new_keys, key=itemgetter(4)) + + for key in ordered_new_keys: try: - (keyhash,type,options,comment) = key + (keyhash, key_type, options, comment, rank) = key + option_str = "" if options: option_strings = [] @@ -394,7 +409,15 @@ def writekeys(module, filename, keys): option_strings.append("%s=%s" % (option_key, value)) option_str = ",".join(option_strings) option_str += " " - key_line = "%s%s %s %s\n" % (option_str, type, keyhash, comment) + + # comment line or invalid line, just leave it + if not key_type: + key_line = key + + if key_type == 'skipped': + key_line = key[0] + else: + key_line = "%s%s %s %s\n" % (option_str, key_type, keyhash, comment) except: key_line = key f.writelines(key_line) @@ -430,43 +453,47 @@ def enforce_state(module, params): module.fail_json(msg=error_msg % key) # extract individual keys into an array, skipping blank lines and comments - key = [s for s in key.splitlines() if s and not s.startswith('#')] + new_keys = [s for s in key.splitlines() if s and not s.startswith('#')] # check current state -- just get the filename, don't create file do_write = False params["keyfile"] = keyfile(module, user, do_write, path, manage_dir) existing_keys = readkeys(module, params["keyfile"]) - # Add a place holder for keys that should exist in the state=present and # exclusive=true case keys_to_exist = [] + # we will order any non exclusive new keys higher than all the existing keys, + # resulting in the new keys being written to the key file after existing keys, but + # in the order of new_keys + max_rank_of_existing_keys = len(existing_keys) + # Check our new keys, if any of them exist we'll continue. - for new_key in key: - parsed_new_key = parsekey(module, new_key) + for rank_index, new_key in enumerate(new_keys): + parsed_new_key = parsekey(module, new_key, rank=rank_index) if not parsed_new_key: module.fail_json(msg="invalid key specified: %s" % new_key) if key_options is not None: parsed_options = parseoptions(module, key_options) - parsed_new_key = (parsed_new_key[0], parsed_new_key[1], parsed_options, parsed_new_key[3]) + # rank here is the rank in the provided new keys, which may be unrelated to rank in existing_keys + parsed_new_key = (parsed_new_key[0], parsed_new_key[1], parsed_options, parsed_new_key[3], parsed_new_key[4]) matched = False non_matching_keys = [] if parsed_new_key[0] in existing_keys: - # Then we check if everything matches, including + # Then we check if everything (except the rank at index 4) matches, including # the key type and options. If not, we append this # existing key to the non-matching list # We only want it to match everything when the state # is present - if parsed_new_key != existing_keys[parsed_new_key[0]] and state == "present": + if parsed_new_key[:4] != existing_keys[parsed_new_key[0]][:4] and state == "present": non_matching_keys.append(existing_keys[parsed_new_key[0]]) else: matched = True - # handle idempotent state=present if state=="present": keys_to_exist.append(parsed_new_key[0]) @@ -476,8 +503,12 @@ def enforce_state(module, params): del existing_keys[non_matching_key[0]] do_write = True + # new key that didn't exist before. Where should it go in the ordering? if not matched: - existing_keys[parsed_new_key[0]] = parsed_new_key + # We want the new key to be after existing keys if not exclusive (rank > max_rank_of_existing_keys) + total_rank = max_rank_of_existing_keys + parsed_new_key[4] + # replace existing key tuple with new parsed key with its total rank + existing_keys[parsed_new_key[0]] = (parsed_new_key[0], parsed_new_key[1], parsed_new_key[2], parsed_new_key[3], total_rank) do_write = True elif state=="absent": @@ -487,6 +518,7 @@ def enforce_state(module, params): do_write = True # remove all other keys to honor exclusive + # for 'exclusive', make sure keys are written in the order the new keys were if state == "present" and exclusive: to_remove = frozenset(existing_keys).difference(keys_to_exist) for key in to_remove: From 38b5a66b7af16829d18700fe98ea906f9ce26e1d Mon Sep 17 00:00:00 2001 From: Timothy Appnel Date: Fri, 21 Oct 2016 18:32:54 -0400 Subject: [PATCH 561/770] clarifies synchronize module on use of --delayed-updates --- files/synchronize.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/files/synchronize.py b/files/synchronize.py index ff69a2a67c6..b48a1f1a25f 100644 --- a/files/synchronize.py +++ b/files/synchronize.py @@ -20,9 +20,9 @@ --- module: synchronize version_added: "1.4" -short_description: Uses rsync to make synchronizing file paths in your playbooks quick and easy. +short_description: A wrapper around rsync to make common tasks in your playbooks quick and easy. description: - - C(synchronize) is a wrapper around the rsync command, meant to make common tasks with rsync easier. It is run and originates on the local host where Ansible is being run. Of course, you could just use the command action to call rsync yourself, but you also have to add a fair number of boilerplate options and host facts. You `still` may need to call rsync directly via C(command) or C(shell) depending on your use case. C(synchronize) does not provide access to the full power of rsync, but does make most invocations easier to follow. + - C(synchronize) is a wrapper around rsync to make common tasks in your playbooks quick and easy. It is run and originates on the local host where Ansible is being run. Of course, you could just use the C(command) action to call rsync yourself, but you also have to add a fair number of boilerplate options and host facts. C(synchronize) is not intended to provide access to the full power of rsync, but does make the most common invocations easier to implement. You `still` may need to call rsync directly via C(command) or C(shell) depending on your use case. options: src: description: @@ -130,7 +130,7 @@ required: false rsync_timeout: description: - - Specify a --timeout for the rsync command in seconds. + - Specify a --timeout for the rsync command in seconds. default: 0 required: false set_remote_user: @@ -174,11 +174,11 @@ - Expect that dest=~/x will be ~/x even if using sudo. - Inspect the verbose output to validate the destination user/host/path are what was expected. - - To exclude files and directories from being synchronized, you may add + - To exclude files and directories from being synchronized, you may add C(.rsync-filter) files to the source directory. - rsync daemon must be up and running with correct permission when using rsync protocol in source or destination path. - + - The C(synchronize) module forces `--delay-updates` to avoid leaving a destination in a broken in-between state if the underlying rsync process encounters an error. Those synchronizing large numbers of files that are willing to trade safety for performance should call rsync directly. author: "Timothy Appnel (@tima)" ''' @@ -397,7 +397,7 @@ def main(): if private_key is None: private_key = '' else: - private_key = '-i '+ private_key + private_key = '-i '+ private_key ssh_opts = '-S none' @@ -433,9 +433,9 @@ def main(): # expand the paths if '@' not in source: - source = os.path.expanduser(source) + source = os.path.expanduser(source) if '@' not in dest: - dest = os.path.expanduser(dest) + dest = os.path.expanduser(dest) cmd = ' '.join([cmd, source, dest]) cmdstr = cmd @@ -446,7 +446,7 @@ def main(): changed = changed_marker in out out_clean=out.replace(changed_marker,'') out_lines=out_clean.split('\n') - while '' in out_lines: + while '' in out_lines: out_lines.remove('') if module._diff: diff = {'prepared': out_clean} @@ -461,4 +461,3 @@ def main(): from ansible.module_utils.basic import * main() - From 952ad58bda62c4014f6c5796b4750cb4467461f9 Mon Sep 17 00:00:00 2001 From: Charles Zaffery Date: Fri, 21 Oct 2016 12:55:14 -0700 Subject: [PATCH 562/770] Remove line when 'state: absent' with 'option:' instead of commenting --- files/ini_file.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index b66acd7e355..68d72ca9ba2 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -186,10 +186,10 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese else: index = index + 1 break - else: - # comment out the existing option line + elif state == 'absent': + # delete the existing line if match_active_opt(option, line): - ini_lines[index] = '#%s' % ini_lines[index] + del ini_lines[index] changed = True break From 66d47c8149d84e52f64b7c4d1f340d45dca94d9c Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 22 Oct 2016 08:55:34 -0700 Subject: [PATCH 563/770] Only change to short IDs for delete (#5353) * Only change to short IDs for delete If the user specifies long IDs, use them for all commands except for deleting a key. Need to use short IDs there because of an upstream apt_key bug. Fixed in apt_key 1.10 (fix is present in Ubuntu 16.04 but not Ubuntu 14.0 or some Debians). Fixes #5237 * Check that apt-key really erased the key When erasing a key, apt-key does not understand how to process subkeys. This update explicitly checks that the key_id is no longer present and throws an error if it is. It also hints at subkeys being a possible problem in the error message and the documentation. Fixes #5119 * Fix apt_key check mode with long ids apt-key can be given a key id longer than 16 chars to more accurately define what key to download. However, we can use a maximum of 16 chars to verify whether a key is installed or not. So we need to use different lengths for the id depending on what we're doing with it. Fixes #2622 Also: * Some style cleanups * Use get_bin_path to find the path to apt-key and then use that when invoking apt-key * Return a nice user error message if the key was not found on the keyserver * Make file and keyring parameters type='path' so envars and tilde are expanded --- packaging/os/apt_key.py | 187 ++++++++++++++++++++++++++-------------- 1 file changed, 120 insertions(+), 67 deletions(-) diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index 129c467e7f7..656f6e46640 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -37,16 +37,17 @@ default: none description: - identifier of key. Including this allows check mode to correctly report the changed state. + - "If specifying a subkey's id be aware that apt-key does not understand how to remove keys via a subkey id. Specify the primary key's id instead." data: required: false default: none description: - - keyfile contents + - keyfile contents to add to the keyring file: required: false default: none description: - - keyfile path + - path to a keyfile to add to the keyring keyring: required: false default: none @@ -106,29 +107,69 @@ # FIXME: standardize into module_common from traceback import format_exc -from re import compile as re_compile -# FIXME: standardize into module_common -from distutils.spawn import find_executable -from os import environ -from sys import exc_info -import traceback -match_key = re_compile("^gpg:.*key ([0-9a-fA-F]+):.*$") +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils._text import to_native +from ansible.module_utils.urls import fetch_url + + +apt_key_bin = None + + +def find_needed_binaries(module): + global apt_key_bin + + apt_key_bin = module.get_bin_path('apt-key', required=True) + + ### FIXME: Is there a reason that gpg and grep are checked? Is it just + # cruft or does the apt .deb package not require them (and if they're not + # installed, /usr/bin/apt-key fails?) + module.get_bin_path('gpg', required=True) + module.get_bin_path('grep', required=True) + + +def parse_key_id(key_id): + """validate the key_id and break it into segments + + :arg key_id: The key_id as supplied by the user. A valid key_id will be + 8, 16, or more hexadecimal chars with an optional leading ``0x``. + :returns: The portion of key_id suitable for apt-key del, the portion + suitable for comparisons with --list-public-keys, and the portion that + can be used with --recv-key. If key_id is long enough, these will be + the last 8 characters of key_id, the last 16 characters, and all of + key_id. If key_id is not long enough, some of the values will be the + same. + + * apt-key del <= 1.10 has a bug with key_id != 8 chars + * apt-key adv --list-public-keys prints 16 chars + * apt-key adv --recv-key can take more chars + + """ + # Make sure the key_id is valid hexadecimal + int(key_id, 16) + + key_id = key_id.upper() + if key_id.startswith('0X'): + key_id = key_id[2:] + + key_id_len = len(key_id) + if (key_id_len != 8 and key_id_len != 16) and key_id_len <= 16: + raise ValueError('key_id must be 8, 16, or 16+ hexadecimal characters in length') -REQUIRED_EXECUTABLES=['gpg', 'grep', 'apt-key'] + short_key_id = key_id[-8:] + fingerprint = key_id + if key_id_len > 16: + fingerprint = key_id[-16:] -def check_missing_binaries(module): - missing = [e for e in REQUIRED_EXECUTABLES if not find_executable(e)] - if len(missing): - module.fail_json(msg="binaries are missing", names=missing) + return short_key_id, fingerprint, key_id def all_keys(module, keyring, short_format): if keyring: - cmd = "apt-key --keyring %s adv --list-public-keys --keyid-format=long" % keyring + cmd = "%s --keyring %s adv --list-public-keys --keyid-format=long" % (apt_key_bin, keyring) else: - cmd = "apt-key adv --list-public-keys --keyid-format=long" + cmd = "%s adv --list-public-keys --keyid-format=long" % apt_key_bin (rc, out, err) = module.run_command(cmd) results = [] lines = to_native(out).split('\n') @@ -172,32 +213,37 @@ def download_key(module, url): def import_key(module, keyring, keyserver, key_id): if keyring: - cmd = "apt-key --keyring %s adv --keyserver %s --recv %s" % (keyring, keyserver, key_id) + cmd = "%s --keyring %s adv --keyserver %s --recv %s" % (apt_key_bin, keyring, keyserver, key_id) else: - cmd = "apt-key adv --keyserver %s --recv %s" % (keyserver, key_id) + cmd = "%s adv --keyserver %s --recv %s" % (apt_key_bin, keyserver, key_id) for retry in range(5): - (rc, out, err) = module.run_command(cmd) + lang_env = dict(LANG='C', LC_ALL='C', LC_MESSAGES='C') + (rc, out, err) = module.run_command(cmd, environ_update=lang_env) if rc == 0: break else: # Out of retries - module.fail_json(cmd=cmd, msg="error fetching key from keyserver: %s" % keyserver, - rc=rc, stdout=out, stderr=err) + if rc == 2 and 'not found on keyserver' in out: + msg = 'Key %s not found on keyserver %s' % (key_id, keyserver) + module.fail_json(cmd=cmd, msg=msg) + else: + msg = "Error fetching key %s from keyserver: %s" % (key_id, keyserver) + module.fail_json(cmd=cmd, msg=msg, rc=rc, stdout=out, stderr=err) return True def add_key(module, keyfile, keyring, data=None): if data is not None: if keyring: - cmd = "apt-key --keyring %s add -" % keyring + cmd = "%s --keyring %s add -" % (apt_key_bin, keyring) else: - cmd = "apt-key add -" + cmd = "%s add -" % apt_key_bin (rc, out, err) = module.run_command(cmd, data=data, check_rc=True, binary_data=True) else: if keyring: - cmd = "apt-key --keyring %s add %s" % (keyring, keyfile) + cmd = "%s --keyring %s add %s" % (apt_key_bin, keyring, keyfile) else: - cmd = "apt-key add %s" % (keyfile) + cmd = "%s add %s" % (apt_key_bin, keyfile) (rc, out, err) = module.run_command(cmd, check_rc=True) return True @@ -205,9 +251,9 @@ def add_key(module, keyfile, keyring, data=None): def remove_key(module, key_id, keyring): # FIXME: use module.run_command, fail at point of error and don't discard useful stdin/stdout if keyring: - cmd = 'apt-key --keyring %s del %s' % (keyring, key_id) + cmd = '%s --keyring %s del %s' % (apt_key_bin, keyring, key_id) else: - cmd = 'apt-key del %s' % key_id + cmd = '%s del %s' % (apt_key_bin, key_id) (rc, out, err) = module.run_command(cmd, check_rc=True) return True @@ -218,14 +264,15 @@ def main(): id=dict(required=False, default=None), url=dict(required=False), data=dict(required=False), - file=dict(required=False), + file=dict(required=False, type='path'), key=dict(required=False), - keyring=dict(required=False), + keyring=dict(required=False, type='path'), validate_certs=dict(default='yes', type='bool'), keyserver=dict(required=False), state=dict(required=False, choices=['present', 'absent'], default='present') ), - supports_check_mode=True + supports_check_mode=True, + mutually_exclusive=(('filename', 'keyserver', 'data', 'url'),), ) key_id = module.params['id'] @@ -237,64 +284,70 @@ def main(): keyserver = module.params['keyserver'] changed = False - # we use the "short" id: key_id[-8:], short_format=True - # it's a workaround for https://bugs.launchpad.net/ubuntu/+source/apt/+bug/1481871 - + fingerprint = short_key_id = key_id + short_format = False if key_id: try: - _ = int(key_id, 16) - if key_id.startswith('0x'): - key_id = key_id[2:] - key_id = key_id.upper()[-8:] + key_id, fingerprint, short_key_id = parse_key_id(key_id) except ValueError: - module.fail_json(msg="Invalid key_id", id=key_id) + module.fail_json(msg='Invalid key_id', id=key_id) - # FIXME: I think we have a common facility for this, if not, want - check_missing_binaries(module) + if len(fingerprint) == 8: + short_format = True + + find_needed_binaries(module) - short_format = True keys = all_keys(module, keyring, short_format) return_values = {} if state == 'present': - if key_id and key_id in keys: + if fingerprint and fingerprint in keys: module.exit_json(changed=False) + elif fingerprint and fingerprint not in keys and module.check_mode: + ### TODO: Someday we could go further -- write keys out to + # a temporary file and then extract the key id from there via gpg + # to decide if the key is installed or not. + module.exit_json(changed=True) else: if not filename and not data and not keyserver: data = download_key(module, url) - if key_id and key_id in keys: - module.exit_json(changed=False) + + if filename: + add_key(module, filename, keyring) + elif keyserver: + import_key(module, keyring, keyserver, key_id) else: - if module.check_mode: - module.exit_json(changed=True) - if filename: - add_key(module, filename, keyring) - elif keyserver: - import_key(module, keyring, keyserver, key_id) - else: - add_key(module, "-", keyring, data) - changed=False - keys2 = all_keys(module, keyring, short_format) - if len(keys) != len(keys2): - changed=True - if key_id and not key_id in keys2: - module.fail_json(msg="key does not seem to have been added", id=key_id) - module.exit_json(changed=changed) + add_key(module, "-", keyring, data) + + changed = False + keys2 = all_keys(module, keyring, short_format) + if len(keys) != len(keys2): + changed=True + + if fingerprint and fingerprint not in keys2: + module.fail_json(msg="key does not seem to have been added", id=key_id) + module.exit_json(changed=changed) + elif state == 'absent': if not key_id: module.fail_json(msg="key is required") - if key_id in keys: + if fingerprint in keys: if module.check_mode: module.exit_json(changed=True) - if remove_key(module, key_id, keyring): - changed=True + + # we use the "short" id: key_id[-8:], short_format=True + # it's a workaround for https://bugs.launchpad.net/ubuntu/+source/apt/+bug/1481871 + if remove_key(module, short_key_id, keyring): + keys = all_keys(module, keyring, short_format) + if fingerprint in keys: + module.fail_json(msg="apt-key del did not return an error but the key was not removed (check that the id is correct and *not* a subkey)", id=key_id) + changed = True else: - # FIXME: module.fail_json or exit-json immediately at point of failure + # FIXME: module.fail_json or exit-json immediately at point of failure module.fail_json(msg="error removing key_id", **return_values) module.exit_json(changed=changed, **return_values) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * -main() + +if __name__ == '__main__': + main() From ac314a5e3db5fc7d8a6953775f9b00c3ed4f5451 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 22 Oct 2016 08:36:29 -0700 Subject: [PATCH 564/770] Fix builddep when a source package exists without a binary package builddep only requires a source package to be in the repos but our code was checking for a binary package before running buiddep. Reversing the order makes it work correctly. Fixes #4519 --- packaging/os/apt.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index b58af6a7c4e..3983b6bd854 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -216,18 +216,22 @@ sample: "AH00558: apache2: Could not reliably determine the server's fully qualified domain name, using 127.0.1.1. Set the 'ServerName' directive globally to ..." ''' -import traceback # added to stave off future warnings about apt api import warnings warnings.filterwarnings('ignore', "apt API not stable yet", FutureWarning) -import os import datetime import fnmatch import itertools +import os +import re import sys +import time +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils._text import to_native +from ansible.module_utils.urls import fetch_url # APT related constants APT_ENV_VARS = dict( @@ -392,16 +396,19 @@ def expand_pkgspec_from_fnmatches(m, pkgspec, cache): if frozenset('*?[]!').intersection(pkgname_pattern): # handle multiarch pkgnames, the idea is that "apt*" should # only select native packages. But "apt*:i386" should still work - if not ":" in pkgname_pattern: + if ":" not in pkgname_pattern: + # Filter the multiarch packages from the cache only once try: pkg_name_cache = _non_multiarch except NameError: - pkg_name_cache = _non_multiarch = [pkg.name for pkg in cache if not ':' in pkg.name] + pkg_name_cache = _non_multiarch = [pkg.name for pkg in cache if ':' not in pkg.name] # noqa: F841 else: + # Create a cache of pkg_names including multiarch only once try: pkg_name_cache = _all_pkg_names except NameError: - pkg_name_cache = _all_pkg_names = [pkg.name for pkg in cache] + pkg_name_cache = _all_pkg_names = [pkg.name for pkg in cache] # noqa: F841 + matches = fnmatch.filter(pkg_name_cache, pkgname_pattern) if len(matches) == 0: @@ -445,12 +452,13 @@ def install(m, pkgspec, cache, upgrade=False, default_release=None, packages = "" pkgspec = expand_pkgspec_from_fnmatches(m, pkgspec, cache) for package in pkgspec: - name, version = package_split(package) - installed, upgradable, has_files = package_status(m, name, version, cache, state='install') if build_dep: # Let apt decide what to install pkg_list.append("'%s'" % package) continue + + name, version = package_split(package) + installed, upgradable, has_files = package_status(m, name, version, cache, state='install') if not installed or (upgrade and upgradable): pkg_list.append("'%s'" % package) if installed and upgradable and version: @@ -823,7 +831,6 @@ def main(): # reopen cache w/ modified config cache.open(progress=None) - mtimestamp, updated_cache_time = get_updated_cache_time() # Cache valid time is default 0, which will update the cache if # needed and `update_cache` was set to true @@ -923,9 +930,6 @@ def main(): except apt.cache.FetchFailedException: module.fail_json(msg="Could not fetch updated apt files") -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * if __name__ == "__main__": main() From 1182d1f0b76d56f3667e27987a10b9ec8f03357d Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sat, 22 Oct 2016 09:29:47 -0700 Subject: [PATCH 565/770] Order of return values was reversed --- packaging/os/apt_key.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index 656f6e46640..7822578d040 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -288,7 +288,7 @@ def main(): short_format = False if key_id: try: - key_id, fingerprint, short_key_id = parse_key_id(key_id) + short_key_id, fingerprint, key_id = parse_key_id(key_id) except ValueError: module.fail_json(msg='Invalid key_id', id=key_id) From 52e0536fae2ae8df752f41868ed50486b5653292 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Sat, 22 Oct 2016 13:21:20 -0400 Subject: [PATCH 566/770] refactor stat make format function 'format only' added platform dependant info, when it is available avoid rechecking same info added comments to each info gathering section (cherry picked from commit a79acf73d7eb79b76d808ff8a1d6c505dfd9ec82) --- files/stat.py | 103 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 62 insertions(+), 41 deletions(-) diff --git a/files/stat.py b/files/stat.py index 10ed4e0ebe2..c07cf699d89 100644 --- a/files/stat.py +++ b/files/stat.py @@ -331,8 +331,7 @@ from ansible.module_utils._text import to_bytes -def format_output(module, path, st, follow, get_md5, get_checksum, - checksum_algorithm, mimetype=None, charset=None): +def format_output(module, path, st): mode = st.st_mode # back to ansible @@ -367,37 +366,30 @@ def format_output(module, path, st, follow, get_md5, get_checksum, xoth=bool(mode & stat.S_IXOTH), isuid=bool(mode & stat.S_ISUID), isgid=bool(mode & stat.S_ISGID), - readable=os.access(path, os.R_OK), - writeable=os.access(path, os.W_OK), - executable=os.access(path, os.X_OK), ) - if stat.S_ISLNK(mode): - output['lnk_source'] = os.path.realpath(path) - - if stat.S_ISREG(mode) and get_md5 and os.access(path, os.R_OK): - # Will fail on FIPS-140 compliant systems - try: - output['md5'] = module.md5(path) - except ValueError: - output['md5'] = None - - if stat.S_ISREG(mode) and get_checksum and os.access(path, os.R_OK): - output['checksum'] = module.digest_from_file(path, checksum_algorithm) - - try: - pw = pwd.getpwuid(st.st_uid) - - output['pw_name'] = pw.pw_name - - grp_info = grp.getgrgid(st.st_gid) - output['gr_name'] = grp_info.gr_name - except: - pass + # Platform dependant flags: + for other in [ + # Some Linux + ('st_blocks','blocks'), + ('st_blksize', 'block_size'), + ('st_rdev','device_type'), + ('st_flags', 'flags'), + # Some Berkley based + ('st_gen', 'generation'), + ('st_birthtime', 'birthtime'), + # RISCOS + ('st_ftype', 'file_type'), + ('st_attrs', 'attributes'), + ('st_obtype', 'object_type'), + # OS X + ('st_rsize', 'real_size'), + ('st_creator', 'creator'), + ('st_type', 'file_type'), + ]: + if hasattr(st, other[0]): + output[other[1]] = getattr(st, other[0]) - if not (mimetype is None and charset is None): - output['mime_type'] = mimetype - output['charset'] = charset return output @@ -425,6 +417,7 @@ def main(): get_checksum = module.params.get('get_checksum') checksum_algorithm = module.params.get('checksum_algorithm') + # main stat data try: if follow: st = os.stat(b_path) @@ -438,26 +431,54 @@ def main(): module.fail_json(msg=e.strerror) - mimetype = None - charset = None - if get_mime: - mimetype = 'unknown' - charset = 'unknown' + # process base results + output = format_output(module, path, st) + + # resolved permissions + for perm in [('readable', os.R_OK), ('writeable', os.W_OK), ('executable', os.X_OK)]: + output[perm[0]] = os.access(path, perm[1]) + # symlink info + if output.get('islnk'): + output['lnk_source'] = os.path.realpath(path) + + try: # user data + pw = pwd.getpwuid(st.st_uid) + output['pw_name'] = pw.pw_name + except: + pass + + try: # group data + grp_info = grp.getgrgid(st.st_gid) + output['gr_name'] = grp_info.gr_name + except: + pass + + # checksums + if output.get('isreg') and output.get('readable'): + if get_md5: + # Will fail on FIPS-140 compliant systems + try: + output['md5'] = module.md5(path) + except ValueError: + output['md5'] = None + + if get_checksum: + output['checksum'] = module.digest_from_file(path, checksum_algorithm) + + # try to get mime data if requested + if get_mime: + output['mimetype'] = output['charset'] = 'unknown' filecmd = [module.get_bin_path('file', True), '-i', path] try: rc, out, err = module.run_command(filecmd) if rc == 0: mimetype, charset = out.split(':')[1].split(';') - mimetype = mimetype.strip() - charset = charset.split('=')[1].strip() + output['mimetype'] = mimetype.strip() + output['charset'] = charset.split('=')[1].strip() except: pass - output = format_output(module, path, st, follow, get_md5, get_checksum, - checksum_algorithm, mimetype=mimetype, - charset=charset) - module.exit_json(changed=False, stat=output) if __name__ == '__main__': From 15d62dc17fa4d5bdf192647f6c11693a4ae159d3 Mon Sep 17 00:00:00 2001 From: Luca Berruti Date: Thu, 25 Aug 2016 20:32:54 +0200 Subject: [PATCH 567/770] ini_file: fixes #1788, fails --check when file doesn't exist. --- files/ini_file.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 68d72ca9ba2..12aff23a0a1 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -127,17 +127,16 @@ def do_ini(module, filename, section=None, option=None, value=None, state='prese if not os.path.exists(filename): - try: - open(filename,'w').close() - except: - module.fail_json(msg="Destination file %s not writable" % filename) - ini_file = open(filename, 'r') - try: - ini_lines = ini_file.readlines() - # append a fake section line to simplify the logic - ini_lines.append('[') - finally: - ini_file.close() + ini_lines = [] + else: + ini_file = open(filename, 'r') + try: + ini_lines = ini_file.readlines() + finally: + ini_file.close() + + # append a fake section line to simplify the logic + ini_lines.append('[') within_section = not section section_start = 0 @@ -243,8 +242,9 @@ def main(): (changed,backup_file) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces) - file_args = module.load_file_common_arguments(module.params) - changed = module.set_fs_attributes_if_different(file_args, changed) + if not module.check_mode and os.path.exists(dest): + file_args = module.load_file_common_arguments(module.params) + changed = module.set_fs_attributes_if_different(file_args, changed) results = { 'changed': changed, 'msg': "OK", 'dest': dest } if backup_file is not None: From 486bedb5e5f2e51c63c47019977f0cd30e4e9996 Mon Sep 17 00:00:00 2001 From: Luca Berruti Date: Thu, 25 Aug 2016 22:50:28 +0200 Subject: [PATCH 568/770] ini_file: add create= option. --- files/ini_file.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 12aff23a0a1..7b165e5fa01 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -77,6 +77,14 @@ required: false default: false version_added: "2.1" + create: + required: false + choices: [ "yes", "no" ] + default: "no" + description: + - If specified, the file will be created if it does not already exist. + By default it will fail if the file is missing. + version_added: "2.2" notes: - While it is possible to add an I(option) without specifying a I(value), this makes no sense. @@ -123,10 +131,16 @@ def match_active_opt(option, line): # ============================================================== # do_ini -def do_ini(module, filename, section=None, option=None, value=None, state='present', backup=False, no_extra_spaces=False): +def do_ini(module, filename, section=None, option=None, value=None, + state='present', backup=False, no_extra_spaces=False, create=False): if not os.path.exists(filename): + if not create: + module.fail_json(rc=257, msg='Destination %s does not exist !' % filename) + destpath = os.path.dirname(filename) + if not os.path.exists(destpath) and not module.check_mode: + os.makedirs(destpath) ini_lines = [] else: ini_file = open(filename, 'r') @@ -226,7 +240,8 @@ def main(): value = dict(required=False), backup = dict(default='no', type='bool'), state = dict(default='present', choices=['present', 'absent']), - no_extra_spaces = dict(required=False, default=False, type='bool') + no_extra_spaces = dict(required=False, default=False, type='bool'), + create=dict(default=False, type='bool') ), add_file_common_args = True, supports_check_mode = True @@ -239,8 +254,9 @@ def main(): state = module.params['state'] backup = module.params['backup'] no_extra_spaces = module.params['no_extra_spaces'] + create = module.params['create'] - (changed,backup_file) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces) + (changed,backup_file) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces, create) if not module.check_mode and os.path.exists(dest): file_args = module.load_file_common_arguments(module.params) From a5e6e4a134cd7a3d36b51413a53da345646a763f Mon Sep 17 00:00:00 2001 From: Luca Berruti Date: Thu, 25 Aug 2016 21:05:25 +0200 Subject: [PATCH 569/770] ini_file: diff support. --- files/ini_file.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 7b165e5fa01..4b424fd0603 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -134,6 +134,10 @@ def match_active_opt(option, line): def do_ini(module, filename, section=None, option=None, value=None, state='present', backup=False, no_extra_spaces=False, create=False): + diff = {'before': '', + 'after': '', + 'before_header': '%s (content)' % filename, + 'after_header': '%s (content)' % filename} if not os.path.exists(filename): if not create: @@ -149,6 +153,9 @@ def do_ini(module, filename, section=None, option=None, value=None, finally: ini_file.close() + if module._diff: + diff['before'] = ''.join(ini_lines) + # append a fake section line to simplify the logic ini_lines.append('[') @@ -214,6 +221,8 @@ def do_ini(module, filename, section=None, option=None, value=None, ini_lines.append(assignment_format % (option, value)) changed = True + if module._diff: + diff['after'] = ''.join(ini_lines) backup_file = None if changed and not module.check_mode: @@ -225,7 +234,7 @@ def do_ini(module, filename, section=None, option=None, value=None, finally: ini_file.close() - return (changed, backup_file) + return (changed, backup_file, diff) # ============================================================== # main @@ -256,13 +265,13 @@ def main(): no_extra_spaces = module.params['no_extra_spaces'] create = module.params['create'] - (changed,backup_file) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces, create) + (changed,backup_file,diff) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces, create) if not module.check_mode and os.path.exists(dest): file_args = module.load_file_common_arguments(module.params) changed = module.set_fs_attributes_if_different(file_args, changed) - results = { 'changed': changed, 'msg': "OK", 'dest': dest } + results = { 'changed': changed, 'msg': "OK", 'dest': dest, 'diff': diff } if backup_file is not None: results['backup_file'] = backup_file From 9c766a9f88b88d526e5f32ffdb0af860e16b1dda Mon Sep 17 00:00:00 2001 From: Luca Berruti Date: Thu, 25 Aug 2016 21:05:52 +0200 Subject: [PATCH 570/770] ini_file: return more infos on changes. --- files/ini_file.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 4b424fd0603..9dc3634c2f0 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -162,6 +162,7 @@ def do_ini(module, filename, section=None, option=None, value=None, within_section = not section section_start = 0 changed = False + msg = 'OK' if no_extra_spaces: assignment_format = '%s=%s\n' else: @@ -179,11 +180,13 @@ def do_ini(module, filename, section=None, option=None, value=None, # search backwards for previous non-blank or non-comment line if not re.match(r'^[ \t]*([#;].*)?$', ini_lines[i - 1]): ini_lines.insert(i, assignment_format % (option, value)) + msg = 'option added' changed = True break elif state == 'absent' and not option: # remove the entire section del ini_lines[section_start:index] + msg = 'section removed' changed = True break else: @@ -193,6 +196,8 @@ def do_ini(module, filename, section=None, option=None, value=None, if match_opt(option, line): newline = assignment_format % (option, value) changed = ini_lines[index] != newline + if changed: + msg = 'option changed' ini_lines[index] = newline if changed: # remove all possible option occurrences from the rest of the section @@ -211,6 +216,7 @@ def do_ini(module, filename, section=None, option=None, value=None, if match_active_opt(option, line): del ini_lines[index] changed = True + msg = 'option changed' break # remove the fake section line @@ -220,6 +226,7 @@ def do_ini(module, filename, section=None, option=None, value=None, ini_lines.append('[%s]\n' % section) ini_lines.append(assignment_format % (option, value)) changed = True + msg = 'section and option added' if module._diff: diff['after'] = ''.join(ini_lines) @@ -234,7 +241,7 @@ def do_ini(module, filename, section=None, option=None, value=None, finally: ini_file.close() - return (changed, backup_file, diff) + return (changed, backup_file, diff, msg) # ============================================================== # main @@ -265,13 +272,13 @@ def main(): no_extra_spaces = module.params['no_extra_spaces'] create = module.params['create'] - (changed,backup_file,diff) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces, create) + (changed,backup_file,diff,msg) = do_ini(module, dest, section, option, value, state, backup, no_extra_spaces, create) if not module.check_mode and os.path.exists(dest): file_args = module.load_file_common_arguments(module.params) changed = module.set_fs_attributes_if_different(file_args, changed) - results = { 'changed': changed, 'msg': "OK", 'dest': dest, 'diff': diff } + results = { 'changed': changed, 'msg': msg, 'dest': dest, 'diff': diff } if backup_file is not None: results['backup_file'] = backup_file From 30ed23bf1c9b555c84ecf0547c11bbfdde8e3879 Mon Sep 17 00:00:00 2001 From: Rowan Wookey Date: Thu, 8 Sep 2016 12:58:11 +0100 Subject: [PATCH 571/770] Added work around for Ubuntu Xenial calling php7_module php7.0 --- web_infrastructure/apache2_module.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/web_infrastructure/apache2_module.py b/web_infrastructure/apache2_module.py index 16a9998845f..9f9df923e03 100644 --- a/web_infrastructure/apache2_module.py +++ b/web_infrastructure/apache2_module.py @@ -80,6 +80,12 @@ def _module_is_enabled(module): result, stdout, stderr = module.run_command("%s -M" % control_binary) + """ + Work around for Ubuntu Xenial listing php7_module as php7.0 + """ + if name == "php7.0": + name = "php7" + if result != 0: module.fail_json(msg="Error executing %s: %s" % (control_binary, stderr)) From bfee04d5035447773f232d27b62f8d160d2dd9ae Mon Sep 17 00:00:00 2001 From: Evgenii Terechkov Date: Fri, 29 Jul 2016 08:17:04 +0700 Subject: [PATCH 572/770] Ensure that we use shell to run apt-get -y install ... >/dev/null this commit must fix #2839 --- packaging/os/apt_rpm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 100644 => 100755 packaging/os/apt_rpm.py diff --git a/packaging/os/apt_rpm.py b/packaging/os/apt_rpm.py old mode 100644 new mode 100755 index fec220e0512..5d923de85ea --- a/packaging/os/apt_rpm.py +++ b/packaging/os/apt_rpm.py @@ -126,7 +126,7 @@ def install_packages(module, pkgspec): cmd = ("%s -y install %s > /dev/null" % (APT_PATH, packages)) - rc, out, err = module.run_command(cmd) + rc, out, err = module.run_command(cmd,use_unsafe_shell=True) installed = True for packages in pkgspec: From b16e2597a46465558b605fd494c1acb31b346b6a Mon Sep 17 00:00:00 2001 From: Evgenii Terechkov Date: Tue, 30 Aug 2016 23:50:12 +0700 Subject: [PATCH 573/770] Replace dangerous shell calls with module.run_command --- packaging/os/apt_rpm.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/packaging/os/apt_rpm.py b/packaging/os/apt_rpm.py index 5d923de85ea..59eccfee6ee 100755 --- a/packaging/os/apt_rpm.py +++ b/packaging/os/apt_rpm.py @@ -75,7 +75,7 @@ def query_package(module, name): # rpm -q returns 0 if the package is installed, # 1 if it is not installed - rc = os.system("%s -q %s" % (RPM_PATH,name)) + rc, out, err = module.run_command("%s -q %s" % (RPM_PATH,name)) if rc == 0: return True else: @@ -84,14 +84,14 @@ def query_package(module, name): def query_package_provides(module, name): # rpm -q returns 0 if the package is installed, # 1 if it is not installed - rc = os.system("%s -q --provides %s >/dev/null" % (RPM_PATH,name)) + rc, out, err = module.run_command("%s -q --provides %s" % (RPM_PATH,name)) return rc == 0 def update_package_db(module): - rc = os.system("%s update" % APT_PATH) + rc, out, err = module.run_command("%s update" % APT_PATH) if rc != 0: - module.fail_json(msg="could not update package db") + module.fail_json(msg="could not update package db: %s" % err) def remove_packages(module, packages): @@ -102,10 +102,10 @@ def remove_packages(module, packages): if not query_package(module, package): continue - rc = os.system("%s -y remove %s > /dev/null" % (APT_PATH,package)) + rc, out, err = module.run_command("%s -y remove %s" % (APT_PATH,package)) if rc != 0: - module.fail_json(msg="failed to remove %s" % (package)) + module.fail_json(msg="failed to remove %s: %s" % (package, err)) remove_c += 1 @@ -124,9 +124,7 @@ def install_packages(module, pkgspec): if len(packages) != 0: - cmd = ("%s -y install %s > /dev/null" % (APT_PATH, packages)) - - rc, out, err = module.run_command(cmd,use_unsafe_shell=True) + rc, out, err = module.run_command("%s -y install %s" % (APT_PATH, packages)) installed = True for packages in pkgspec: From 10b0580ff48b7d7937836425a3f2c37322ad34a8 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Wed, 3 Aug 2016 16:29:47 -0700 Subject: [PATCH 574/770] Ensure trailing newline is written to cron file Records whether existing cron file (or CRONCMD output) has a terminating newline, and ensures a trailing newline is written as necessary EVEN IF NO CHANGE WAS MADE to the target env/job Fixes #2316 --- system/cron.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/system/cron.py b/system/cron.py index 654ca410bb6..f13032c80a2 100644 --- a/system/cron.py +++ b/system/cron.py @@ -224,6 +224,7 @@ def __init__(self, module, user=None, cron_file=None): self.root = (os.getuid() == 0) self.lines = None self.ansible = "#Ansible: " + self.terminated= True if cron_file: if os.path.isabs(cron_file): @@ -242,7 +243,9 @@ def read(self): # read the cronfile try: f = open(self.cron_file, 'r') - self.lines = f.read().splitlines() + read_cron_file = f.read() + self.terminated = read_cron_file.endswith(('\r', '\n')) + self.lines = read_cron_file.splitlines() f.close() except IOError: # cron file does not exist @@ -256,6 +259,8 @@ def read(self): if rc != 0 and rc != 1: # 1 can mean that there are no jobs. raise CronTabError("Unable to read crontab") + self.terminated = out.endswith(('\r', '\n')) + lines = out.splitlines() count = 0 for l in lines: @@ -464,8 +469,8 @@ def render(self): crons.append(cron) result = '\n'.join(crons) - if result and result[-1] not in ['\n', '\r']: - result += '\n' + if result: + result = result.rstrip('\r\n') + '\n' return result def _read_user_execute(self): @@ -660,6 +665,10 @@ def main(): crontab.remove_job(name) changed = True + # no changes to env/job, but existing crontab needs a terminating newline + if not changed and not crontab.terminated: + changed = True + res_args = dict( jobs = crontab.get_jobnames(), envs = crontab.get_envnames(), From 7c9401a7ff4e3c1d1078be06293afca089d7fc05 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Thu, 4 Aug 2016 14:00:04 -0700 Subject: [PATCH 575/770] Rendering of crontab should reflect actual newline termination, in diff mode --- system/cron.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/system/cron.py b/system/cron.py index f13032c80a2..634d1d6b8b1 100644 --- a/system/cron.py +++ b/system/cron.py @@ -460,7 +460,7 @@ def _update_env(self, name, decl, addenvfunction): self.lines = newlines - def render(self): + def render(self, diff=None): """ Render this crontab as it would be in the crontab. """ @@ -469,7 +469,7 @@ def render(self): crons.append(cron) result = '\n'.join(crons) - if result: + if result and not diff: result = result.rstrip('\r\n') + '\n' return result @@ -586,7 +586,7 @@ def main(): if module._diff: diff = dict() - diff['before'] = crontab.render() + diff['before'] = crontab.render(diff=True) if crontab.cron_file: diff['before_header'] = crontab.cron_file else: From b0c94e957ecffad4d96ec6c8723289a7cd2807e9 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Sun, 14 Aug 2016 23:51:06 -0700 Subject: [PATCH 576/770] Record existing cron file as string property, rather than only recording termination This seems less hackish, and feels more proper for diff generation --- system/cron.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/system/cron.py b/system/cron.py index 634d1d6b8b1..b7739a1c7bc 100644 --- a/system/cron.py +++ b/system/cron.py @@ -224,7 +224,7 @@ def __init__(self, module, user=None, cron_file=None): self.root = (os.getuid() == 0) self.lines = None self.ansible = "#Ansible: " - self.terminated= True + self.existing = '' if cron_file: if os.path.isabs(cron_file): @@ -243,9 +243,8 @@ def read(self): # read the cronfile try: f = open(self.cron_file, 'r') - read_cron_file = f.read() - self.terminated = read_cron_file.endswith(('\r', '\n')) - self.lines = read_cron_file.splitlines() + self.existing = f.read() + self.lines = self.existing.splitlines() f.close() except IOError: # cron file does not exist @@ -259,7 +258,7 @@ def read(self): if rc != 0 and rc != 1: # 1 can mean that there are no jobs. raise CronTabError("Unable to read crontab") - self.terminated = out.endswith(('\r', '\n')) + self.existing = out lines = out.splitlines() count = 0 @@ -268,6 +267,9 @@ def read(self): not re.match( r'# \(/tmp/.*installed on.*\)', l) and not re.match( r'# \(.*version.*\)', l)): self.lines.append(l) + else: + pattern = re.escape(l) + '[\r\n]?' + self.existing = re.sub(pattern, '', self.existing, 1) count += 1 def is_empty(self): @@ -460,7 +462,7 @@ def _update_env(self, name, decl, addenvfunction): self.lines = newlines - def render(self, diff=None): + def render(self): """ Render this crontab as it would be in the crontab. """ @@ -469,7 +471,7 @@ def render(self, diff=None): crons.append(cron) result = '\n'.join(crons) - if result and not diff: + if result: result = result.rstrip('\r\n') + '\n' return result @@ -586,7 +588,7 @@ def main(): if module._diff: diff = dict() - diff['before'] = crontab.render(diff=True) + diff['before'] = crontab.existing if crontab.cron_file: diff['before_header'] = crontab.cron_file else: @@ -666,7 +668,7 @@ def main(): changed = True # no changes to env/job, but existing crontab needs a terminating newline - if not changed and not crontab.terminated: + if not changed and not crontab.existing.endswith(('\r', '\n')): changed = True res_args = dict( From ea7005a18522ce4089b4614563bbdac90224ca47 Mon Sep 17 00:00:00 2001 From: Jonathan Mainguy Date: Sun, 23 Oct 2016 19:29:27 -0400 Subject: [PATCH 577/770] update maintainer --- system/hostname.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hostname.py b/system/hostname.py index c56572ebade..1e9354b9772 100644 --- a/system/hostname.py +++ b/system/hostname.py @@ -22,7 +22,7 @@ --- module: hostname author: - - "Hiroaki Nakamura (@hnakamur)" + - "Adrian Likins (@alikins)" - "Hideki Saito (@saito-hideki)" version_added: "1.4" short_description: Manage hostname From 454f741ef5b56cccd123e12d7b2e6fe31d47c755 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Sun, 23 Oct 2016 22:17:06 -0700 Subject: [PATCH 578/770] First set of fixes for uri module to work with py3. This fix handles changes in the response headers (no longer all lowercased) and switches from unicode() to to_text(). --- network/basics/uri.py | 48 ++++++++++++++++++++++++------------------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/network/basics/uri.py b/network/basics/uri.py index 3ecc85ea592..cef369516a4 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -20,19 +20,6 @@ # # see examples/playbooks/uri.yml -import cgi -import shutil -import tempfile -import datetime - -try: - import json -except ImportError: - import simplejson as json - -import ansible.module_utils.six as six - - DOCUMENTATION = ''' --- module: uri @@ -217,6 +204,23 @@ ''' +import cgi +import datetime +import os +import shutil +import tempfile + +try: + import json +except ImportError: + import simplejson as json + +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.pycompat24 import get_exception +import ansible.module_utils.six as six +from ansible.module_utils._text import to_text +from ansible.module_utils.urls import fetch_url, url_argument_spec + def write_file(module, url, dest, content): # create a tempfile with some test content @@ -446,11 +450,17 @@ def main(): # Default content_encoding to try content_encoding = 'utf-8' - if 'content_type' in uresp: - content_type, params = cgi.parse_header(uresp['content_type']) + content_type_key = None + for key in uresp: + # Py2: content_type; Py3: Content_type + if 'content_type' == key.lower(): + content_type_key = key + break + if content_type_key is not None: + content_type, params = cgi.parse_header(uresp[content_type_key]) if 'charset' in params: content_encoding = params['charset'] - u_content = unicode(content, content_encoding, errors='replace') + u_content = to_text(content, encoding=content_encoding) if 'application/json' in content_type or 'text/json' in content_type: try: js = json.loads(u_content) @@ -458,7 +468,7 @@ def main(): except: pass else: - u_content = unicode(content, content_encoding, errors='replace') + u_content = to_text(content, encoding=content_encoding) if resp['status'] not in status_code: uresp['msg'] = 'Status code was not %s: %s' % (status_code, uresp.get('msg', '')) @@ -469,9 +479,5 @@ def main(): module.exit_json(changed=changed, **uresp) -# import module snippets -from ansible.module_utils.basic import * -from ansible.module_utils.urls import * - if __name__ == '__main__': main() From 6f3e703ed4adee5c55053aca2ef830a1da10f4be Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 23 Oct 2016 23:16:35 -0700 Subject: [PATCH 579/770] switch win async to use Win32 CreateProcess should fix flaky async startup behavior where watchdog/module exec starts and immediately dies --- windows/async_wrapper.ps1 | 114 +++++++++++++++++++++++++++++++------- 1 file changed, 94 insertions(+), 20 deletions(-) diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1 index 54d820fe58b..1ebd578f647 100644 --- a/windows/async_wrapper.ps1 +++ b/windows/async_wrapper.ps1 @@ -41,19 +41,56 @@ Function Start-Watchdog { [switch]$start_suspended ) +# BEGIN Ansible.Async native type definition $native_process_util = @" using Microsoft.Win32.SafeHandles; using System; using System.ComponentModel; using System.Diagnostics; + using System.IO; using System.Linq; using System.Runtime.InteropServices; + using System.Text; using System.Threading; namespace Ansible.Async { public static class NativeProcessUtil { + [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Unicode)] + public static extern bool CreateProcess( + string lpApplicationName, + string lpCommandLine, + IntPtr lpProcessAttributes, + IntPtr lpThreadAttributes, + bool bInheritHandles, + uint dwCreationFlags, + IntPtr lpEnvironment, + string lpCurrentDirectory, + [In] ref STARTUPINFO lpStartupInfo, + out PROCESS_INFORMATION lpProcessInformation); + + [DllImport("kernel32.dll", SetLastError = true, CharSet=CharSet.Unicode)] + public static extern uint SearchPath ( + string lpPath, + string lpFileName, + string lpExtension, + int nBufferLength, + [MarshalAs (UnmanagedType.LPTStr)] + StringBuilder lpBuffer, + out IntPtr lpFilePart); + + public static string SearchPath(string findThis) + { + StringBuilder sbOut = new StringBuilder(1024); + IntPtr filePartOut; + + if(SearchPath(null, findThis, null, sbOut.Capacity, sbOut, out filePartOut) == 0) + throw new FileNotFoundException("Couldn't locate " + findThis + " on path"); + + return sbOut.ToString(); + } + [DllImport("kernel32.dll", SetLastError=true)] static extern SafeFileHandle OpenThread( ThreadAccessRights dwDesiredAccess, @@ -63,12 +100,6 @@ Function Start-Watchdog { [DllImport("kernel32.dll", SetLastError=true)] static extern int ResumeThread(SafeHandle hThread); - [Flags] - enum ThreadAccessRights : uint - { - SUSPEND_RESUME = 0x0002 - } - public static void ResumeThreadById(int threadId) { var threadHandle = OpenThread(ThreadAccessRights.SUSPEND_RESUME, false, threadId); @@ -104,8 +135,46 @@ Function Start-Watchdog { ResumeThreadById(thread.Id); } } + + [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] + public struct STARTUPINFO + { + public Int32 cb; + public string lpReserved; + public string lpDesktop; + public string lpTitle; + public Int32 dwX; + public Int32 dwY; + public Int32 dwXSize; + public Int32 dwYSize; + public Int32 dwXCountChars; + public Int32 dwYCountChars; + public Int32 dwFillAttribute; + public Int32 dwFlags; + public Int16 wShowWindow; + public Int16 cbReserved2; + public IntPtr lpReserved2; + public IntPtr hStdInput; + public IntPtr hStdOutput; + public IntPtr hStdError; + } + + [StructLayout(LayoutKind.Sequential)] + public struct PROCESS_INFORMATION + { + public IntPtr hProcess; + public IntPtr hThread; + public int dwProcessId; + public int dwThreadId; + } + + [Flags] + enum ThreadAccessRights : uint + { + SUSPEND_RESUME = 0x0002 + } } -"@ +"@ # END Ansible.Async native type definition Add-Type -TypeDefinition $native_process_util @@ -304,31 +373,36 @@ Function Start-Watchdog { $encoded_command = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($raw_script)) - $exec_path = "powershell -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" - - # FUTURE: create under new job to ensure we kill all children on exit? + # FUTURE: create under new job to ensure all children die on exit? + # FUTURE: move these flags into C# enum # start process suspended + breakaway so we can record the watchdog pid without worrying about a completion race Set-Variable CREATE_BREAKAWAY_FROM_JOB -Value ([uint32]0x01000000) -Option Constant Set-Variable CREATE_SUSPENDED -Value ([uint32]0x00000004) -Option Constant + Set-Variable CREATE_UNICODE_ENVIRONMENT -Value ([uint32]0x000000400) -Option Constant + Set-Variable CREATE_NEW_CONSOLE -Value ([uint32]0x00000010) -Option Constant - $pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB + $pstartup_flags = $CREATE_BREAKAWAY_FROM_JOB -bor $CREATE_UNICODE_ENVIRONMENT -bor $CREATE_NEW_CONSOLE If($start_suspended) { $pstartup_flags = $pstartup_flags -bor $CREATE_SUSPENDED } - $pstartup = ([wmiclass]"Win32_ProcessStartup") - $pstartup.Properties['CreateFlags'].Value = $pstartup_flags - # execute the dynamic watchdog as a breakway process, which will in turn exec the module - # FUTURE: use CreateProcess + stream redirection to watch for/return quick watchdog failures? - $result = $([wmiclass]"Win32_Process").Create($exec_path, $null, $pstartup) + $si = New-Object Ansible.Async.STARTUPINFO + $si.cb = [System.Runtime.InteropServices.Marshal]::SizeOf([type][Ansible.Async.STARTUPINFO]) + + $pi = New-Object Ansible.Async.PROCESS_INFORMATION - # On fast + idle machines, the process never starts without this delay. Hopefully the switch to - # Win32 process launch will make this unnecessary. - Sleep -Seconds 1 + # FUTURE: direct cmdline CreateProcess path lookup fails- this works but is sub-optimal + $exec_cmd = [Ansible.Async.NativeProcessUtil]::SearchPath("powershell.exe") + $exec_args = "`"$exec_cmd`" -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" + + If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, [IntPtr]::Zero, [IntPtr]::Zero, $true, $pstartup_flags, [IntPtr]::Zero, $PSScriptRoot, [ref]$si, [ref]$pi)) { + #throw New-Object System.ComponentModel.Win32Exception + throw "create bang $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" + } - $watchdog_pid = $result.ProcessId + $watchdog_pid = $pi.dwProcessId return $watchdog_pid } From af58bfbfe304727658c06e8f60cc5564ce8085a7 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Sun, 23 Oct 2016 23:44:55 -0700 Subject: [PATCH 580/770] fix win async tempdir deletion failure --- windows/async_wrapper.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1 index 1ebd578f647..6463ef3fa48 100644 --- a/windows/async_wrapper.ps1 +++ b/windows/async_wrapper.ps1 @@ -397,7 +397,7 @@ Function Start-Watchdog { $exec_cmd = [Ansible.Async.NativeProcessUtil]::SearchPath("powershell.exe") $exec_args = "`"$exec_cmd`" -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" - If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, [IntPtr]::Zero, [IntPtr]::Zero, $true, $pstartup_flags, [IntPtr]::Zero, $PSScriptRoot, [ref]$si, [ref]$pi)) { + If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, [IntPtr]::Zero, [IntPtr]::Zero, $true, $pstartup_flags, [IntPtr]::Zero, $env:windir, [ref]$si, [ref]$pi)) { #throw New-Object System.ComponentModel.Win32Exception throw "create bang $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" } From 124bb9241694aa5492a85e7abfabd3720ae2b682 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 24 Oct 2016 07:01:00 -0700 Subject: [PATCH 581/770] Fix uri for change in case in response In python3, response fields are title cased whereas in python2 they were not. We return these fields to the module's caller so we need to normalize all of them to be lower case. This reverts the lowercase check from 454f741ef5b56cccd123e12d7b2e6fe31d47c755 as that one was only targetted as a single field. --- network/basics/uri.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/network/basics/uri.py b/network/basics/uri.py index cef369516a4..afff08757b4 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -438,9 +438,11 @@ def main(): # Transmogrify the headers, replacing '-' with '_', since variables dont # work with dashes. + # In python3, the headers are title cased. Lowercase them to be + # compatible with the python2 behaviour. uresp = {} for key, value in six.iteritems(resp): - ukey = key.replace("-", "_") + ukey = key.replace("-", "_").lower() uresp[ukey] = value try: @@ -450,14 +452,8 @@ def main(): # Default content_encoding to try content_encoding = 'utf-8' - content_type_key = None - for key in uresp: - # Py2: content_type; Py3: Content_type - if 'content_type' == key.lower(): - content_type_key = key - break - if content_type_key is not None: - content_type, params = cgi.parse_header(uresp[content_type_key]) + if 'content_type' in uresp: + content_type, params = cgi.parse_header(uresp['content_type']) if 'charset' in params: content_encoding = params['charset'] u_content = to_text(content, encoding=content_encoding) From 087ba94e6ba731423eb36a5eefc520d468730394 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Mon, 24 Oct 2016 07:42:01 -0700 Subject: [PATCH 582/770] Exposed backup file path, simplified result args (#5364) Fixes #245 --- files/replace.py | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/files/replace.py b/files/replace.py index b89e81390b6..ef66c223911 100644 --- a/files/replace.py +++ b/files/replace.py @@ -131,7 +131,7 @@ def main(): params = module.params dest = os.path.expanduser(params['dest']) - diff = dict() + res_args = dict() if os.path.isdir(dest): module.fail_json(rc=256, msg='Destination %s is a directory !' % dest) @@ -143,12 +143,6 @@ def main(): contents = f.read() f.close() - if module._diff: - diff = { - 'before_header': dest, - 'before': contents, - } - mre = re.compile(params['regexp'], re.MULTILINE) result = re.subn(mre, params['replace'], contents, 0) @@ -156,22 +150,25 @@ def main(): msg = '%s replacements made' % result[1] changed = True if module._diff: - diff['after_header'] = dest - diff['after'] = result[0] + res_args['diff'] = { + 'before_header': dest, + 'before': contents, + 'after_header': dest, + 'after': result[0], + } else: msg = '' changed = False - diff = dict() if changed and not module.check_mode: if params['backup'] and os.path.exists(dest): - module.backup_local(dest) + res_args['backup_file'] = module.backup_local(dest) if params['follow'] and os.path.islink(dest): dest = os.path.realpath(dest) write_changes(module, result[0], dest) - msg, changed = check_file_attrs(module, changed, msg) - module.exit_json(changed=changed, msg=msg, diff=diff) + res_args['msg'], res_args['changed'] = check_file_attrs(module, changed, msg) + module.exit_json(**res_args) # this is magic, see lib/ansible/module_common.py from ansible.module_utils.basic import * From 1072555cc9d730dc1540e0e24758e6c63ecbd1c9 Mon Sep 17 00:00:00 2001 From: Hiroaki Nakamura Date: Mon, 24 Oct 2016 23:58:41 +0900 Subject: [PATCH 583/770] hostname: add support for alpine linux (#4837) * Add update_current_and_permanent_hostname to the hostname module * Add support for Alpine Linux to the hostname module --- system/hostname.py | 104 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 92 insertions(+), 12 deletions(-) diff --git a/system/hostname.py b/system/hostname.py index 1e9354b9772..4c4285f665f 100644 --- a/system/hostname.py +++ b/system/hostname.py @@ -29,7 +29,7 @@ requirements: [ hostname ] description: - Set system's hostname. - - Currently implemented on Debian, Ubuntu, Fedora, RedHat, openSUSE, Linaro, ScientificLinux, Arch, CentOS, AMI. + - Currently implemented on Debian, Ubuntu, Fedora, RedHat, openSUSE, Linaro, ScientificLinux, Arch, CentOS, AMI, Alpine Linux. - Any distribution that uses systemd as their init system. - Note, this module does *NOT* modify /etc/hosts. You need to modify it yourself using other modules like template or replace. options: @@ -56,6 +56,15 @@ class UnimplementedStrategy(object): def __init__(self, module): self.module = module + def update_current_and_permanent_hostname(self): + self.unimplemented_error() + + def update_current_hostname(self): + self.unimplemented_error() + + def update_permanent_hostname(self): + self.unimplemented_error() + def get_current_hostname(self): self.unimplemented_error() @@ -103,6 +112,9 @@ def __init__(self, module): else: self.strategy = self.strategy_class(module) + def update_current_and_permanent_hostname(self): + return self.strategy.update_current_and_permanent_hostname() + def get_current_hostname(self): return self.strategy.get_current_hostname() @@ -129,6 +141,26 @@ class GenericStrategy(object): def __init__(self, module): self.module = module self.hostname_cmd = self.module.get_bin_path('hostname', True) + self.changed = False + + def update_current_and_permanent_hostname(self): + self.update_current_hostname() + self.update_permanent_hostname() + return self.changed + + def update_current_hostname(self): + name = self.module.params['name'] + current_name = self.get_current_hostname() + if current_name != name: + self.set_current_hostname(name) + self.changed = True + + def update_permanent_hostname(self): + name = self.module.params['name'] + permanent_name = self.get_permanent_hostname() + if permanent_name != name: + self.set_permanent_hostname(name) + self.changed = True def get_current_hostname(self): cmd = [self.hostname_cmd] @@ -283,6 +315,59 @@ def set_permanent_hostname(self, name): self.module.fail_json(msg="failed to update hostname: %s" % str(err)) +# =========================================== + +class AlpineStrategy(GenericStrategy): + """ + This is a Alpine Linux Hostname manipulation strategy class - it edits + the /etc/hostname file then run hostname -F /etc/hostname. + """ + + HOSTNAME_FILE = '/etc/hostname' + + def update_current_and_permanent_hostname(self): + self.update_permanent_hostname() + self.update_current_hostname() + return self.changed + + def get_permanent_hostname(self): + if not os.path.isfile(self.HOSTNAME_FILE): + try: + open(self.HOSTNAME_FILE, "a").write("") + except IOError: + err = get_exception() + self.module.fail_json(msg="failed to write file: %s" % + str(err)) + try: + f = open(self.HOSTNAME_FILE) + try: + return f.read().strip() + finally: + f.close() + except Exception: + err = get_exception() + self.module.fail_json(msg="failed to read hostname: %s" % + str(err)) + + def set_permanent_hostname(self, name): + try: + f = open(self.HOSTNAME_FILE, 'w+') + try: + f.write("%s\n" % name) + finally: + f.close() + except Exception: + err = get_exception() + self.module.fail_json(msg="failed to update hostname: %s" % + str(err)) + + def set_current_hostname(self, name): + cmd = [self.hostname_cmd, '-F', self.HOSTNAME_FILE] + rc, out, err = self.module.run_command(cmd) + if rc != 0: + self.module.fail_json(msg="Command failed rc=%d, out=%s, err=%s" % + (rc, out, err)) + # =========================================== @@ -617,6 +702,11 @@ class ALTLinuxHostname(Hostname): distribution = 'Altlinux' strategy_class = RedHatStrategy +class AlpineLinuxHostname(Hostname): + platform = 'Linux' + distribution = 'Alpine' + strategy_class = AlpineStrategy + class OpenBSDHostname(Hostname): platform = 'OpenBSD' distribution = None @@ -643,18 +733,8 @@ def main(): ) hostname = Hostname(module) - - changed = False name = module.params['name'] - current_name = hostname.get_current_hostname() - if current_name != name: - hostname.set_current_hostname(name) - changed = True - - permanent_name = hostname.get_permanent_hostname() - if permanent_name != name: - hostname.set_permanent_hostname(name) - changed = True + changed = hostname.update_current_and_permanent_hostname() module.exit_json(changed=changed, name=name, ansible_facts=dict(ansible_hostname=name.split('.')[0], From 4c020102a9cd6fe908c9a4a326a38f972f63a903 Mon Sep 17 00:00:00 2001 From: Fahri Cihan Demirci Date: Mon, 24 Oct 2016 18:55:01 -0400 Subject: [PATCH 584/770] Fix String Type for Python 3 Branch Comparison * Use the `to_native` conversion method to convert a command output to the appropriate form when looking for branch names in the command output, therefore avoiding a `TypeError` in Python 3. --- source_control/git.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index a9952172272..06965f03a98 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -223,7 +223,7 @@ from ansible.module_utils.basic import AnsibleModule, get_module_path from ansible.module_utils.known_hosts import add_git_host_key from ansible.module_utils.six import b, string_types -from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils._text import to_native def head_splitter(headfile, remote, module=None, fail_on_error=False): @@ -519,7 +519,7 @@ def get_tags(git_path, module, dest): def is_remote_branch(git_path, module, dest, remote, version): cmd = '%s ls-remote %s -h refs/heads/%s' % (git_path, remote, version) (rc, out, err) = module.run_command(cmd, check_rc=True, cwd=dest) - if to_bytes(version, errors='surrogate_or_strict') in out: + if to_native(version, errors='surrogate_or_strict') in out: return True else: return False From c51ced56cce443e481c4edc7f95fd3f3c95f8c20 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 25 Oct 2016 17:21:00 -0700 Subject: [PATCH 585/770] fix win_shell/win_command deadlock on large interleaved stdout/stderr (#5384) fixes #5229 --- windows/win_command.ps1 | 38 ++++++++++++++++++++++++++++---- windows/win_shell.ps1 | 49 +++++++++++++++++++++++++++++++++++++---- 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/windows/win_command.ps1 b/windows/win_command.ps1 index 5e934abe0f0..2a03b6a03d5 100644 --- a/windows/win_command.ps1 +++ b/windows/win_command.ps1 @@ -42,8 +42,11 @@ If($removes -and -not $(Test-Path $removes)) { $util_def = @' using System; using System.ComponentModel; +using System.Diagnostics; +using System.IO; using System.Linq; using System.Runtime.InteropServices; +using System.Threading; namespace Ansible.Command { @@ -68,6 +71,32 @@ namespace Ansible.Command return cmdlineParts; } + + public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) + { + var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); + var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); + + string so = null, se = null; + + ThreadPool.QueueUserWorkItem((s)=> + { + so = stdoutStream.ReadToEnd(); + sowait.Set(); + }); + + ThreadPool.QueueUserWorkItem((s) => + { + se = stderrStream.ReadToEnd(); + sewait.Set(); + }); + + foreach(var wh in new WaitHandle[] { sowait, sewait }) + wh.WaitOne(); + + stdout = so; + stderr = se; + } } } '@ @@ -112,11 +141,12 @@ Catch [System.ComponentModel.Win32Exception] { Exit-Json @{failed=$true;changed=$false;cmd=$raw_command_line;rc=$excep.Exception.NativeErrorCode;msg=$excep.Exception.Message} } -# TODO: resolve potential deadlock here if stderr fills buffer (~4k) before stdout is closed, -# perhaps some async stream pumping with Process Output/ErrorDataReceived events... +$stdout = $stderr = [string] $null + +[Ansible.Command.NativeUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null -$result.stdout = $proc.StandardOutput.ReadToEnd() -$result.stderr = $proc.StandardError.ReadToEnd() +$result.stdout = $stdout +$result.stderr = $stderr $proc.WaitForExit() | Out-Null diff --git a/windows/win_shell.ps1 b/windows/win_shell.ps1 index 750ca121ad4..850f2b9561a 100644 --- a/windows/win_shell.ps1 +++ b/windows/win_shell.ps1 @@ -22,6 +22,44 @@ Set-StrictMode -Version 2 $ErrorActionPreference = "Stop" +$helper_def = @" +using System.Diagnostics; +using System.IO; +using System.Threading; + +namespace Ansible.Shell +{ + public class ProcessUtil + { + public static void GetProcessOutput(StreamReader stdoutStream, StreamReader stderrStream, out string stdout, out string stderr) + { + var sowait = new EventWaitHandle(false, EventResetMode.ManualReset); + var sewait = new EventWaitHandle(false, EventResetMode.ManualReset); + + string so = null, se = null; + + ThreadPool.QueueUserWorkItem((s)=> + { + so = stdoutStream.ReadToEnd(); + sowait.Set(); + }); + + ThreadPool.QueueUserWorkItem((s) => + { + se = stderrStream.ReadToEnd(); + sewait.Set(); + }); + + foreach(var wh in new WaitHandle[] { sowait, sewait }) + wh.WaitOne(); + + stdout = so; + stderr = se; + } + } +} +"@ + $parsed_args = Parse-Args $args $false $raw_command_line = $(Get-AnsibleParam $parsed_args "_raw_params" -failifempty $true).Trim() @@ -40,6 +78,8 @@ If($removes -and -not $(Test-Path $removes)) { Exit-Json @{cmd=$raw_command_line; msg="skipped, since $removes does not exist"; changed=$false; skipped=$true; rc=0} } +Add-Type -TypeDefinition $helper_def + $exec_args = $null If(-not $executable -or $executable -eq "powershell") { @@ -80,11 +120,12 @@ Catch [System.ComponentModel.Win32Exception] { Exit-Json @{failed=$true;changed=$false;cmd=$raw_command_line;rc=$excep.Exception.NativeErrorCode;msg=$excep.Exception.Message} } -# TODO: resolve potential deadlock here if stderr fills buffer (~4k) before stdout is closed, -# perhaps some async stream pumping with Process Output/ErrorDataReceived events... +$stdout = $stderr = [string] $null + +[Ansible.Shell.ProcessUtil]::GetProcessOutput($proc.StandardOutput, $proc.StandardError, [ref] $stdout, [ref] $stderr) | Out-Null -$result.stdout = $proc.StandardOutput.ReadToEnd() -$result.stderr = $proc.StandardError.ReadToEnd() +$result.stdout = $stdout +$result.stderr = $stderr # TODO: decode CLIXML stderr output (and other streams?) From 48cd199871a097077f1ae04ef54c5a9467d6034b Mon Sep 17 00:00:00 2001 From: bencomp Date: Sun, 16 Oct 2016 21:52:34 +0200 Subject: [PATCH 586/770] Make find return sample a YAML dict In the description of the find module return value, the sample dict has its key=value strings converted to key=value: None in the web documentation. This commit updates the sample output to a 'real' dict. Minor additional edit in the description: "return list *of* files". --- files/find.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/files/find.py b/files/find.py index d48e52ed05a..9eae9e1bcc6 100644 --- a/files/find.py +++ b/files/find.py @@ -34,7 +34,7 @@ short_description: return a list of files based on specific criteria requirements: [] description: - - Return a list files based on specific criteria. Multiple criteria are AND'd together. + - Return a list of files based on specific criteria. Multiple criteria are AND'd together. options: age: required: false @@ -139,13 +139,13 @@ returned: success type: list of dictionaries sample: [ - { path="/var/tmp/test1", - mode=0644, - ..., - checksum=16fac7be61a6e4591a33ef4b729c5c3302307523 + { path: "/var/tmp/test1", + mode: "0644", + "...": "...", + checksum: 16fac7be61a6e4591a33ef4b729c5c3302307523 }, - { path="/var/tmp/test2", - ... + { path: "/var/tmp/test2", + "...": "..." }, ] matched: From 1163b4bb50f369e43765c5699b8866db2938f4bb Mon Sep 17 00:00:00 2001 From: Charles Paul Date: Thu, 27 Oct 2016 12:00:20 +0800 Subject: [PATCH 587/770] add id: back to documentation --- cloud/amazon/ec2.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index f2407c7aaaa..0b921c4b718 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -29,6 +29,13 @@ required: false default: null aliases: ['keypair'] + id: + version_added: "1.1" + description: + - identifier for this instance or set of instances, so that the module will be idempotent with respect to EC2 instances. This identifier is valid for at least 24 hours after the termination of the instance, and should not be reused for another call later on. For details, see the description of client token at U(http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/Run_Instance_Idempotency.html). + required: false + default: null + aliases: [] group: description: - security group (or list of groups) to use with the instance From de7ec946e9d1cdb636f172b4ec9aeff7e07191d4 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 28 Oct 2016 09:39:56 -0700 Subject: [PATCH 588/770] fix JSON junk in win_file state=directory case (#5427) --- windows/win_file.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_file.ps1 b/windows/win_file.ps1 index 958f9f04fcc..e064c5c6dbd 100644 --- a/windows/win_file.ps1 +++ b/windows/win_file.ps1 @@ -102,7 +102,7 @@ Else If ( $state -eq "directory" ) { - New-Item -ItemType directory -Path $path + New-Item -ItemType directory -Path $path | Out-Null $result.changed = $TRUE } From 679a0ae5e9bd53aa0fbe37ea833d7074784ffffd Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 31 Oct 2016 21:24:05 -0700 Subject: [PATCH 589/770] Removed tests migrated to ansible/ansible repo. (#5452) --- test/unit/cloud/openstack/test_os_server.py | 221 -------------------- 1 file changed, 221 deletions(-) delete mode 100644 test/unit/cloud/openstack/test_os_server.py diff --git a/test/unit/cloud/openstack/test_os_server.py b/test/unit/cloud/openstack/test_os_server.py deleted file mode 100644 index bb1f79ad2f9..00000000000 --- a/test/unit/cloud/openstack/test_os_server.py +++ /dev/null @@ -1,221 +0,0 @@ -import mock -import pytest -import yaml -import inspect -import collections - -from cloud.openstack import os_server - - -class AnsibleFail(Exception): - pass - - -class AnsibleExit(Exception): - pass - - -def params_from_doc(func): - '''This function extracts the docstring from the specified function, - parses it as a YAML document, and returns parameters for the os_server - module.''' - - doc = inspect.getdoc(func) - cfg = yaml.load(doc) - - for task in cfg: - for module, params in task.items(): - for k, v in params.items(): - if k in ['nics'] and type(v) == str: - params[k] = [v] - task[module] = collections.defaultdict(str, - params) - - return cfg[0]['os_server'] - - -class FakeCloud (object): - ports = [ - {'name': 'port1', 'id': '1234'}, - {'name': 'port2', 'id': '4321'}, - ] - - networks = [ - {'name': 'network1', 'id': '5678'}, - {'name': 'network2', 'id': '8765'}, - ] - - images = [ - {'name': 'cirros', 'id': '1'}, - {'name': 'fedora', 'id': '2'}, - ] - - flavors = [ - {'name': 'm1.small', 'id': '1', 'flavor_ram': 1024}, - {'name': 'm1.tiny', 'id': '2', 'flavor_ram': 512}, - ] - - def _find(self, source, name): - for item in source: - if item['name'] == name or item['id'] == name: - return item - - def get_image_id(self, name, exclude=None): - image = self._find(self.images, name) - if image: - return image['id'] - - def get_flavor(self, name): - return self._find(self.flavors, name) - - def get_flavor_by_ram(self, ram, include=None): - for flavor in self.flavors: - if flavor['ram'] >= ram and (include is None or include in - flavor['name']): - return flavor - - def get_port(self, name): - return self._find(self.ports, name) - - def get_network(self, name): - return self._find(self.networks, name) - - create_server = mock.MagicMock() - - -class TestNetworkArgs(object): - '''This class exercises the _network_args function of the - os_server module. For each test, we parse the YAML document - contained in the docstring to retrieve the module parameters for the - test.''' - - def setup_method(self, method): - self.cloud = FakeCloud() - self.module = mock.MagicMock() - self.module.params = params_from_doc(method) - - def test_nics_string_net_id(self): - ''' - - os_server: - nics: net-id=1234 - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['net-id'] == '1234') - - def test_nics_string_net_id_list(self): - ''' - - os_server: - nics: net-id=1234,net-id=4321 - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['net-id'] == '1234') - assert(args[1]['net-id'] == '4321') - - def test_nics_string_port_id(self): - ''' - - os_server: - nics: port-id=1234 - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['port-id'] == '1234') - - def test_nics_string_net_name(self): - ''' - - os_server: - nics: net-name=network1 - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['net-id'] == '5678') - - def test_nics_string_port_name(self): - ''' - - os_server: - nics: port-name=port1 - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['port-id'] == '1234') - - def test_nics_structured_net_id(self): - ''' - - os_server: - nics: - - net-id: '1234' - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['net-id'] == '1234') - - def test_nics_structured_mixed(self): - ''' - - os_server: - nics: - - net-id: '1234' - - port-name: port1 - - 'net-name=network1,port-id=4321' - ''' - args = os_server._network_args(self.module, self.cloud) - assert(args[0]['net-id'] == '1234') - assert(args[1]['port-id'] == '1234') - assert(args[2]['net-id'] == '5678') - assert(args[3]['port-id'] == '4321') - - -class TestCreateServer(object): - def setup_method(self, method): - self.cloud = FakeCloud() - self.module = mock.MagicMock() - self.module.params = params_from_doc(method) - self.module.fail_json.side_effect = AnsibleFail() - self.module.exit_json.side_effect = AnsibleExit() - - self.meta = mock.MagicMock() - self.meta.gett_hostvars_from_server.return_value = { - 'id': '1234' - } - os_server.meta = self.meta - - def test_create_server(self): - ''' - - os_server: - image: cirros - flavor: m1.tiny - nics: - - net-name: network1 - ''' - with pytest.raises(AnsibleExit): - os_server._create_server(self.module, self.cloud) - - assert(self.cloud.create_server.call_count == 1) - assert(self.cloud.create_server.call_args[1]['image'] - == self.cloud.get_image_id('cirros')) - assert(self.cloud.create_server.call_args[1]['flavor'] - == self.cloud.get_flavor('m1.tiny')['id']) - assert(self.cloud.create_server.call_args[1]['nics'][0]['net-id'] - == self.cloud.get_network('network1')['id']) - - def test_create_server_bad_flavor(self): - ''' - - os_server: - image: cirros - flavor: missing_flavor - nics: - - net-name: network1 - ''' - with pytest.raises(AnsibleFail): - os_server._create_server(self.module, self.cloud) - - assert('missing_flavor' in - self.module.fail_json.call_args[1]['msg']) - - def test_create_server_bad_nic(self): - ''' - - os_server: - image: cirros - flavor: m1.tiny - nics: - - net-name: missing_network - ''' - with pytest.raises(AnsibleFail): - os_server._create_server(self.module, self.cloud) - - assert('missing_network' in - self.module.fail_json.call_args[1]['msg']) From d2106f1c929908aaeaeb53a77f71a1b9b0d59049 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 1 Nov 2016 06:46:22 -0700 Subject: [PATCH 590/770] Need to locate a pip inside a venv when venv is specified Alternative to #5359 Fixes #5347 --- packaging/language/pip.py | 58 ++++++++++++++++++++++++--------------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 95cd7caf5f6..7c8aa8cb78e 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -190,7 +190,7 @@ import os import sys -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import AnsibleModule, is_executable from ansible.module_utils._text import to_native from ansible.module_utils.six import PY3 @@ -263,17 +263,14 @@ def _is_present(name, version, installed_pkgs, pkg_command): def _get_pip(module, env=None, executable=None): - # On Debian and Ubuntu, pip is pip. - # On Fedora18 and up, pip is python-pip. - # On Fedora17 and below, CentOS and RedHat 6 and 5, pip is pip-python. - # On Fedora, CentOS, and RedHat, the exception is in the virtualenv. - # There, pip is just pip. - # On python 3.4, pip should be default, cf PEP 453 - # By default, it will try to use pip required for the current python + # Older pip only installed under the "/usr/bin/pip" name. Many Linux + # distros install it there. + # By default, we try to use pip required for the current python # interpreter, so people can use pip to install modules dependencies - candidate_pip_basenames = ['pip2', 'pip', 'python-pip', 'pip-python'] + candidate_pip_basenames = ('pip2', 'pip') if PY3: - candidate_pip_basenames = ['pip3'] + # pip under python3 installs the "/usr/bin/pip3" name + candidate_pip_basenames = ('pip3',) pip = None if executable is not None: @@ -282,22 +279,39 @@ def _get_pip(module, env=None, executable=None): pip = executable else: # If you define your own executable that executable should be the only candidate. - candidate_pip_basenames = [executable] + # As noted in the docs, executable doesn't work with virtualenvs. + candidate_pip_basenames = (executable,) + if pip is None: if env is None: opt_dirs = [] + for basename in candidate_pip_basenames: + pip = module.get_bin_path(basename, False, opt_dirs) + if pip is not None: + break + else: + # For-else: Means that we did not break out of the loop + # (therefore, that pip was not found) + module.fail_json(msg='Unable to find any of %s to use. pip' + ' needs to be installed.' % ', '.join(candidate_pip_basenames)) else: - # Try pip with the virtualenv directory first. - opt_dirs = ['%s/bin' % env] - for basename in candidate_pip_basenames: - pip = module.get_bin_path(basename, False, opt_dirs) - if pip is not None: - break - # pip should have been found by now. The final call to get_bin_path will - # trigger fail_json. - if pip is None: - basename = candidate_pip_basenames[0] - pip = module.get_bin_path(basename, True, opt_dirs) + # If we're using a virtualenv we must use the pip from the + # virtualenv + venv_dir = os.path.join(env, 'bin') + candidate_pip_basenames = (candidate_pip_basenames[0], 'pip') + for basename in candidate_pip_basenames: + candidate = os.path.join(venv_dir, basename) + if os.path.exists(candidate) and is_executable(candidate): + pip = candidate + break + else: + # For-else: Means that we did not break out of the loop + # (therefore, that pip was not found) + module.fail_json(msg='Unable to find pip in the virtualenv,' + ' %s, under any of these names: %s. Make sure pip is' + ' present in the virtualenv.' % (env, + ', '.join(candidate_pip_basenames))) + return pip From ebcc46fa27de3a62904cd85392b50598271905d2 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 26 Oct 2016 21:11:40 -0400 Subject: [PATCH 591/770] make sure all svcadm operations are syncronous fixes #5296 --- system/service.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/service.py b/system/service.py index 18b76d32c6c..d216e683cae 100644 --- a/system/service.py +++ b/system/service.py @@ -1365,9 +1365,9 @@ def service_control(self): elif self.action == 'stop': subcmd = "disable -st" elif self.action == 'reload': - subcmd = "refresh" + subcmd = "refresh -s" elif self.action == 'restart' and status == 'online': - subcmd = "restart" + subcmd = "restart -s" elif self.action == 'restart' and status != 'online': subcmd = "enable -rst" From 1f6f58052d999fefd138cf9be7f75cc6fd888813 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 1 Nov 2016 14:58:17 -0400 Subject: [PATCH 592/770] minor updates to include docs --- utilities/logic/include.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/utilities/logic/include.py b/utilities/logic/include.py index c251c501dd0..bdca4298bdd 100644 --- a/utilities/logic/include.py +++ b/utilities/logic/include.py @@ -15,9 +15,10 @@ module: include short_description: include a play or task list. description: - - Loads a file with a list of plays or tasks to be executed in the current playbook. + - Includes a file with a list of plays or tasks to be executed in the current playbook. - Files with a list of plays can only be included at the top level, lists of tasks can only be included where tasks normally run (in play). - - Before 2.0 all includes were 'static', executed at play load time. + - Before 2.0 all includes were 'static', executed at play compile time. + - Static includes are not subject to most directives, for example, loops or conditionals, they are applied instead to each inherited task. - Since 2.0 task includes are dynamic and behave more like real tasks. This means they can be looped, skipped and use variables from any source. Ansible tries to auto detect this, use the `static` directive (new in 2.1) to bypass autodetection. version_added: "0.6" From 45760eef44e66336549907c19908e9aa1b1b6935 Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Sat, 20 Aug 2016 14:56:41 -0700 Subject: [PATCH 593/770] Identify existing unmanaged jobs by exact match, when no header comment is found * updated `find_job` method to find by exact match of job, when no matching header comment is found * note this fallback injects a header comment for later calls to `update_job` or `remove_job` * abstracted header comment building to `do_comment` method Fixes #3256 --- system/cron.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/system/cron.py b/system/cron.py index b7739a1c7bc..2ab23ef6e17 100644 --- a/system/cron.py +++ b/system/cron.py @@ -307,9 +307,12 @@ def write(self, backup_file=None): if rc != 0: self.module.fail_json(msg=err) + def do_comment(self, name): + return "%s%s" % (self.ansible, name) + def add_job(self, name, job): # Add the comment - self.lines.append("%s%s" % (self.ansible, name)) + self.lines.append(self.do_comment(name)) # Add the job self.lines.append("%s" % (job)) @@ -370,7 +373,8 @@ def remove_job_file(self): except: raise CronTabError("Unexpected error:", sys.exc_info()[0]) - def find_job(self, name): + def find_job(self, name, job=None): + # attempt to find job by 'Ansible:' header comment comment = None for l in self.lines: if comment is not None: @@ -381,6 +385,19 @@ def find_job(self, name): elif re.match( r'%s' % self.ansible, l): comment = re.sub( r'%s' % self.ansible, '', l) + # failing that, attempt to find job by exact match + if job: + for i, l in enumerate(self.lines): + if l == job: + # if no leading ansible header, insert one + if not re.match( r'%s' % self.ansible, self.lines[i-1]): + self.lines.insert(i, self.do_comment(name)) + return [self.lines[i], l, True] + # if a leading blank ansible header AND job has a name, update header + elif name and self.lines[i-1] == self.do_comment(None): + self.lines[i-1] = self.do_comment(name) + return [self.lines[i-1], l, True] + return [] def find_env(self, name): @@ -431,7 +448,7 @@ def get_envnames(self): return envnames def _update_job(self, name, job, addlinesfunction): - ansiblename = "%s%s" % (self.ansible, name) + ansiblename = self.do_comment(name) newlines = [] comment = None @@ -652,17 +669,22 @@ def main(): crontab.remove_env(name) changed = True else: - old_job = crontab.find_job(name) - if do_install: job = crontab.get_cron_job(minute, hour, day, month, weekday, job, special_time, disabled) + old_job = crontab.find_job(name, job) + if len(old_job) == 0: crontab.add_job(name, job) changed = True if len(old_job) > 0 and old_job[1] != job: crontab.update_job(name, job) changed = True + if len(old_job) > 2: + crontab.update_job(name, job) + changed = True else: + old_job = crontab.find_job(name) + if len(old_job) > 0: crontab.remove_job(name) changed = True From c4342c3c7df725bd894d2ddefeebb225246e5a10 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 6 Oct 2016 11:52:03 -0400 Subject: [PATCH 594/770] allow groups to be passed as YAML list as well keeps backwards copat with 'comma delimited string' fixes #5163 --- system/user.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/user.py b/system/user.py index ab2c4be27a1..f4482924980 100644 --- a/system/user.py +++ b/system/user.py @@ -262,7 +262,7 @@ def __init__(self, module): self.non_unique = module.params['non_unique'] self.seuser = module.params['seuser'] self.group = module.params['group'] - self.groups = module.params['groups'] + self.groups = ','.join(module.params['groups']) self.comment = module.params['comment'] self.shell = module.params['shell'] self.password = module.params['password'] @@ -2080,7 +2080,7 @@ def main(): uid=dict(default=None, type='str'), non_unique=dict(default='no', type='bool'), group=dict(default=None, type='str'), - groups=dict(default=None, type='str'), + groups=dict(default=None, type='list'), comment=dict(default=None, type='str'), home=dict(default=None, type='path'), shell=dict(default=None, type='str'), From 86fc6970be09e4e1d855185f5043ab384a792fec Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 6 Oct 2016 11:58:21 -0400 Subject: [PATCH 595/770] docit --- system/user.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/system/user.py b/system/user.py index f4482924980..fd60b4f0cbf 100644 --- a/system/user.py +++ b/system/user.py @@ -61,9 +61,10 @@ groups: required: false description: - - Puts the user in this comma-delimited list of groups. When set to - the empty string ('groups='), the user is removed from all groups - except the primary group. + - Puts the user in list of groups. When set to the empty string ('groups='), + the user is removed from all groups except the primary group. + - Before version 2.3, the only input format allowed was a 'comma separated string', + now it should be able to accept YAML lists also. append: required: false default: "no" From d3151df16c1312932de001e71ad66aa38053a786 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 1 Feb 2016 16:07:04 -0500 Subject: [PATCH 596/770] added ability to control sleep between attempts the default was 1s but it makes sense to make this configurable --- utilities/logic/wait_for.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/utilities/logic/wait_for.py b/utilities/logic/wait_for.py index d1cf928af64..1eb6a52ad35 100644 --- a/utilities/logic/wait_for.py +++ b/utilities/logic/wait_for.py @@ -78,28 +78,39 @@ description: - port number to poll required: false + default: null state: description: - either C(present), C(started), or C(stopped), C(absent), or C(drained) - When checking a port C(started) will ensure the port is open, C(stopped) will check that it is closed, C(drained) will check for active connections - When checking for a file or a search string C(present) or C(started) will ensure that the file or string is present before continuing, C(absent) will check that file is absent or removed choices: [ "present", "started", "stopped", "absent", "drained" ] + required: False default: "started" path: version_added: "1.4" required: false + default: null description: - path to a file on the filesytem that must exist before continuing search_regex: version_added: "1.4" required: false + default: null description: - Can be used to match a string in either a file or a socket connection. Defaults to a multiline regex. exclude_hosts: version_added: "1.8" required: false + default: null description: - list of hosts or IPs to ignore when looking for active TCP connections for C(drained) state + sleep: + version_added: "2.1" + required: false + default: 1 + description: + - Number of seconds to sleep between checks, before 2.1 this was hardcoded to 1 second. notes: - The ability to use search_regex with a port connection was added in 1.7. requirements: [] @@ -362,7 +373,8 @@ def main(): path=dict(default=None, type='path'), search_regex=dict(default=None), state=dict(default='started', choices=['started', 'stopped', 'present', 'absent', 'drained']), - exclude_hosts=dict(default=None, type='list') + exclude_hosts=dict(default=None, type='list'), + sleep=dict(default=1, type='int') ), ) @@ -407,8 +419,6 @@ def main(): try: f = open(path) f.close() - time.sleep(1) - pass except IOError: break elif port: @@ -416,11 +426,10 @@ def main(): s = _create_connection(host, port, connect_timeout) s.shutdown(socket.SHUT_RDWR) s.close() - time.sleep(1) except: break - else: - time.sleep(1) + # Conditions not yet met, wait and try again + time.sleep(params['sleep']) else: elapsed = datetime.datetime.now() - start if port: @@ -498,7 +507,7 @@ def main(): break # Conditions not yet met, wait and try again - time.sleep(1) + time.sleep(params['sleep']) else: # while-else # Timeout expired @@ -524,7 +533,8 @@ def main(): break except IOError: pass - time.sleep(1) + # Conditions not yet met, wait and try again + time.sleep(params['sleep']) else: elapsed = datetime.datetime.now() - start module.fail_json(msg="Timeout when waiting for %s:%s to drain" % (host, port), elapsed=elapsed.seconds) From b8eb9769a32c88a792851913b1d28c47f6ebf76f Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 19 May 2016 11:52:46 -0400 Subject: [PATCH 597/770] added logout to docker_login also cleaned up 'actions' and minor doc issues --- cloud/docker/docker_login.py | 84 ++++++++++++++++++++++++------------ 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 0af5b4fd947..52f5f14c2e0 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -24,20 +24,17 @@ DOCUMENTATION = ''' --- module: docker_login - short_description: Log into a Docker registry. - version_added: "2.0" - description: - Provides functionality similar to the "docker login" command. - Authenticate with a docker registry and add the credentials to your local Docker config file. Adding the credentials to the config files allows future connections to the registry using tools such as Ansible's Docker modules, the Docker CLI and docker-py without needing to provide credentials. - Running in check mode will perform the authentication without updating the config file. - options: registry_url: + required: False description: - The registry URL. default: "https://index.docker.io/v1/" @@ -47,40 +44,51 @@ username: description: - The username for the registry account - required: true - default: null + required: True password: description: - The plaintext password for the registry account - required: true - default: null + required: True email: + required: False description: - "The email address for the registry account. NOTE: private registries may not require this, but Docker Hub requires it." default: None reauthorize: + required: False description: - Refresh exiting authentication found in the configuration file. - default: false + default: no + choices: ['yes', 'no'] aliases: - reauth config_path: description: - Custom path to the Docker CLI configuration file. default: ~/.docker/config.json + required: False aliases: - self.config_path - dockercfg_path + state: + version_added: '2.2' + description: + - This controls the current state of the user. C(present) will login in a user, C(absent) will log him out. + - To logout you only need the registry server, which defaults to DockerHub. + - Before 2.1 you could ONLY log in. + - docker does not support 'logout' with a custom config file. + choices: ['present', 'absent'] + default: 'present' + required: False extends_documentation_fragment: - docker - requirements: - "python >= 2.6" - "docker-py >= 1.7.0" - "Docker API >= 1.20" - + - 'Only to be able to logout (state=absent): the docker command line utility' authors: - "Olaf Kilian " - "Chris Houseknecht (@chouseknecht)" @@ -109,20 +117,16 @@ email: docker@docker.io config_path: /tmp/.mydockercfg +- name: Log out of DockerHub + docker_login: + state: absent + email: docker@docker.com ''' RETURN = ''' -actions: - description: List of actions taken by the module. - returned: always - type: list - sample: [ - "Log into https://index.docker.io/v1/", - "Updated config file /Users/chouseknecht/.docker/config.json with new authorization for https://index.docker.io/v1/" - ] login_results: description: Results from the login. - returned: always + returned: when state='present' type: dict sample: { "email": "testuer@yahoo.com", @@ -155,7 +159,10 @@ def __init__(self, client, results): self.reauthorize = parameters.get('reauthorize') self.config_path = parameters.get('config_path') - self.login() + if parameters['state'] == 'present': + self.login() + else: + self.logout() def fail(self, msg): self.client.fail(msg) @@ -190,6 +197,24 @@ def login(self): if not self.check_mode: self.update_config_file() + def logout(self): + ''' + Log out of the registry. On success update the config file. + TODO: port to API once docker.py supports this. + + :return: None + ''' + + cmd = "%s logout " % self.client.module.get_bin_path('docker', True) + #TODO: docker does not support config file in logout, restore this when they do + #if self.config_path and self.config_file_exists(self.config_path): + # cmd += "--config '%s' " % self.config_path + cmd += "'%s'" % self.registry_url + + (rc, out, err) = self.client.module.run_command(cmd) + if rc != 0: + self.fail("Could not log out: %s" % err) + def config_file_exists(self, path): if os.path.exists(path): self.log("Configuration file %s exists" % (path)) @@ -223,7 +248,7 @@ def update_config_file(self): ''' If the authorization not stored in the config file or reauthorize is True, update the config file with the new authorization. - + :return: None ''' @@ -265,16 +290,16 @@ def main(): argument_spec=dict( registry_url=dict(type='str', required=False, default=DEFAULT_DOCKER_REGISTRY, aliases=['registry', 'url']), - username=dict(type='str', required=True), - password=dict(type='str', required=True, no_log=True), + username=dict(type='str', required=False), + password=dict(type='str', required=False, no_log=True), email=dict(type='str'), reauthorize=dict(type='bool', default=False, aliases=['reauth']), - config_path=dict(type='str', default='~/.docker/config.json', aliases=['self.config_path', - 'dockercfg_path']), + state=dict(type='str', default='present', choices=['present', 'absent']), + config_path=dict(type='str', default='~/.docker/config.json', aliases=['self.config_path', 'dockercfg_path']), ) required_if = [ - ('registry_url', DEFAULT_DOCKER_REGISTRY, ['email']) + ('state', 'present', ['username', 'password']), ] client = AnsibleDockerClient( @@ -289,7 +314,12 @@ def main(): login_result={} ) + if module.params['state'] == 'present' and module.params['registry_url'] == DEFAULT_DOCKER_REGISTRY and not module.params['email']: + module.fail_json(msg="'email' is required when loging into DockerHub") + LoginManager(client, results) + if 'actions' in results: + del results['actions'] client.module.exit_json(**results) # import module snippets From 859180d0133343751efade55d1799d9da365d417 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Tue, 1 Nov 2016 18:17:51 -0400 Subject: [PATCH 598/770] corrected version added --- cloud/docker/docker_login.py | 2 +- utilities/logic/wait_for.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index 52f5f14c2e0..bee5be46e67 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -72,7 +72,7 @@ - self.config_path - dockercfg_path state: - version_added: '2.2' + version_added: '2.3' description: - This controls the current state of the user. C(present) will login in a user, C(absent) will log him out. - To logout you only need the registry server, which defaults to DockerHub. diff --git a/utilities/logic/wait_for.py b/utilities/logic/wait_for.py index 1eb6a52ad35..d57d18d7b92 100644 --- a/utilities/logic/wait_for.py +++ b/utilities/logic/wait_for.py @@ -106,11 +106,11 @@ description: - list of hosts or IPs to ignore when looking for active TCP connections for C(drained) state sleep: - version_added: "2.1" + version_added: "2.3" required: false default: 1 description: - - Number of seconds to sleep between checks, before 2.1 this was hardcoded to 1 second. + - Number of seconds to sleep between checks, before 2.3 this was hardcoded to 1 second. notes: - The ability to use search_regex with a port connection was added in 1.7. requirements: [] From 5890fba5dcdb47aba19857b640cab8c5ec793230 Mon Sep 17 00:00:00 2001 From: jctanner Date: Tue, 1 Nov 2016 19:30:50 -0400 Subject: [PATCH 599/770] apt: update cache until corrupt package lists are fixed (#5448) * apt: If the cache object fails to lost due to a corrupt file, try to update the cache until it is fixed. * Append -q to the update parameters * Remove unused variable * Use a string that doesn't rely on internationalization * Use py24 exception style * Use get_exception Fixes #2951 --- packaging/os/apt.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 3983b6bd854..be8e1016a62 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -755,6 +755,31 @@ def get_updated_cache_time(): return mtimestamp, updated_cache_time +# https://github.com/ansible/ansible-modules-core/issues/2951 +def get_cache(module): + '''Attempt to get the cache object and update till it works''' + cache = None + try: + cache = apt.Cache() + except SystemError: + e = get_exception() + if '/var/lib/apt/lists/' in str(e).lower(): + # update cache until files are fixed or retries exceeded + retries = 0 + while retries < 2: + (rc, so, se) = module.run_command(['apt-get', 'update', '-q']) + retries += 1 + if rc == 0: + break + if rc != 0: + module.fail_json(msg='Updating the cache to correct corrupt package lists failed:\n%s\n%s' % (str(e), str(so) + str(se))) + # try again + cache = apt.Cache() + else: + module.fail_json(msg=str(e)) + return cache + + def main(): module = AnsibleModule( argument_spec = dict( @@ -821,8 +846,10 @@ def main(): if p['state'] == 'removed': p['state'] = 'absent' + # Get the cache object + cache = get_cache(module) + try: - cache = apt.Cache() if p['default_release']: try: apt_pkg.config['APT::Default-Release'] = p['default_release'] From e69e1ca2c84b428fc31fa77157626894333fd88f Mon Sep 17 00:00:00 2001 From: Jamie Dyer Date: Wed, 2 Nov 2016 01:26:15 +0000 Subject: [PATCH 600/770] Make the RDS endpoint available if AWS returns it. Fixes #3865 (#4143) --- cloud/amazon/rds.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py index 15ec55e8305..cad4dbb1412 100644 --- a/cloud/amazon/rds.py +++ b/cloud/amazon/rds.py @@ -539,8 +539,8 @@ def get_data(self): 'iops' : self.instance.iops } - # Endpoint exists only if the instance is available - if self.status == 'available': + # Only assign an Endpoint if one is available + if hasattr(self.instance, 'endpoint'): d["endpoint"] = self.instance.endpoint[0] d["port"] = self.instance.endpoint[1] if self.instance.vpc_security_groups is not None: @@ -587,9 +587,9 @@ def get_data(self): } if self.instance["VpcSecurityGroups"] is not None: d['vpc_security_groups'] = ','.join(x['VpcSecurityGroupId'] for x in self.instance['VpcSecurityGroups']) - if self.status == 'available': - d['endpoint'] = self.instance["Endpoint"]["Address"] - d['port'] = self.instance["Endpoint"]["Port"] + if "Endpoint" in self.instance and self.instance["Endpoint"] is not None: + d['endpoint'] = self.instance["Endpoint"].get('Address', None) + d['port'] = self.instance["Endpoint"].get('Port', None) else: d['endpoint'] = None d['port'] = None From ebc3a0628fe752e3d650b2d62b9b267a3086c425 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Wed, 2 Nov 2016 13:49:55 +0000 Subject: [PATCH 601/770] Correct typos in docs strings (#5233) --- network/netvisor/pn_trunk.py | 4 ++-- network/nxos/nxos_ntp_options.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/network/netvisor/pn_trunk.py b/network/netvisor/pn_trunk.py index 9d06cfc668e..68fcb07df5d 100644 --- a/network/netvisor/pn_trunk.py +++ b/network/netvisor/pn_trunk.py @@ -105,10 +105,10 @@ pn_mirror_receive: description: - Specify if the configuration receives mirrored traffic. - pn_unkown_ucast_level: + pn_unknown_ucast_level: description: - Specify an unkown unicast level in percent. The default value is 100%. - pn_unkown_mcast_level: + pn_unknown_mcast_level: description: - Specify an unkown multicast level in percent. The default value is 100%. pn_broadcast_level: diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py index 9c03013241f..fbc1cd5745b 100644 --- a/network/nxos/nxos_ntp_options.py +++ b/network/nxos/nxos_ntp_options.py @@ -41,7 +41,7 @@ required: false default: null choices: ['true','false'] - stratrum: + stratum: description: - If C(master=true), an optional stratum can be supplied (1-15). The device default is 8. From 76fd19c3cad3c1f5db66aa6668ffcfe01bb5ac2c Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 2 Nov 2016 10:15:51 -0400 Subject: [PATCH 602/770] dont join when group is none --- system/user.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/user.py b/system/user.py index fd60b4f0cbf..c424beea4b5 100644 --- a/system/user.py +++ b/system/user.py @@ -263,7 +263,6 @@ def __init__(self, module): self.non_unique = module.params['non_unique'] self.seuser = module.params['seuser'] self.group = module.params['group'] - self.groups = ','.join(module.params['groups']) self.comment = module.params['comment'] self.shell = module.params['shell'] self.password = module.params['password'] @@ -283,6 +282,10 @@ def __init__(self, module): self.update_password = module.params['update_password'] self.home = module.params['home'] self.expires = None + self.groups = None + + if module.params['groups'] is not None: + self.groups = ','.join(module.params['groups']) if module.params['expires']: try: From 258dd810e93cc979c07fd130162ebd903e286a3c Mon Sep 17 00:00:00 2001 From: Chris Becker Date: Wed, 2 Nov 2016 15:30:52 -0400 Subject: [PATCH 603/770] Add more specific language to module description and examples * Add 'on the remote server' to `file` parameter description * Add example showing how to use the `file` parameter, with specific language about the file's location being on the 'remote server' --- packaging/os/apt_key.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index 7822578d040..516255e1a41 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -47,7 +47,7 @@ required: false default: none description: - - path to a keyfile to add to the keyring + - path to a keyfile on the remote server to add to the keyring keyring: required: false default: none @@ -102,6 +102,9 @@ # Add an Apt signing key to a specific keyring file - apt_key: id=473041FA url=https://ftp-master.debian.org/keys/archive-key-6.0.asc keyring=/etc/apt/trusted.gpg.d/debian.gpg state=present + +# Add Apt signing key on remote server to keyring +- apt_key: id=473041FA file=/tmp/apt.gpg state=present ''' From 375aa92315961a75032d13d02315e94ae6a4377e Mon Sep 17 00:00:00 2001 From: Allen Sanabria Date: Tue, 30 Aug 2016 15:52:59 -0700 Subject: [PATCH 604/770] Updated documentation for PR http://github.com/ansible/ansible/pull/17207 --- utilities/logic/include_vars.py | 52 +++++++++++++++++++++++++++++---- 1 file changed, 47 insertions(+), 5 deletions(-) diff --git a/utilities/logic/include_vars.py b/utilities/logic/include_vars.py index a0b3280d807..811fd99ecdb 100644 --- a/utilities/logic/include_vars.py +++ b/utilities/logic/include_vars.py @@ -10,28 +10,47 @@ DOCUMENTATION = ''' --- -author: "Benno Joy (@bennojoy)" +author: "Allen Sanabria (@linuxdynasty)" module: include_vars short_description: Load variables from files, dynamically within a task. description: - - Loads variables from a YAML/JSON file dynamically during task runtime. It can work with conditionals, or use host specific variables to determine the path name to load from. + - Loads variables from a YAML/JSON files dynamically from within a file or + from a directory recursively during task runtime. If loading a directory, the files are sorted alphabetically before being loaded. options: file: version_added: "2.2" description: - The file name from which variables should be loaded. - If the path is relative, it will look for the file in vars/ subdirectory of a role or relative to playbook. + dir: + version_added: "2.2" + description: + - The directory name from which the variables should be loaded. + - If the path is relative, it will look for the file in vars/ subdirectory of a role or relative to playbook. + default: null name: version_added: "2.2" description: - The name of a variable into which assign the included vars, if omitted (null) they will be made top level vars. default: null + depth: + version_added: "2.2" + description: + - By default, this module will recursively go through each sub directory and load up the variables. By explicitly setting the depth, this module will only go as deep as the depth. + default: 0 + files_matching: + version_added: "2.2" + description: + - Limit the variables that are loaded within any directory to this regular expression. + default: null + ignore_files: + version_added: "2.2" + description: + - List of file names to ignore. The defaults can not be overridden, but can be extended. + default: null free-form: description: - This module allows you to specify the 'file' option directly w/o any other options. -notes: - - The file is always required either as the explicit option or using the free-form. -version_added: "1.4" ''' EXAMPLES = """ @@ -54,4 +73,27 @@ # bare include (free-form) - include_vars: myvars.yml +# Include all yml files in vars/all and all nested directories +- include_vars: + dir: 'vars/all' + +# Include all yml files in vars/all and all nested directories and save the output in test. +- include_vars: + dir: 'vars/all' + name: test + +# Include all yml files in vars/services +- include_vars: + dir: 'vars/services' + depth: 1 + +# Include only bastion.yml files +- include_vars: + dir: 'vars' + files_matching: 'bastion.yml' + +# Include only all yml files exception bastion.yml +- include_vars: + dir: 'vars' + ignore_files: 'bastion.yml' """ From 872a3822c25db6cbb40e35feae1dae0cd56e49e8 Mon Sep 17 00:00:00 2001 From: Ricardo Carrillo Cruz Date: Thu, 3 Nov 2016 02:21:41 +0100 Subject: [PATCH 605/770] Add rebuild support to os_server_actions (#4289) Fixes #2714 --- cloud/openstack/os_server_actions.py | 33 ++++++++++++++++++++++++---- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/cloud/openstack/os_server_actions.py b/cloud/openstack/os_server_actions.py index 44ff6afc8a8..e298fb9ea29 100644 --- a/cloud/openstack/os_server_actions.py +++ b/cloud/openstack/os_server_actions.py @@ -35,6 +35,7 @@ description: - Perform server actions on an existing compute instance from OpenStack. This module does not return any data other than changed true/false. + When I(action) is 'rebuild', then I(image) parameter is required. options: server: description: @@ -55,8 +56,14 @@ description: - Perform the given action. The lock and unlock actions always return changed as the servers API does not provide lock status. - choices: [stop, start, pause, unpause, lock, unlock, suspend, resume] + choices: [stop, start, pause, unpause, lock, unlock, suspend, resume, + rebuild] default: present + image: + description: + - Image the server should be rebuilt with + default: null + version_added: "2.3" requirements: - "python >= 2.6" - "shade" @@ -82,7 +89,8 @@ 'lock': 'ACTIVE', # API doesn't show lock/unlock status 'unlock': 'ACTIVE', 'suspend': 'SUSPENDED', - 'resume': 'ACTIVE',} + 'resume': 'ACTIVE', + 'rebuild': 'ACTIVE'} _admin_actions = ['pause', 'unpause', 'suspend', 'resume', 'lock', 'unlock'] @@ -113,11 +121,15 @@ def main(): argument_spec = openstack_full_argument_spec( server=dict(required=True), action=dict(required=True, choices=['stop', 'start', 'pause', 'unpause', - 'lock', 'unlock', 'suspend', 'resume']), + 'lock', 'unlock', 'suspend', 'resume', + 'rebuild']), + image=dict(required=False), ) module_kwargs = openstack_module_kwargs() - module = AnsibleModule(argument_spec, supports_check_mode=True, **module_kwargs) + module = AnsibleModule(argument_spec, supports_check_mode=True, + required_if=[('action', 'rebuild', ['image'])], + **module_kwargs) if not HAS_SHADE: module.fail_json(msg='shade is required for this module') @@ -125,6 +137,7 @@ def main(): action = module.params['action'] wait = module.params['wait'] timeout = module.params['timeout'] + image = module.params['image'] try: if action in _admin_actions: @@ -203,6 +216,18 @@ def main(): _wait(timeout, cloud, server, action) module.exit_json(changed=True) + elif action == 'rebuild': + image = cloud.get_image(image) + + if image is None: + module.fail_json(msg="Image does not exist") + + # rebuild doesn't set a state, just do it + cloud.nova_client.servers.rebuild(server=server.id, image=image.id) + if wait: + _wait(timeout, cloud, server, action) + module.exit_json(changed=True) + except shade.OpenStackCloudException as e: module.fail_json(msg=str(e), extra_data=e.extra_data) From cb1d90ccfd2f3ed62fc8fb1e6620463f5c9c15fe Mon Sep 17 00:00:00 2001 From: zuiurs Date: Mon, 31 Oct 2016 16:47:55 +0900 Subject: [PATCH 606/770] fix the behavior that the dest is directory This fixes the behavior that the dest is directory, when we set the "force: no" argument. To be join the dest and the src's basename, before checking the "force" argument. --- files/copy.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/files/copy.py b/files/copy.py index 9066226540a..d42c285ecc2 100644 --- a/files/copy.py +++ b/files/copy.py @@ -284,18 +284,19 @@ def main(): directory_args['mode'] = None adjust_recursive_directory_permissions(pre_existing_dir, new_directory_list, module, directory_args, changed) + if os.path.isdir(b_dest): + basename = os.path.basename(src) + if original_basename: + basename = original_basename + dest = os.path.join(dest, basename) + b_dest = to_bytes(dest, errors='surrogate_or_strict') + if os.path.exists(b_dest): if os.path.islink(b_dest) and follow: b_dest = os.path.realpath(b_dest) dest = to_native(b_dest, errors='surrogate_or_strict') if not force: module.exit_json(msg="file already exists", src=src, dest=dest, changed=False) - if os.path.isdir(b_dest): - basename = os.path.basename(src) - if original_basename: - basename = original_basename - dest = os.path.join(dest, basename) - b_dest = to_bytes(dest, errors='surrogate_or_strict') if os.access(b_dest, os.R_OK): checksum_dest = module.sha1(dest) else: From 7cc4d3fe04f633f4090d56292f04aecaa2c45735 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Wed, 2 Nov 2016 21:43:40 -0700 Subject: [PATCH 607/770] Simplify compileall checks. Remove py3 skip list. --- test/utils/shippable/sanity-skip-python24.txt | 2 +- test/utils/shippable/sanity-skip-python3.txt | 0 test/utils/shippable/sanity-test-python24.txt | 2 -- test/utils/shippable/sanity.sh | 3 +-- 4 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 test/utils/shippable/sanity-skip-python3.txt delete mode 100644 test/utils/shippable/sanity-test-python24.txt diff --git a/test/utils/shippable/sanity-skip-python24.txt b/test/utils/shippable/sanity-skip-python24.txt index 1434a04094b..1a0a28c4d49 100644 --- a/test/utils/shippable/sanity-skip-python24.txt +++ b/test/utils/shippable/sanity-skip-python24.txt @@ -1 +1 @@ -/cloud/ +/cloud/[^/]+/(?!(ec2_facts.py|_ec2_ami_search.py)) diff --git a/test/utils/shippable/sanity-skip-python3.txt b/test/utils/shippable/sanity-skip-python3.txt deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/test/utils/shippable/sanity-test-python24.txt b/test/utils/shippable/sanity-test-python24.txt deleted file mode 100644 index 5ad993ee773..00000000000 --- a/test/utils/shippable/sanity-test-python24.txt +++ /dev/null @@ -1,2 +0,0 @@ -cloud/amazon/_ec2_ami_search.py -cloud/amazon/ec2_facts.py diff --git a/test/utils/shippable/sanity.sh b/test/utils/shippable/sanity.sh index 3b754afa7b0..8c1453022e7 100755 --- a/test/utils/shippable/sanity.sh +++ b/test/utils/shippable/sanity.sh @@ -29,11 +29,10 @@ fi validate_modules="${build_dir}/test/sanity/validate-modules/validate-modules" -python2.4 -m compileall -fq -i "test/utils/shippable/sanity-test-python24.txt" python2.4 -m compileall -fq -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python24.txt"))" | tr '\n' '|')" . python2.6 -m compileall -fq . python2.7 -m compileall -fq . -python3.5 -m compileall -fq . -x "($(printf %s "$(< "test/utils/shippable/sanity-skip-python3.txt"))" | tr '\n' '|')" +python3.5 -m compileall -fq . ANSIBLE_DEPRECATION_WARNINGS=false \ "${validate_modules}" --exclude '/utilities/|/shippable(/|$)' . From 69e3aa12de5ff6fc81cdb2f14cd588969d09974f Mon Sep 17 00:00:00 2001 From: Matt Robinson Date: Thu, 3 Nov 2016 22:31:19 +0000 Subject: [PATCH 608/770] apt_repository: Relax PPA checks and add basename (#5432) Allow installation of PPA repositories on non-Ubuntu Debian derived distribution targets (e.g. neon, Mint, Debian itself) by removing the specific check for UbuntuDistribution before allowing PPA: format sources. This fixes the addition of PPA repositories under KDE neon (as the codenames match the base Ubuntu distribution). To make the functionality also useful under Mint and Debian which have different codenames to their Ubuntu upstream / downstream releases, add a 'codename' option to override the default used in the PPA source entry. --- packaging/os/apt_repository.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index eae6228034f..dac098dab27 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -28,9 +28,8 @@ description: - Add or remove an APT repositories in Ubuntu and Debian. notes: - - This module works on Debian and Ubuntu. + - This module works on Debian, Ubuntu and their derivatives. - This module supports Debian Squeeze (version 6) as well as its successors. - - This module treats Debian and Ubuntu distributions separately. So PPA could be installed only on Ubuntu machines. options: repo: required: true @@ -70,6 +69,12 @@ Defaults to a file name based on the repository source url. The .list extension will be automatically added. required: false + codename: + version_added: '2.3' + description: + - Override the distribution codename to use for PPA repositories. + Should usually only be set when working with a PPA on a non-Ubuntu target (e.g. Debian or Mint) + required: false author: "Alexander Saltanov (@sashka)" version_added: "0.7" requirements: @@ -90,9 +95,11 @@ # Remove specified repository from sources list. apt_repository: repo='deb http://archive.canonical.com/ubuntu hardy partner' state=absent -# On Ubuntu target: add nginx stable repository from PPA and install its signing key. -# On Debian target: adding PPA is not available, so it will fail immediately. +# Add nginx stable repository from PPA and install its signing key. +# On Ubuntu target: apt_repository: repo='ppa:nginx/stable' +# On Debian target +apt_repository: repo='ppa:nginx/stable' codename='trusty' ''' import glob @@ -375,6 +382,7 @@ class UbuntuSourcesList(SourcesList): def __init__(self, module, add_ppa_signing_keys_callback=None): self.module = module self.add_ppa_signing_keys_callback = add_ppa_signing_keys_callback + self.codename = module.params['codename'] or distro.codename super(UbuntuSourcesList, self).__init__(module) def _get_ppa_info(self, owner_name, ppa_name): @@ -394,7 +402,7 @@ def _expand_ppa(self, path): except IndexError: ppa_name = 'ppa' - line = 'deb http://ppa.launchpad.net/%s/%s/ubuntu %s main' % (ppa_owner, ppa_name, distro.codename) + line = 'deb http://ppa.launchpad.net/%s/%s/ubuntu %s main' % (ppa_owner, ppa_name, self.codename) return line, ppa_owner, ppa_name def _key_already_exists(self, key_fingerprint): @@ -415,7 +423,7 @@ def add_source(self, line, comment='', file=None): command = ['apt-key', 'adv', '--recv-keys', '--keyserver', 'hkp://keyserver.ubuntu.com:80', info['signing_key_fingerprint']] self.add_ppa_signing_keys_callback(command) - file = file or self._suggest_filename('%s_%s' % (line, distro.codename)) + file = file or self._suggest_filename('%s_%s' % (line, self.codename)) else: source = self._parse(line, raise_if_invalid_or_disabled=True)[2] file = file or self._suggest_filename(source) @@ -469,6 +477,7 @@ def main(): # this should not be needed, but exists as a failsafe install_python_apt=dict(required=False, default="yes", type='bool'), validate_certs = dict(default='yes', type='bool'), + codename = dict(required=False), ), supports_check_mode=True, ) @@ -487,13 +496,11 @@ def main(): else: module.fail_json(msg='%s is not installed, and install_python_apt is False' % PYTHON_APT) - if isinstance(distro, aptsources_distro.UbuntuDistribution): + if isinstance(distro, aptsources_distro.Distribution): sourceslist = UbuntuSourcesList(module, add_ppa_signing_keys_callback=get_add_ppa_signing_key_callback(module)) - elif isinstance(distro, aptsources_distro.Distribution): - sourceslist = SourcesList(module) else: - module.fail_json(msg='Module apt_repository supports only Debian and Ubuntu.') + module.fail_json(msg='Module apt_repository is not supported on target.') sources_before = sourceslist.dump() From be846c0995c1052929977ef2fd92b78e4852bad1 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 3 Nov 2016 21:41:17 -0400 Subject: [PATCH 609/770] ios_facts module will no longer error on missing command (#5491) The module will error if it tries to use a cli command that is not available on a given platform. This fix will address that problem. If the cli command is not available, then the command is silently discarded and the facts that the command output is based on is not returned. Any failed commands are provided in the module return under the failed_commands key. This fix also updates the Examples docstring to make it consistent with other ios_* modules fixes #5444 fixes #5372 --- network/ios/ios_facts.py | 116 +++++++++++++++++++-------------------- 1 file changed, 56 insertions(+), 60 deletions(-) diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py index 503d394b41f..c030e78a961 100644 --- a/network/ios/ios_facts.py +++ b/network/ios/ios_facts.py @@ -42,19 +42,31 @@ """ EXAMPLES = """ +# Note: examples below use the following provider dict to handle +# transport and authentication to the node. +vars: + cli: + host: "{{ inventory_hostname }}" + username: cisco + password: cisco + transport: cli + # Collect all facts from the device - ios_facts: gather_subset: all + provider: "{{ cli }}" # Collect only the config and default facts - ios_facts: gather_subset: - config + provider: "{{ cli }}" # Do not collect hardware facts - ios_facts: gather_subset: - "!hardware" + provider: "{{ cli }}" """ RETURN = """ @@ -127,44 +139,35 @@ import itertools import ansible.module_utils.ios -from ansible.module_utils.netcli import CommandRunner, AddCommandError from ansible.module_utils.network import NetworkModule from ansible.module_utils.six import iteritems from ansible.module_utils.six.moves import zip -def add_command(runner, command): - try: - runner.add_command(command) - except AddCommandError: - # AddCommandError is raised for any issue adding a command to - # the runner. Silently ignore the exception in this case - pass - class FactsBase(object): - def __init__(self, runner): - self.runner = runner + def __init__(self, module): + self.module = module self.facts = dict() + self.failed_commands = list() - self.commands() + def run(self, cmd): + try: + return self.module.cli(cmd)[0] + except: + self.failed_commands.append(cmd) - def commands(self): - raise NotImplementedError class Default(FactsBase): - def commands(self): - add_command(self.runner, 'show version') - def populate(self): - data = self.runner.get_command('show version') - - self.facts['version'] = self.parse_version(data) - self.facts['serialnum'] = self.parse_serialnum(data) - self.facts['model'] = self.parse_model(data) - self.facts['image'] = self.parse_image(data) - self.facts['hostname'] = self.parse_hostname(data) + data = self.run('show version') + if data: + self.facts['version'] = self.parse_version(data) + self.facts['serialnum'] = self.parse_serialnum(data) + self.facts['model'] = self.parse_model(data) + self.facts['image'] = self.parse_image(data) + self.facts['hostname'] = self.parse_hostname(data) def parse_version(self, data): match = re.search(r'Version (\S+),', data) @@ -194,20 +197,17 @@ def parse_serialnum(self, data): class Hardware(FactsBase): - def commands(self): - add_command(self.runner, 'dir | include Directory') - add_command(self.runner, 'show version') - add_command(self.runner, 'show memory statistics | include Processor') - def populate(self): - data = self.runner.get_command('dir | include Directory') - self.facts['filesystems'] = self.parse_filesystems(data) + data = self.run('dir | include Directory') + if data: + self.facts['filesystems'] = self.parse_filesystems(data) - data = self.runner.get_command('show memory statistics | include Processor') - match = re.findall(r'\s(\d+)\s', data) - if match: - self.facts['memtotal_mb'] = int(match[0]) / 1024 - self.facts['memfree_mb'] = int(match[1]) / 1024 + data = self.run('show memory statistics | include Processor') + if data: + match = re.findall(r'\s(\d+)\s', data) + if match: + self.facts['memtotal_mb'] = int(match[0]) / 1024 + self.facts['memfree_mb'] = int(match[1]) / 1024 def parse_filesystems(self, data): return re.findall(r'^Directory of (\S+)/', data, re.M) @@ -215,37 +215,33 @@ def parse_filesystems(self, data): class Config(FactsBase): - def commands(self): - add_command(self.runner, 'show running-config') - def populate(self): - self.facts['config'] = self.runner.get_command('show running-config') + data = self.run('show running-config') + if data: + self.facts['config'] = data class Interfaces(FactsBase): - def commands(self): - add_command(self.runner, 'show interfaces') - add_command(self.runner, 'show ipv6 interface') - add_command(self.runner, 'show lldp') - add_command(self.runner, 'show lldp neighbors detail') - def populate(self): self.facts['all_ipv4_addresses'] = list() self.facts['all_ipv6_addresses'] = list() - data = self.runner.get_command('show interfaces') - interfaces = self.parse_interfaces(data) - self.facts['interfaces'] = self.populate_interfaces(interfaces) + data = self.run('show interfaces') + if data: + interfaces = self.parse_interfaces(data) + self.facts['interfaces'] = self.populate_interfaces(interfaces) - data = self.runner.get_command('show ipv6 interface') - if len(data) > 0: + data = self.run('show ipv6 interface') + if data: data = self.parse_interfaces(data) self.populate_ipv6_interfaces(data) - if 'LLDP is not enabled' not in self.runner.get_command('show lldp'): - neighbors = self.runner.get_command('show lldp neighbors detail') - self.facts['neighbors'] = self.parse_neighbors(neighbors) + data = self.run('show lldp') + if 'LLDP is not enabled' not in data: + neighbors = self.run('show lldp neighbors detail') + if neighbors: + self.facts['neighbors'] = self.parse_neighbors(neighbors) def populate_interfaces(self, interfaces): facts = dict() @@ -434,27 +430,27 @@ def main(): facts = dict() facts['gather_subset'] = list(runable_subsets) - runner = CommandRunner(module) - instances = list() for key in runable_subsets: - instances.append(FACT_SUBSETS[key](runner)) + instances.append(FACT_SUBSETS[key](module)) - runner.run() + failed_commands = list() try: for inst in instances: inst.populate() + failed_commands.extend(inst.failed_commands) facts.update(inst.facts) except Exception: - module.exit_json(out=module.from_json(runner.items)) + exc = get_exception() + module.fail_json(msg=str(exc)) ansible_facts = dict() for key, value in iteritems(facts): key = 'ansible_net_%s' % key ansible_facts[key] = value - module.exit_json(ansible_facts=ansible_facts) + module.exit_json(ansible_facts=ansible_facts, failed_commands=failed_commands) if __name__ == '__main__': From 2584fca0ae3b323e7be64e139efe19ea26180269 Mon Sep 17 00:00:00 2001 From: jctanner Date: Thu, 3 Nov 2016 22:07:04 -0400 Subject: [PATCH 610/770] Re-add version_added to include_vars (#5493) --- utilities/logic/include_vars.py | 1 + 1 file changed, 1 insertion(+) diff --git a/utilities/logic/include_vars.py b/utilities/logic/include_vars.py index 811fd99ecdb..87747bb7e3b 100644 --- a/utilities/logic/include_vars.py +++ b/utilities/logic/include_vars.py @@ -16,6 +16,7 @@ description: - Loads variables from a YAML/JSON files dynamically from within a file or from a directory recursively during task runtime. If loading a directory, the files are sorted alphabetically before being loaded. +version_added: "1.4" options: file: version_added: "2.2" From a26eadef5b14179c5e8a3408ebeb5daece835488 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Thu, 3 Nov 2016 22:47:58 -0400 Subject: [PATCH 611/770] fixes problem when trying load banner into ios device (#5494) this fix will now handle loading a multiline banner on ios based devices without hanging. It separates the processing of banners from the remainder of the config link #5318 --- network/ios/ios_config.py | 56 ++++++++++++++++++++++++++++++++++----- 1 file changed, 50 insertions(+), 6 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 1022419a8ab..26a318de378 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -202,50 +202,91 @@ sample: /playbooks/ansible/backup/ios_config.2016-07-16@22:28:34 """ import re +import time from ansible.module_utils.basic import get_exception +from ansible.module_utils.six import iteritems from ansible.module_utils.ios import NetworkModule, NetworkError from ansible.module_utils.netcfg import NetworkConfig, dumps from ansible.module_utils.netcli import Command + def check_args(module, warnings): if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' 'removed in the future') +def extract_banners(config): + banners = {} + for cmd in ['exec', 'login', 'incoming']: + regex = r'banner %s \^C(.+?)(?=\^C)' % cmd + match = re.search(regex, config, re.S) + if match: + key = 'banner %s' % cmd + banners[key] = match.group(1).strip() + config = config.replace(str(match.group(1)), '') + + config = re.sub(r'banner \w+ \^C\^C', '!! banner removed', config) + return (config, banners) + +def diff_banners(want, have): + candidate = {} + for key, value in iteritems(want): + if value != have.get(key): + candidate[key] = value + return candidate + +def load_banners(module, banners): + for key, value in iteritems(banners): + key += ' @' + for cmd in ['config terminal', key, value, '@', 'end']: + cmd += '\r' + module.connection.shell.shell.sendall(cmd) + time.sleep(1) + module.connection.shell.receive() + def get_config(module, result): contents = module.params['config'] if not contents: defaults = module.params['defaults'] contents = module.config.get_config(include_defaults=defaults) - return NetworkConfig(indent=1, contents=contents) + + contents, banners = extract_banners(contents) + return NetworkConfig(indent=1, contents=contents), banners def get_candidate(module): candidate = NetworkConfig(indent=1) + banners = {} + if module.params['src']: - candidate.load(module.params['src']) + src, banners = extract_banners(module.params['src']) + candidate.load(src) + elif module.params['lines']: parents = module.params['parents'] or list() candidate.add(module.params['lines'], parents=parents) - return candidate + + return candidate, banners def run(module, result): match = module.params['match'] replace = module.params['replace'] path = module.params['parents'] - candidate = get_candidate(module) + candidate, want_banners = get_candidate(module) if match != 'none': - config = get_config(module, result) + config, have_banners = get_config(module, result) path = module.params['parents'] configobjs = candidate.difference(config, path=path,match=match, replace=replace) else: configobjs = candidate.items - if configobjs: + banners = diff_banners(want_banners, have_banners) + + if configobjs or banners: commands = dumps(configobjs, 'commands').split('\n') if module.params['lines']: @@ -256,11 +297,14 @@ def run(module, result): commands.extend(module.params['after']) result['updates'] = commands + result['banners'] = banners # send the configuration commands to the device and merge # them with the current running config if not module.check_mode: module.config(commands) + load_banners(module, banners) + result['changed'] = True if module.params['save']: From 4db6d47ef86b1063548853cacf4196fa687c1e87 Mon Sep 17 00:00:00 2001 From: Jesse Keating Date: Fri, 4 Nov 2016 09:38:17 -0700 Subject: [PATCH 612/770] Add update_password argument to os_user (#5219) There is a desire to not have this module always result in a change if a password argument is supplied. The OpenStack API does not return a password back when we get a user, so we've been assuming that if a password argument was supplied, we should attempt to change the password (even if nothing else is changing), and that results in a "changed" state. Now we will only send along a password change attempt if the user wants one (the default to match history). Fixes #5217 --- cloud/openstack/os_user.py | 49 ++++++++++++++++++++++++++++++++------ 1 file changed, 42 insertions(+), 7 deletions(-) diff --git a/cloud/openstack/os_user.py b/cloud/openstack/os_user.py index 9bc66ecd52f..264eb3f8e85 100644 --- a/cloud/openstack/os_user.py +++ b/cloud/openstack/os_user.py @@ -43,6 +43,14 @@ - Password for the user required: false default: None + update_password: + required: false + default: always + choices: ['always', 'on_create'] + version_added: "2.3" + description: + - C(always) will attempt to update password. C(on_create) will only + set the password for newly created users. email: description: - Email address for the user @@ -89,6 +97,17 @@ cloud: mycloud state: absent name: demouser + +# Create a user but don't update password if user exists +- os_user: + cloud: mycloud + state: present + name: demouser + password: secret + update_password: on_create + email: demo@example.com + domain: default + default_project: demo ''' @@ -122,12 +141,13 @@ def _needs_update(params_dict, user): for k, v in params_dict.items(): - if k != 'password' and user[k] != v: + if k not in ('password', 'update_password') and user[k] != v: return True # We don't get password back in the user object, so assume any supplied # password is a change. - if params_dict['password'] is not None: + if (params_dict['password'] is not None and + params_dict['update_password'] == 'always'): return True return False @@ -164,11 +184,17 @@ def main(): domain=dict(required=False, default=None), enabled=dict(default=True, type='bool'), state=dict(default='present', choices=['absent', 'present']), + update_password=dict(default='always', choices=['always', + 'on_create']), ) module_kwargs = openstack_module_kwargs() module = AnsibleModule( argument_spec, + required_if=[ + ('update_password', 'always', ['password']), + ('update_password', 'on_create', ['password']), + ], **module_kwargs) if not HAS_SHADE: @@ -181,6 +207,7 @@ def main(): domain = module.params['domain'] enabled = module.params['enabled'] state = module.params['state'] + update_password = module.params['update_password'] try: cloud = shade.openstack_cloud(**module.params) @@ -203,17 +230,25 @@ def main(): enabled=enabled) changed = True else: - params_dict = {'email': email, 'enabled': enabled, 'password': password} + params_dict = {'email': email, 'enabled': enabled, + 'password': password, + 'update_password': update_password} if domain_id is not None: params_dict['domain_id'] = domain_id if default_project_id is not None: params_dict['default_project_id'] = default_project_id if _needs_update(params_dict, user): - user = cloud.update_user( - user['id'], password=password, email=email, - default_project=default_project_id, domain_id=domain_id, - enabled=enabled) + if update_password == 'always': + user = cloud.update_user( + user['id'], password=password, email=email, + default_project=default_project_id, + domain_id=domain_id, enabled=enabled) + else: + user = cloud.update_user( + user['id'], email=email, + default_project=default_project_id, + domain_id=domain_id, enabled=enabled) changed = True else: changed = False From 94d28fbd6fc68c7bb7216214575c68e0a9c5fc5a Mon Sep 17 00:00:00 2001 From: Tom Melendez Date: Fri, 4 Nov 2016 12:31:00 -0700 Subject: [PATCH 613/770] Support for named_ports in Managed Instance Groups. A libcloud guard has been implemented, as this feature will only be available in libcloud >= 1.3, or by using trunk. (#5167) --- cloud/google/gce_mig.py | 124 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) diff --git a/cloud/google/gce_mig.py b/cloud/google/gce_mig.py index bb44be1f25b..11433b35b20 100644 --- a/cloud/google/gce_mig.py +++ b/cloud/google/gce_mig.py @@ -84,6 +84,13 @@ on Autoscaling. required: false default: null + named_ports: + version_added: "2.3" + description: + - Define named ports that backend services can forward data to. Format is a a list of + name:port dictionaries. + required: false + default: null ''' EXAMPLES = ''' @@ -104,6 +111,11 @@ state: present size: 1 template: my-instance-template-1 + named_ports: + - name: http + port: 80 + - name: foobar + port: 82 - pause: seconds=30 - name: Recreate MIG Instances with Instance Template change. gce_mig: @@ -167,6 +179,12 @@ type: string sample: "my-managed-instance-group" +named_ports: + description: list of named ports acted upon + returned: when named_ports are initially set or updated + type: list + sample: [{ "name": "http", "port": 80 }, { "name": "foo", "port": 82 }] + size: description: Number of VMs in Managed Instance Group. returned: changed @@ -222,6 +240,18 @@ returned: When the delete of an Autoscaler was attempted. type: bool sample: true + +set_named_ports: + description: True if the named_ports have been set + returned: named_ports have been set + type: bool + sample: true + +updated_named_ports: + description: True if the named_ports have been updated + returned: named_ports have been updated + type: bool + sample: true ''' import socket @@ -324,6 +354,38 @@ def _validate_autoscaling_params(params): return (True, '') +def _validate_named_port_params(params): + """ + Validate the named ports parameters + + :param params: Ansible dictionary containing named_ports configuration + It is expected that autoscaling config will be found at the + key 'named_ports'. That key should contain a list of + {name : port} dictionaries. + :type params: ``dict`` + + :return: Tuple containing a boolean and a string. True if params + are valid, False otherwise, plus str for message. + :rtype: ``(``bool``, ``str``)`` + """ + if not params['named_ports']: + # It's optional, so if not set at all, it's valid. + return (True, '') + if not isinstance(params['named_ports'], list): + return (False, 'named_ports: expected list of name:port dictionaries.') + req_fields = [ + {'name': 'name', 'required': True, 'type': str}, + {'name': 'port', 'required': True, 'type': int} + ] # yapf: disable + + for np in params['named_ports']: + (valid_named_ports, np_msg) = _check_params(np, req_fields) + if not valid_named_ports: + return (False, np_msg) + + return (True, '') + + def _get_instance_list(mig, field='name', filter_list=['NONE']): """ Helper to grab field from instances response. @@ -595,6 +657,38 @@ def get_mig(gce, name, zone): return None +def update_named_ports(mig, named_ports): + """ + Set the named ports on a Managed Instance Group. + + Sort the existing named ports and new. If different, update. + This also implicitly allows for the removal of named_por + + :param mig: Managed Instance Group Object from libcloud. + :type mig: :class: `GCEInstanceGroupManager` + + :param named_ports: list of dictionaries in the format of {'name': port} + :type named_ports: ``list`` of ``dict`` + + :return: True if successful + :rtype: ``bool`` + """ + changed = False + existing_ports = [] + new_ports = [] + if hasattr(mig.instance_group, 'named_ports'): + existing_ports = sorted(mig.instance_group.named_ports, + key=lambda x: x['name']) + if named_ports is not None: + new_ports = sorted(named_ports, key=lambda x: x['name']) + + if existing_ports != new_ports: + if mig.instance_group.set_named_ports(named_ports): + changed = True + + return changed + + def main(): module = AnsibleModule(argument_spec=dict( name=dict(required=True), @@ -607,6 +701,7 @@ def main(): state=dict(choices=['absent', 'present'], default='present'), zone=dict(required=True), autoscaling=dict(type='dict', default=None), + named_ports=dict(type='list', default=None), service_account_email=dict(), service_account_permissions=dict(type='list'), pem_file=dict(), @@ -634,11 +729,22 @@ def main(): params['template'] = module.params.get('template') params['recreate_instances'] = module.params.get('recreate_instances') params['autoscaling'] = module.params.get('autoscaling', None) + params['named_ports'] = module.params.get('named_ports', None) (valid_autoscaling, as_msg) = _validate_autoscaling_params(params) if not valid_autoscaling: module.fail_json(msg=as_msg, changed=False) + if params['named_ports'] is not None and not hasattr( + gce, 'ex_instancegroup_set_named_ports'): + module.fail_json( + msg="Apache Libcloud 1.3.0+ is required to use 'named_ports' option", + changed=False) + + (valid_named_ports, np_msg) = _validate_named_port_params(params) + if not valid_named_ports: + module.fail_json(msg=np_msg, changed=False) + changed = False json_output = {'state': params['state'], 'zone': params['zone']} mig = get_mig(gce, params['name'], params['zone']) @@ -681,6 +787,18 @@ def main(): changed=False) json_output['created_autoscaler'] = True + # Add named ports if available + if params['named_ports']: + mig = get_mig(gce, params['name'], params['zone']) + if not mig: + module.fail_json( + msg='Unable to fetch created MIG %s to create \ + autoscaler in zone: %s' % ( + params['name'], params['zone']), changed=False) + json_output['set_named_ports'] = update_named_ports( + mig, params['named_ports']) + if json_output['set_named_ports']: + json_output['named_ports'] = params['named_ports'] elif params['state'] == 'absent': # Delete MIG @@ -704,6 +822,7 @@ def main(): else: # Update MIG + # If we're going to update a MIG, we need a size and template values. # If not specified, we use the values from the existing MIG. if not params['size']: @@ -755,6 +874,11 @@ def main(): changed = update_autoscaler(gce, autoscaler, params['autoscaling']) json_output['updated_autoscaler'] = changed + named_ports = params['named_ports'] or [] + json_output['updated_named_ports'] = update_named_ports(mig, + named_ports) + if json_output['updated_named_ports']: + json_output['named_ports'] = named_ports json_output['changed'] = changed json_output.update(params) From 872594b49a69a1f3795e0de3f7cf0194b6bdfd53 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Sun, 23 Oct 2016 19:24:00 +0200 Subject: [PATCH 614/770] Make service work when the service is not present in rc.conf After installing a package from the ports collection on a fresh FreeBSD 11.0, Ansible was unable to enable it, failing with "unable to get current rcvar value". Debugging showed that sysrc didn't see the variable from /usr/local/etc/rc.d/myservice, but adding the value was working. So we will just fallback to the default value if we can't find it. --- system/service.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/system/service.py b/system/service.py index d216e683cae..c8781b1c912 100644 --- a/system/service.py +++ b/system/service.py @@ -988,7 +988,7 @@ def service_enable(self): # and hope for the best. for rcvar in rcvars: if '=' in rcvar: - self.rcconf_key = rcvar.split('=')[0] + self.rcconf_key, default_rcconf_value = rcvar.split('=', 1) break if self.rcconf_key is None: @@ -997,8 +997,10 @@ def service_enable(self): if self.sysrc_cmd: # FreeBSD >= 9.2 rc, current_rcconf_value, stderr = self.execute_command("%s -n %s" % (self.sysrc_cmd, self.rcconf_key)) + # it can happen that rcvar is not set (case of a system coming from the ports collection) + # so we will fallback on the default if rc != 0: - self.module.fail_json(msg="unable to get current rcvar value", stdout=stdout, stderr=stderr) + current_rcconf_value = default_rcconf_value if current_rcconf_value.strip().upper() != self.rcconf_value: From 931a7133503832702f614cb3427226ef35418301 Mon Sep 17 00:00:00 2001 From: Matt Martz Date: Sat, 5 Nov 2016 22:00:06 -0500 Subject: [PATCH 615/770] If fetch_url failed to download the URL fail early with a proper error message. Fixes #5474 (#5476) --- packaging/os/yum.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 131144d8535..be5e1676999 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -225,6 +225,8 @@ def fetch_rpm_from_url(spec, module=None): package = os.path.join(tempdir, str(spec.rsplit('/', 1)[1])) try: rsp, info = fetch_url(module, spec) + if not rsp: + module.fail_json(msg="Failure downloading %s, %s" % (spec, info['msg'])) f = open(package, 'w') data = rsp.read(BUFSIZE) while data: From be0e6faa7b550e6608ff5ce1c4e91f4cac25dff8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Sun, 6 Nov 2016 17:21:41 +0100 Subject: [PATCH 616/770] apt: fix changed when cache updated but not pkg (#5487) --- packaging/os/apt.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index be8e1016a62..20306cd6deb 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -938,12 +938,6 @@ def main(): retvals['cache_updated'] = updated_cache # Store when the update time was last retvals['cache_update_time'] = updated_cache_time - # If the cache was updated and the general state change was set to - # False make sure that the change in cache state is accurately - # updated by setting the general changed state to the same as - # the cache state. - if updated_cache and not retvals['changed']: - retvals['changed'] = updated_cache if success: module.exit_json(**retvals) From d856c67cdea724d99731bdc42ce1d1835ea955b2 Mon Sep 17 00:00:00 2001 From: Eduard Iskandarov Date: Sun, 6 Nov 2016 19:32:22 +0300 Subject: [PATCH 617/770] Fixes: #4516 add placement_group argument for ec2_asg module --- cloud/amazon/ec2_asg.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 0c22bc76943..7ebb3c93f4a 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -53,6 +53,12 @@ description: - Maximum number of instances in group, if unspecified then the current group value will be used. required: false + placement_group: + description: + - Physical location of your cluster placement group created in Amazon EC2. + required: false + version_added: "2.3" + default: None desired_capacity: description: - Desired number of instances in group, if unspecified then the current group value will be used. @@ -394,6 +400,7 @@ def create_autoscaling_group(connection, module): launch_config_name = module.params.get('launch_config_name') min_size = module.params['min_size'] max_size = module.params['max_size'] + placement_group = module.params.get('placement_group') desired_capacity = module.params.get('desired_capacity') vpc_zone_identifier = module.params.get('vpc_zone_identifier') set_tags = module.params.get('tags') @@ -437,6 +444,7 @@ def create_autoscaling_group(connection, module): launch_config=launch_configs[0], min_size=min_size, max_size=max_size, + placement_group=placement_group, desired_capacity=desired_capacity, vpc_zone_identifier=vpc_zone_identifier, connection=connection, @@ -816,6 +824,7 @@ def main(): launch_config_name=dict(type='str'), min_size=dict(type='int'), max_size=dict(type='int'), + placement_group=dict(type='str'), desired_capacity=dict(type='int'), vpc_zone_identifier=dict(type='list'), replace_batch_size=dict(type='int', default=1), From eac46e609c4f1115bcbe912554114c12bda51bba Mon Sep 17 00:00:00 2001 From: Michael Herold Date: Sun, 6 Nov 2016 22:36:45 +0100 Subject: [PATCH 618/770] Removes outdated "requirement" and outdated "note" (#5332) - 7f59773460d79b3dae34c375ba68caea1bfc09a8 no longer uses `ConfigParser` - 1d4c0abe2902d91b2895452feedcf72bf3dd9e20 removed the `import` statement --- files/ini_file.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 9dc3634c2f0..4f74bda5fad 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -88,11 +88,6 @@ notes: - While it is possible to add an I(option) without specifying a I(value), this makes no sense. - - A section named C(default) cannot be added by the module, but if it exists, individual - options within the section can be updated. (This is a limitation of Python's I(ConfigParser).) - Either use M(template) to create a base INI file with a C([default]) section, or use - M(lineinfile) to add the missing line. -requirements: [ ConfigParser ] author: - "Jan-Piet Mens (@jpmens)" - "Ales Nosek (@noseka1)" From 9d07911b03f4ef2a492fb9d3b88d58007f138eb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Abel=20Bold=C3=BA?= Date: Mon, 7 Nov 2016 14:40:24 +0100 Subject: [PATCH 619/770] Add uplink provider type (#5282) --- cloud/openstack/os_network.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_network.py b/cloud/openstack/os_network.py index d80267e8937..5220dba5eba 100644 --- a/cloud/openstack/os_network.py +++ b/cloud/openstack/os_network.py @@ -69,7 +69,7 @@ provider_network_type: description: - The type of physical network that maps to this network resource. - choices: ['flat', 'vlan', 'vxlan', 'gre'] + choices: ['flat', 'vlan', 'vxlan', 'gre', 'uplink'] required: false default: None version_added: "2.1" @@ -169,7 +169,7 @@ def main(): external=dict(default=False, type='bool'), provider_physical_network=dict(required=False), provider_network_type=dict(required=False, default=None, - choices=['flat', 'vlan', 'vxlan', 'gre']), + choices=['flat', 'vlan', 'vxlan', 'gre', 'uplink']), provider_segmentation_id=dict(required=False), state=dict(default='present', choices=['absent', 'present']), project=dict(default=None) From 1ca20679dd397cb0236ebb0557615bffb8da3c70 Mon Sep 17 00:00:00 2001 From: mickael-ange Date: Mon, 7 Nov 2016 23:55:23 +0800 Subject: [PATCH 620/770] Added aws and ec2 extends_documentation_fragment to cloud/amazon/rds module. (#3951) --- cloud/amazon/rds.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py index cad4dbb1412..bde50fce371 100644 --- a/cloud/amazon/rds.py +++ b/cloud/amazon/rds.py @@ -223,7 +223,9 @@ author: - "Bruce Pennypacker (@bpennypacker)" - "Will Thames (@willthames)" - +extends_documentation_fragment: + - aws + - ec2 ''' # FIXME: the command stuff needs a 'state' like alias to make things consistent -- MPD From b8efa3cde0b4e8f1d3ab24fa86cf29a7cf7f1100 Mon Sep 17 00:00:00 2001 From: Michael Scherer Date: Mon, 7 Nov 2016 11:35:23 +0100 Subject: [PATCH 621/770] Add no_log on password argument Also do not use a wildcard import, for later refactoring --- web_infrastructure/supervisorctl.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/web_infrastructure/supervisorctl.py b/web_infrastructure/supervisorctl.py index 6ac5bc7de67..bd6baa98e3b 100644 --- a/web_infrastructure/supervisorctl.py +++ b/web_infrastructure/supervisorctl.py @@ -19,6 +19,7 @@ # along with Ansible. If not, see . # import os +from ansible.module_utils.basic import AnsibleModule, is_executable DOCUMENTATION = ''' --- @@ -101,7 +102,7 @@ def main(): config=dict(required=False, type='path'), server_url=dict(required=False), username=dict(required=False), - password=dict(required=False), + password=dict(required=False, no_log=True), supervisorctl_path=dict(required=False, type='path'), state=dict(required=True, choices=['present', 'started', 'restarted', 'stopped', 'absent']) ) @@ -239,8 +240,5 @@ def take_action_on_processes(processes, status_filter, action, expected_result): module.fail_json(name=name, msg="ERROR (no such process)") take_action_on_processes(processes, lambda s: s in ('RUNNING', 'STARTING'), 'stop', 'stopped') -# import module snippets -from ansible.module_utils.basic import * -# is_executable from basic if __name__ == '__main__': main() From 933f508cfa050c79d8e36b2624ddb2d0eea26739 Mon Sep 17 00:00:00 2001 From: bdowling Date: Mon, 7 Nov 2016 19:48:43 -0600 Subject: [PATCH 622/770] ios_mods - added stdout to exception output. Removed to_lines() (#5428) stdout lines are now available when certain exceptions occur (Ref ansible/ansible#18241) Also noticed that to_lines was essentially handled in lib/ansible/plugins/action/__init__.py -- only difference was it didn't handle a list. to_lines() could be removed across network modules now, but this commit is only for ios_command. Also adds disconnect() to ios_command that was added to ios_config in #5247 --- network/ios/ios_command.py | 11 +++-------- network/ios/ios_config.py | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/network/ios/ios_command.py b/network/ios/ios_command.py index 4204a73a682..c5923e4e2b0 100644 --- a/network/ios/ios_command.py +++ b/network/ios/ios_command.py @@ -148,12 +148,6 @@ VALID_KEYS = ['command', 'prompt', 'response'] -def to_lines(stdout): - for item in stdout: - if isinstance(item, string_types): - item = str(item).split('\n') - yield item - def parse_commands(module): for cmd in module.params['commands']: if isinstance(cmd, string_types): @@ -216,7 +210,9 @@ def main(): module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) except NetworkError: exc = get_exception() - module.fail_json(msg=str(exc)) + module.disconnect() + module.fail_json(msg=str(exc), stdout=exc.kwargs.get('stdout')) + result = dict(changed=False, stdout=list()) @@ -228,7 +224,6 @@ def main(): result['stdout'].append(output) result['warnings'] = warnings - result['stdout_lines'] = list(to_lines(result['stdout'])) module.exit_json(**result) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 26a318de378..f7a2d5e6754 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -367,7 +367,7 @@ def main(): except NetworkError: exc = get_exception() module.disconnect() - module.fail_json(msg=str(exc)) + module.fail_json(msg=str(exc), stdout=exc.kwargs.get('stdout')) module.disconnect() module.exit_json(**result) From 516f558b40df3b0d97590bf25abbb4954827ed80 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Mon, 7 Nov 2016 21:26:02 -0500 Subject: [PATCH 623/770] roll up of more fixes for ios_config multiline banners (#5524) * now works for any banner in the config * provides a configurable delimiter link #5318 --- network/ios/ios_config.py | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index f7a2d5e6754..dcbd33b08bf 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -98,6 +98,15 @@ required: false default: line choices: ['line', 'block'] + multiline_delimiter: + description: + - This arugment is used when pushing a multiline configuration + element to the IOS device. It specifies the character to use + as the delimiting character. This only applies to the + configuration action + required: false + default: "@" + version_added: "2.2" force: description: - The force argument instructs the module to not consider the @@ -212,6 +221,10 @@ def check_args(module, warnings): + if module.params['multiline_delimiter']: + if len(module.params['multiline_delimiter']) != 1: + module.fail_json(msg='multiline_delimiter value can only be a ' + 'single character') if module.params['force']: warnings.append('The force argument is deprecated, please use ' 'match=none instead. This argument will be ' @@ -219,12 +232,18 @@ def check_args(module, warnings): def extract_banners(config): banners = {} - for cmd in ['exec', 'login', 'incoming']: + banner_cmds = re.findall(r'^banner (\w+)', config, re.M) + for cmd in banner_cmds: regex = r'banner %s \^C(.+?)(?=\^C)' % cmd match = re.search(regex, config, re.S) if match: key = 'banner %s' % cmd banners[key] = match.group(1).strip() + + for cmd in banner_cmds: + regex = r'banner %s \^C(.+?)(?=\^C)' % cmd + match = re.search(regex, config, re.S) + if match: config = config.replace(str(match.group(1)), '') config = re.sub(r'banner \w+ \^C\^C', '!! banner removed', config) @@ -238,9 +257,10 @@ def diff_banners(want, have): return candidate def load_banners(module, banners): + delimiter = module.params['multiline_delimiter'] for key, value in iteritems(banners): - key += ' @' - for cmd in ['config terminal', key, value, '@', 'end']: + key += ' %s' % delimiter + for cmd in ['config terminal', key, value, delimiter, 'end']: cmd += '\r' module.connection.shell.shell.sendall(cmd) time.sleep(1) @@ -283,6 +303,7 @@ def run(module, result): replace=replace) else: configobjs = candidate.items + have_banners = {} banners = diff_banners(want_banners, have_banners) @@ -296,14 +317,16 @@ def run(module, result): if module.params['after']: commands.extend(module.params['after']) - result['updates'] = commands - result['banners'] = banners + result['updates'] = commands + result['banners'] = banners # send the configuration commands to the device and merge # them with the current running config if not module.check_mode: - module.config(commands) - load_banners(module, banners) + if commands: + module.config(commands) + if banners: + load_banners(module, banners) result['changed'] = True @@ -327,6 +350,7 @@ def main(): match=dict(default='line', choices=['line', 'strict', 'exact', 'none']), replace=dict(default='line', choices=['line', 'block']), + multiline_delimiter=dict(default='@'), # this argument is deprecated in favor of setting match: none # it will be removed in a future version From 6cb4319f659dda6fd593e7e77b85524b5d1e47f9 Mon Sep 17 00:00:00 2001 From: mrLarbi Date: Tue, 8 Nov 2016 16:09:32 +0100 Subject: [PATCH 624/770] ios_config : Set multiline_delimiter version to 2.3 (#5525) --- network/ios/ios_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index dcbd33b08bf..4f6786ee23e 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -106,7 +106,7 @@ configuration action required: false default: "@" - version_added: "2.2" + version_added: "2.3" force: description: - The force argument instructs the module to not consider the From c953397fded4d7cf06437a06e3bcab95ae1c3d1b Mon Sep 17 00:00:00 2001 From: Yair Fried Date: Tue, 8 Nov 2016 17:53:17 +0200 Subject: [PATCH 625/770] Expose internal_network in os_floating_ip (#5510) * Expose internal_network in os_floating_ip Shade project has finally exposed this argument so now this module matches old quantum_floatingip module's capabilities. Use "nat_destination" term instead of "internal_network" to match shade terminology. * Add (private|internal)_network aliases to os_floating_ip * Fix typo in os_floating_ip --- cloud/openstack/os_floating_ip.py | 35 +++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/cloud/openstack/os_floating_ip.py b/cloud/openstack/os_floating_ip.py index 75d76061fd2..1198b6a4699 100644 --- a/cloud/openstack/os_floating_ip.py +++ b/cloud/openstack/os_floating_ip.py @@ -23,6 +23,9 @@ except ImportError: HAS_SHADE = False +from distutils.version import StrictVersion + + DOCUMENTATION = ''' --- module: os_floating_ip @@ -59,6 +62,14 @@ - To which fixed IP of server the floating IP address should be attached to. required: false + nat_destination: + description: + - The name or id of a neutron private network that the fixed IP to + attach floating IP is on + required: false + default: None + aliases: ["fixed_network", "internal_network"] + version_added: "2.3" wait: description: - When attaching a floating IP address, specify whether we should @@ -107,6 +118,17 @@ wait: true timeout: 180 +# Assign a new floating IP from the network `ext_net` to the instance fixed +# ip in network `private_net` of `cattle001`. +- os_floating_ip: + cloud: dguerri + state: present + server: cattle001 + network: ext_net + nat_destination: private_net + wait: true + timeout: 180 + # Detach a floating IP address from a server - os_floating_ip: cloud: dguerri @@ -133,6 +155,8 @@ def main(): floating_ip_address=dict(required=False, default=None), reuse=dict(required=False, type='bool', default=False), fixed_address=dict(required=False, default=None), + nat_destination=dict(required=False, default=None, + aliases=['fixed_network', 'internal_network']), wait=dict(required=False, type='bool', default=False), timeout=dict(required=False, type='int', default=60), purge=dict(required=False, type='bool', default=False), @@ -144,12 +168,18 @@ def main(): if not HAS_SHADE: module.fail_json(msg='shade is required for this module') + if (module.params['nat_destination'] and + StrictVersion(shade.__version__) < StrictVersion('1.8.0')): + module.fail_json(msg="To utilize nat_destination, the installed version of" + "the shade library MUST be >= 1.8.0") + server_name_or_id = module.params['server'] state = module.params['state'] network = module.params['network'] floating_ip_address = module.params['floating_ip_address'] reuse = module.params['reuse'] fixed_address = module.params['fixed_address'] + nat_destination = module.params['nat_destination'] wait = module.params['wait'] timeout = module.params['timeout'] purge = module.params['purge'] @@ -172,7 +202,8 @@ def main(): network_id = cloud.get_network(name_or_id=network)["id"] else: network_id = None - if all([fixed_address, f_ip.fixed_ip_address == fixed_address, + if all([(fixed_address and f_ip.fixed_ip_address == fixed_address) or + (nat_destination and f_ip.internal_network == fixed_address), network, f_ip.network != network_id]): # Current state definitely conflicts with requirements module.fail_json(msg="server {server} already has a " @@ -193,7 +224,7 @@ def main(): server = cloud.add_ips_to_server( server=server, ips=floating_ip_address, ip_pool=network, reuse=reuse, fixed_address=fixed_address, wait=wait, - timeout=timeout) + timeout=timeout, nat_destination=nat_destination) fip_address = cloud.get_server_public_ip(server) # Update the floating IP status f_ip = _get_floating_ip(cloud, fip_address) From cbc3aac2f2e56975c1ef97702742d0198db25433 Mon Sep 17 00:00:00 2001 From: Jason Cormie Date: Tue, 8 Nov 2016 23:34:11 +0000 Subject: [PATCH 626/770] Addition of InstanceUUID to facts (#4424) The Instance UUID(refered to as PersistenceUUID in the API) is a the ID vcenter uses to idenify VMs. My use case for this is that I configure Zabbix using ansible and its vmware module relies on using these to identify VMs. --- cloud/vmware/vsphere_guest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index b3d186eb98f..8d60cec2523 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -1559,6 +1559,7 @@ def gather_facts(vm): 'hw_guest_full_name': vm.properties.config.guestFullName, 'hw_guest_id': vm.properties.config.guestId, 'hw_product_uuid': vm.properties.config.uuid, + 'hw_instance_uuid': vm.properties.config.instanceUuid, 'hw_processor_count': vm.properties.config.hardware.numCPU, 'hw_memtotal_mb': vm.properties.config.hardware.memoryMB, 'hw_interfaces':[], From 80b69c4821d8ab61b19a2a89ed743c1cc9c0de5d Mon Sep 17 00:00:00 2001 From: jctanner Date: Tue, 8 Nov 2016 21:47:59 -0500 Subject: [PATCH 627/770] Correct the handling of state=latest for yum groups. (#4141) * Correct the handling up state=latest for yum groups. * Use yum-deprecated when available Fixes #4119 --- packaging/os/yum.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index be5e1676999..7356880f45d 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -832,6 +832,7 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): # some guess work involved with groups. update @ will install the group if missing if spec.startswith('@'): pkgs['update'].append(spec) + will_update.add(spec) continue # dep/pkgname - find it else: @@ -908,14 +909,16 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): if len(pkgs['install']) > 0: # install missing cmd = yum_basecmd + ['install'] + pkgs['install'] rc, out, err = module.run_command(cmd) - res['changed'] = True + if not out.strip().lower().endswith("no packages marked for update"): + res['changed'] = True else: rc, out, err = [0, '', ''] if len(will_update) > 0: # update present cmd = yum_basecmd + ['update'] + pkgs['update'] rc2, out2, err2 = module.run_command(cmd) - res['changed'] = True + if not out2.strip().lower().endswith("no packages marked for update"): + res['changed'] = True else: rc2, out2, err2 = [0, '', ''] @@ -936,7 +939,14 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): def ensure(module, state, pkgs, conf_file, enablerepo, disablerepo, disable_gpg_check, exclude, repoq): - yumbin = module.get_bin_path('yum') + # fedora will redirect yum to dnf, which has incompatibilities + # with how this module expects yum to operate. If yum-deprecated + # is available, use that instead to emulate the old behaviors. + if module.get_bin_path('yum-deprecated'): + yumbin = module.get_bin_path('yum-deprecated') + else: + yumbin = module.get_bin_path('yum') + # need debug level 2 to get 'Nothing to do' for groupinstall. yum_basecmd = [yumbin, '-d', '2', '-y'] From f31a71c4cfc543894a8731fbbfbc6dab06f6dcf6 Mon Sep 17 00:00:00 2001 From: Sean Summers Date: Tue, 8 Nov 2016 17:23:26 -0500 Subject: [PATCH 628/770] add role_arn to support Service Role Add `role_arn` to support [AWS CloudFormation Service Role](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html) --- cloud/amazon/cloudformation.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 3ea1017d524..e9f14de113f 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -87,6 +87,12 @@ choices: [ json, yaml ] required: false version_added: "2.0" + role_arn: + description: + - The role that AWS CloudFormation assumes to create the stack. [AWS CloudFormation Service Role](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html) + required: false + default: null + version_added: "2.3" author: "James S. Martin (@jsmartin)" extends_documentation_fragment: @@ -147,6 +153,22 @@ ClusterSize: 3 tags: Stack: ansible-cloudformation + +# Use a template from a URL, and assume a role to execute +- name: launch ansible cloudformation example with role assumption + cloudformation: + stack_name="ansible-cloudformation" state=present + region=us-east-1 disable_rollback=true + template_url=https://s3.amazonaws.com/my-bucket/cloudformation.template + role_arn: arn:aws:iam::123456789012:role/cloudformation-iam-role + args: + template_parameters: + KeyName: jmartin + DiskType: ephemeral + InstanceType: m1.small + ClusterSize: 3 + tags: + Stack: ansible-cloudformation ''' RETURN = ''' @@ -339,6 +361,7 @@ def main(): disable_rollback=dict(default=False, type='bool'), template_url=dict(default=None, required=False), template_format=dict(default=None, choices=['json', 'yaml'], required=False), + role_arn=dict(default=None, required=False), tags=dict(default=None, type='dict') ) ) @@ -381,6 +404,9 @@ def main(): if module.params.get('template_url'): stack_params['TemplateURL'] = module.params['template_url'] + if module.params.get('role_arn'): + stack_params['RoleARN'] = module.params['role_arn'] + update = False result = {} From aa5124af27c2c12f13f320468156ea43cc9820d7 Mon Sep 17 00:00:00 2001 From: Sean Summers Date: Wed, 9 Nov 2016 04:16:18 -0500 Subject: [PATCH 629/770] added requires for botocore with RoleARN support Added a requires for the minimum botocore version required to support RoleARN --- cloud/amazon/cloudformation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index e9f14de113f..3bd5ff306a5 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -98,6 +98,7 @@ extends_documentation_fragment: - aws - ec2 +requires: [ botocore>=1.4.57 ] ''' EXAMPLES = ''' From 45588171a85c4af70994b0bbc99dcc32ee344711 Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Wed, 9 Nov 2016 12:37:13 -0500 Subject: [PATCH 630/770] Fix link in `cloudformation` module docs --- cloud/amazon/cloudformation.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 3bd5ff306a5..0d02ad70cb3 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -89,7 +89,7 @@ version_added: "2.0" role_arn: description: - - The role that AWS CloudFormation assumes to create the stack. [AWS CloudFormation Service Role](http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html) + - The role that AWS CloudFormation assumes to create the stack. See the AWS CloudFormation Service Role docs U(http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-iam-servicerole.html) required: false default: null version_added: "2.3" @@ -447,8 +447,6 @@ def main(): result = dict(changed=False, output='Stack is already up-to-date.') else: module.fail_json(msg=error_msg) - #return {'error': error_msg} - #module.fail_json(msg=error_msg) if not result: module.fail_json(msg="empty result") # check the status of the stack while we are creating/updating it. From 2de97ac5bddad0e97b5beabdaadc6cd8a4bc6713 Mon Sep 17 00:00:00 2001 From: jctanner Date: Wed, 9 Nov 2016 13:46:18 -0500 Subject: [PATCH 631/770] replace type() with isinstance() (#5541) Replace all use of type() with isintance() Addresses https://github.com/ansible/ansible/issues/18310 --- cloud/amazon/ec2.py | 14 +++++++------- cloud/amazon/rds_param_group.py | 2 +- cloud/amazon/route53.py | 4 ++-- cloud/azure/azure.py | 2 +- cloud/docker/_docker.py | 2 +- cloud/openstack/os_server.py | 8 ++++---- utilities/logic/async_wrapper.py | 2 +- 7 files changed, 17 insertions(+), 17 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 0b921c4b718..01d105fd35c 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -615,7 +615,7 @@ def find_running_instances_by_count_tag(module, ec2, count_tag, zone=None): def _set_none_to_blank(dictionary): result = dictionary for k in result.iterkeys(): - if type(result[k]) == dict: + if isinstance(result[k], dict): result[k] = _set_none_to_blank(result[k]) elif not result[k]: result[k] = "" @@ -629,27 +629,27 @@ def get_reservations(module, ec2, tags=None, state=None, zone=None): if tags is not None: - if type(tags) is str: + if isinstance(tags, str): try: tags = literal_eval(tags) except: pass # if string, we only care that a tag of that name exists - if type(tags) is str: + if isinstance(tags, str): filters.update({"tag-key": tags}) # if list, append each item to filters - if type(tags) is list: + if isinstance(tags, list): for x in tags: - if type(x) is dict: + if isinstance(x, dict): x = _set_none_to_blank(x) filters.update(dict(("tag:"+tn, tv) for (tn,tv) in x.iteritems())) else: filters.update({"tag-key": x}) # if dict, add the key and value to the filter - if type(tags) is dict: + if isinstance(tags, dict): tags = _set_none_to_blank(tags) filters.update(dict(("tag:"+tn, tv) for (tn,tv) in tags.iteritems())) @@ -909,7 +909,7 @@ def enforce_count(module, ec2, vpc): # ensure all instances are dictionaries all_instances = [] for inst in instances: - if type(inst) is not dict: + if not isinstance(inst, dict): inst = get_instance_info(inst) all_instances.append(inst) diff --git a/cloud/amazon/rds_param_group.py b/cloud/amazon/rds_param_group.py index a3353cb33dc..1d863b1a379 100644 --- a/cloud/amazon/rds_param_group.py +++ b/cloud/amazon/rds_param_group.py @@ -161,7 +161,7 @@ def set_parameter(param, value, immediate): # may be based on a variable (ie. {foo*3/4}) so # just pass it on through to boto converted_value = str(value) - elif type(value) == bool: + elif isinstance(value, bool): converted_value = 1 if value else 0 else: converted_value = int(value) diff --git a/cloud/amazon/route53.py b/cloud/amazon/route53.py index 107ca757d95..6e8c25377dc 100644 --- a/cloud/amazon/route53.py +++ b/cloud/amazon/route53.py @@ -440,10 +440,10 @@ def main(): value_list = () - if type(value_in) is str: + if isinstance(value_in, str): if value_in: value_list = sorted([s.strip() for s in value_in.split(',')]) - elif type(value_in) is list: + elif isinstance(value_in, list): value_list = sorted(value_in) if zone_in[-1:] != '.': diff --git a/cloud/azure/azure.py b/cloud/azure/azure.py index 38e9c2fc7c6..226a8c07253 100644 --- a/cloud/azure/azure.py +++ b/cloud/azure/azure.py @@ -594,7 +594,7 @@ def __getattr__(self, name): raise AttributeError(name) def _wrap(self, func, args, kwargs): - if type(func) == MethodType: + if isinstance(func, MethodType): result = self._handle_temporary_redirects(lambda: func(*args, **kwargs)) else: result = self._handle_temporary_redirects(lambda: func(self.other, *args, **kwargs)) diff --git a/cloud/docker/_docker.py b/cloud/docker/_docker.py index bab50fcbb86..071e7853987 100644 --- a/cloud/docker/_docker.py +++ b/cloud/docker/_docker.py @@ -1303,7 +1303,7 @@ def get_differing_containers(self): for name, value in self.module.params.get('labels').iteritems(): expected_labels[name] = str(value) - if type(container['Config']['Labels']) is dict: + if isinstance(container['Config']['Labels'], dict): actual_labels = container['Config']['Labels'] else: for container_label in container['Config']['Labels'] or []: diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index ec3b4e83ac1..12d8724e4ac 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -386,7 +386,7 @@ def _exit_hostvars(module, cloud, server, changed=True): def _parse_nics(nics): for net in nics: - if type(net) == str: + if isinstance(net, str): for nic in net.split(','): yield dict((nic.split('='),)) else: @@ -396,11 +396,11 @@ def _network_args(module, cloud): args = [] nics = module.params['nics'] - if type(nics) != list: + if not isinstance(nics, list): module.fail_json(msg='The \'nics\' parameter must be a list.') for net in _parse_nics(nics): - if type(net) != dict: + if not isinstance(net, dict): module.fail_json( msg='Each entry in the \'nics\' parameter must be a dict.') @@ -457,7 +457,7 @@ def _create_server(module, cloud): nics = _network_args(module, cloud) - if type(module.params['meta']) is str: + if isinstance(module.params['meta'], str): metas = {} for kv_str in module.params['meta'].split(","): k, v = kv_str.split("=") diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index b2af4067f2b..0d8c41ac32e 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -143,7 +143,7 @@ def _run_module(wrapped_cmd, jid, job_path): if json_warnings: # merge JSON junk warnings with any existing module warnings module_warnings = result.get('warnings', []) - if type(module_warnings) is not list: + if not isinstance(module_warnings, list): module_warnings = [module_warnings] module_warnings.extend(json_warnings) result['warnings'] = module_warnings From 8cbb0dc13e3310b2ac3addda0419bd0d111a5b27 Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Thu, 10 Nov 2016 08:51:51 -0500 Subject: [PATCH 632/770] Fail gracefully in `ec2_asg` module when there are no matching launch configurations --- cloud/amazon/ec2_asg.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 0c22bc76943..6cf0bc8f9b4 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -430,6 +430,8 @@ def create_autoscaling_group(connection, module): availability_zones = module.params['availability_zones'] = [zone.name for zone in ec2_connection.get_all_zones()] enforce_required_arguments(module) launch_configs = connection.get_all_launch_configurations(names=[launch_config_name]) + if len(launch_configs) == 0: + module.fail_json(msg="No launch config found with name %s" % launch_config_name) ag = AutoScalingGroup( group_name=group_name, load_balancers=load_balancers, From 4f83b809dcc51c554f55a06e2930fd76377c6bda Mon Sep 17 00:00:00 2001 From: "Ryan S. Brown" Date: Thu, 10 Nov 2016 10:45:47 -0500 Subject: [PATCH 633/770] Fix doc examples for `ec2_elb_lb` module The examples had the listeners as a list item for each param, when they really need to be their own objects. --- cloud/amazon/ec2_elb_lb.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_elb_lb.py b/cloud/amazon/ec2_elb_lb.py index ec2bcd731c1..066719b84a2 100644 --- a/cloud/amazon/ec2_elb_lb.py +++ b/cloud/amazon/ec2_elb_lb.py @@ -313,8 +313,8 @@ - us-east-1d listeners: - protocol: http - - load_balancer_port: 80 - - instance_port: 80 + load_balancer_port: 80 + instance_port: 80 # Create an ELB with load balancer stickiness enabled - local_action: @@ -327,8 +327,8 @@ - us-east-1d listeners: - protocol: http - - load_balancer_port: 80 - - instance_port: 80 + load_balancer_port: 80 + instance_port: 80 stickiness: type: loadbalancer enabled: yes @@ -345,8 +345,8 @@ - us-east-1d listeners: - protocol: http - - load_balancer_port: 80 - - instance_port: 80 + load_balancer_port: 80 + instance_port: 80 stickiness: type: application enabled: yes @@ -363,8 +363,8 @@ - us-east-1d listeners: - protocol: http - - load_balancer_port: 80 - - instance_port: 80 + load_balancer_port: 80 + instance_port: 80 tags: Name: "New ELB" stack: "production" @@ -381,8 +381,8 @@ - us-east-1d listeners: - protocol: http - - load_balancer_port: 80 - - instance_port: 80 + load_balancer_port: 80 + instance_port: 80 tags: {} """ From e9c6e885fd828f2b6e991d8ccb5a73eed7e0d873 Mon Sep 17 00:00:00 2001 From: tedder Date: Thu, 10 Nov 2016 08:17:17 -0800 Subject: [PATCH 634/770] add .format indexes for 2.6 compatability Fixes #5551; the "".format() style needs to have `{0}` instead of the implied `{}` version for compatability with 2.6. --- cloud/amazon/cloudformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 0d02ad70cb3..ee4481eb61d 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -257,11 +257,11 @@ def get_stack_events(cfn, stack_name): return ret for e in events.get('StackEvents', []): - eventline = 'StackEvent {} {} {}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus']) + eventline = 'StackEvent {0} {1} {2}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus']) ret['events'].append(eventline) if e['ResourceStatus'].endswith('FAILED'): - failline = '{} {} {}: {}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus'], e['ResourceStatusReason']) + failline = '{0} {1} {2}: {3}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus'], e['ResourceStatusReason']) ret['log'].append(failline) return ret From 801f4f0bf430a33be1f94b63789b10ad5ac2ad94 Mon Sep 17 00:00:00 2001 From: Ryan Brown Date: Thu, 10 Nov 2016 11:32:32 -0500 Subject: [PATCH 635/770] Fix `cloudformation` error when stack-rollback fails (#5550) In cases where a CFN stack could not complete (due to lack of permissions or similar) but also failed to roll back, the gathering of stack resources would fail because successfully deleted items in the rollback would no longer have a `PhysicalResourceId` property. This PR fixes that by soft-failing when there's no physical ID associated to a resource. --- cloud/amazon/cloudformation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 0d02ad70cb3..f5b713e046b 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -464,7 +464,7 @@ def main(): for res in reslist.get('StackResourceSummaries', []): stack_resources.append({ "logical_resource_id": res['LogicalResourceId'], - "physical_resource_id": res['PhysicalResourceId'], + "physical_resource_id": res.get('PhysicalResourceId', ''), "resource_type": res['ResourceType'], "last_updated_time": res['LastUpdatedTimestamp'], "status": res['ResourceStatus'], From d52d256196c66392d6c5aa08063b09b68ce23404 Mon Sep 17 00:00:00 2001 From: Andrew Gaffney Date: Thu, 10 Nov 2016 12:47:59 -0700 Subject: [PATCH 636/770] Fix bare variable references in docs (#5554) --- cloud/amazon/ec2_eip.py | 2 +- cloud/amazon/ec2_elb.py | 2 +- cloud/amazon/ec2_tag.py | 2 +- cloud/amazon/ec2_vol.py | 4 ++-- cloud/amazon/iam.py | 2 +- cloud/amazon/iam_policy.py | 2 +- network/cumulus/cl_bond.py | 2 +- network/cumulus/cl_bridge.py | 2 +- network/cumulus/cl_interface.py | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cloud/amazon/ec2_eip.py b/cloud/amazon/ec2_eip.py index eb3f17a9569..2757a618e65 100644 --- a/cloud/amazon/ec2_eip.py +++ b/cloud/amazon/ec2_eip.py @@ -101,7 +101,7 @@ register: ec2 - name: associate new elastic IPs with each of the instances ec2_eip: "device_id={{ item }}" - with_items: ec2.instance_ids + with_items: "{{ ec2.instance_ids }}" - name: allocate a new elastic IP inside a VPC in us-west-2 ec2_eip: region=us-west-2 in_vpc=yes register: eip diff --git a/cloud/amazon/ec2_elb.py b/cloud/amazon/ec2_elb.py index 000bcf04b34..0aab5cc0828 100644 --- a/cloud/amazon/ec2_elb.py +++ b/cloud/amazon/ec2_elb.py @@ -92,7 +92,7 @@ instance_id: "{{ ansible_ec2_instance_id }}" ec2_elbs: "{{ item }}" state: present - with_items: ec2_elbs + with_items: "{{ ec2_elbs }}" """ import time diff --git a/cloud/amazon/ec2_tag.py b/cloud/amazon/ec2_tag.py index 3fd05dd4481..76bba14b20b 100644 --- a/cloud/amazon/ec2_tag.py +++ b/cloud/amazon/ec2_tag.py @@ -89,7 +89,7 @@ instance: "{{ item.id }}" region: eu-west-1 state: list - with_items: ec2.tagged_instances + with_items: "{{ ec2.tagged_instances }}" register: ec2_vol - name: tag the volumes diff --git a/cloud/amazon/ec2_vol.py b/cloud/amazon/ec2_vol.py index 5d325d83fa3..fb78365ff9f 100644 --- a/cloud/amazon/ec2_vol.py +++ b/cloud/amazon/ec2_vol.py @@ -135,7 +135,7 @@ - ec2_vol: instance: "{{ item.id }} " volume_size: 5 - with_items: ec2.instances + with_items: "{{ ec2.instances }}" register: ec2_vol # Example: Launch an instance and then add a volume if not already attached @@ -156,7 +156,7 @@ instance: "{{ item.id }}" name: my_existing_volume_Name_tag device_name: /dev/xvdf - with_items: ec2.instances + with_items: "{{ ec2.instances }}" register: ec2_vol # Remove a volume diff --git a/cloud/amazon/iam.py b/cloud/amazon/iam.py index 9f4e119bf18..15f6741b074 100644 --- a/cloud/amazon/iam.py +++ b/cloud/amazon/iam.py @@ -136,7 +136,7 @@ name: jdavila state: update groups: "{{ item.created_group.group_name }}" - with_items: new_groups.results + with_items: "{{ new_groups.results }}" # Example of role with custom trust policy for Lambda service - name: Create IAM role with custom trust relationship diff --git a/cloud/amazon/iam_policy.py b/cloud/amazon/iam_policy.py index 559ad3c4fda..a95f88f42d3 100644 --- a/cloud/amazon/iam_policy.py +++ b/cloud/amazon/iam_policy.py @@ -94,7 +94,7 @@ policy_name: "READ-ONLY" policy_document: readonlypolicy.json state: present - with_items: new_groups.results + with_items: "{{ new_groups.results }}" # Create a new S3 policy with prefix per user tasks: diff --git a/network/cumulus/cl_bond.py b/network/cumulus/cl_bond.py index 52da0bed3a9..cc8930ce8be 100644 --- a/network/cumulus/cl_bond.py +++ b/network/cumulus/cl_bond.py @@ -171,7 +171,7 @@ mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}" mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}" mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}" -with_dict: cl_bonds +with_dict: "{{ cl_bonds }}" notify: reload networking # In vars file diff --git a/network/cumulus/cl_bridge.py b/network/cumulus/cl_bridge.py index 256fa427d06..752d4fdd6e4 100644 --- a/network/cumulus/cl_bridge.py +++ b/network/cumulus/cl_bridge.py @@ -124,7 +124,7 @@ virtual_ip: "{{ item.value.virtual_ip|default(omit) }}" virtual_mac: "{{ item.value.virtual_mac|default(omit) }}" mstpctl_treeprio: "{{ item.value.mstpctl_treeprio|default(omit) }}" -with_dict: cl_bridges +with_dict: "{{ cl_bridges }}" notify: reload networking # In vars file diff --git a/network/cumulus/cl_interface.py b/network/cumulus/cl_interface.py index 3fd87bf9195..241a69c7eff 100644 --- a/network/cumulus/cl_interface.py +++ b/network/cumulus/cl_interface.py @@ -151,7 +151,7 @@ mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}" mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}" mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}" -with_dict: cl_interfaces +with_dict: "{{ cl_interfaces }}" notify: reload networking From f4bd008184f9ffe5cab05dd7ce2443945701b3f8 Mon Sep 17 00:00:00 2001 From: Ben Tomasik Date: Thu, 10 Nov 2016 17:05:09 -0600 Subject: [PATCH 637/770] Add support for suspending scaling processes Ref: http://docs.aws.amazon.com/autoscaling/latest/userguide/as-suspend-resume-processes.html --- cloud/amazon/ec2_asg.py | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 6cf0bc8f9b4..18febb19190 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -142,6 +142,13 @@ default: ['autoscaling:EC2_INSTANCE_LAUNCH', 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', 'autoscaling:EC2_INSTANCE_TERMINATE', 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR'] required: false version_added: "2.2" + suspend_processes: + description: + - A list of scaling processes to suspend. + required: False + default: [] + choices: ['Launch', 'Terminate', 'HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions', 'AddToLoadBalancer'] + version_added: "2.2" extends_documentation_fragment: - aws - ec2 @@ -387,6 +394,28 @@ def wait_for_elb(asg_connection, module, group_name): module.fail_json(msg = "Waited too long for ELB instances to be healthy. %s" % time.asctime()) log.debug("Waiting complete. ELB thinks {0} instances are healthy.".format(healthy_instances)) + +def suspend_processes(as_group, module): + suspend_processes = set(module.params.get('suspend_processes')) + + try: + suspended_processes = set([p.process_name for p in as_group.suspended_processes]) + except AttributeError: + # New ASG being created, no suspended_processes defined yet + suspended_processes = set() + + if suspend_processes == suspended_processes: + return False + + resume_processes = list(suspended_processes - suspend_processes) + if resume_processes: + as_group.resume_processes(resume_processes) + + if suspend_processes: + as_group.suspend_processes(list(suspend_processes)) + + return True + def create_autoscaling_group(connection, module): group_name = module.params.get('name') load_balancers = module.params['load_balancers'] @@ -450,6 +479,7 @@ def create_autoscaling_group(connection, module): try: connection.create_auto_scaling_group(ag) + suspend_processes(ag, module) if wait_for_instances: wait_for_new_inst(module, connection, group_name, wait_timeout, desired_capacity, 'viable_instances') wait_for_elb(connection, module, group_name) @@ -466,6 +496,10 @@ def create_autoscaling_group(connection, module): else: as_group = as_groups[0] changed = False + + if suspend_processes(as_group, module): + changed = True + for attr in ASG_ATTRIBUTES: if module.params.get(attr, None) is not None: module_attr = module.params.get(attr) @@ -838,7 +872,8 @@ def main(): 'autoscaling:EC2_INSTANCE_LAUNCH_ERROR', 'autoscaling:EC2_INSTANCE_TERMINATE', 'autoscaling:EC2_INSTANCE_TERMINATE_ERROR' - ]) + ]), + suspend_processes=dict(type='list', default=[]) ), ) From af645041e537181c10dc3bfec59a53dc6523ba4b Mon Sep 17 00:00:00 2001 From: Ben Tomasik Date: Fri, 11 Nov 2016 08:42:39 -0600 Subject: [PATCH 638/770] Set suspend_processes version_added to 2.3 --- cloud/amazon/ec2_asg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 18febb19190..0fb569cc5b9 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -148,7 +148,7 @@ required: False default: [] choices: ['Launch', 'Terminate', 'HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions', 'AddToLoadBalancer'] - version_added: "2.2" + version_added: "2.3" extends_documentation_fragment: - aws - ec2 From 00e64def786e1b83774fb26d87826b01cae1a474 Mon Sep 17 00:00:00 2001 From: Lars Van Casteren Date: Fri, 11 Nov 2016 15:54:58 +0100 Subject: [PATCH 639/770] Docs update for `os_security_group` (#5531) The example used equal characters and not colon characters. --- cloud/openstack/os_security_group.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/cloud/openstack/os_security_group.py b/cloud/openstack/os_security_group.py index f2a2bb6ac1e..e5d0b8996a8 100644 --- a/cloud/openstack/os_security_group.py +++ b/cloud/openstack/os_security_group.py @@ -53,17 +53,17 @@ EXAMPLES = ''' # Create a security group - os_security_group: - cloud=mordred - state=present - name=foo - description=security group for foo servers + cloud: mordred + state: present + name: foo + description: security group for foo servers # Update the existing 'foo' security group description - os_security_group: - cloud=mordred - state=present - name=foo - description=updated description for the foo security group + cloud: mordred + state: present + name: foo + description: updated description for the foo security group ''' From 135c70bdd8c9398fa0e716602d940edc5c82183f Mon Sep 17 00:00:00 2001 From: Zaius Dr Date: Fri, 11 Nov 2016 16:54:10 +0100 Subject: [PATCH 640/770] Improve `ec2` module Python3 Support (#5497) Imported six module from ansible module_utils for backwards compatibility. --- cloud/amazon/ec2.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 01d105fd35c..78193b18a84 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -587,6 +587,8 @@ import time from ast import literal_eval +from ansible.module_utils.six import iteritems +from ansible.module_utils.six import get_function_code try: import boto.ec2 @@ -614,7 +616,7 @@ def find_running_instances_by_count_tag(module, ec2, count_tag, zone=None): def _set_none_to_blank(dictionary): result = dictionary - for k in result.iterkeys(): + for k in result: if isinstance(result[k], dict): result[k] = _set_none_to_blank(result[k]) elif not result[k]: @@ -644,14 +646,14 @@ def get_reservations(module, ec2, tags=None, state=None, zone=None): for x in tags: if isinstance(x, dict): x = _set_none_to_blank(x) - filters.update(dict(("tag:"+tn, tv) for (tn,tv) in x.iteritems())) + filters.update(dict(("tag:"+tn, tv) for (tn,tv) in iteritems(x))) else: filters.update({"tag-key": x}) # if dict, add the key and value to the filter if isinstance(tags, dict): tags = _set_none_to_blank(tags) - filters.update(dict(("tag:"+tn, tv) for (tn,tv) in tags.iteritems())) + filters.update(dict(("tag:"+tn, tv) for (tn,tv) in iteritems(tags))) if state: # http://stackoverflow.com/questions/437511/what-are-the-valid-instancestates-for-the-amazon-ec2-api @@ -751,7 +753,7 @@ def boto_supports_profile_name_arg(ec2): True if Boto library accept instance_profile_name argument, else false """ run_instances_method = getattr(ec2, 'run_instances') - return 'instance_profile_name' in run_instances_method.func_code.co_varnames + return 'instance_profile_name' in get_function_code(run_instances_method).co_varnames def create_block_device(module, ec2, volume): # Not aware of a way to determine this programatically @@ -801,7 +803,7 @@ def boto_supports_param_in_spot_request(ec2, param): True if boto library has the named param as an argument on the request_spot_instances method, else False """ method = getattr(ec2, 'request_spot_instances') - return param in method.func_code.co_varnames + return param in get_function_code(method).co_varnames def await_spot_requests(module, ec2, spot_requests, count): """ From afa1750105318e3535038d2d631acc4b4db566e9 Mon Sep 17 00:00:00 2001 From: "Patrick F. Marques" Date: Fri, 11 Nov 2016 12:52:05 +0000 Subject: [PATCH 641/770] Remove duplicated option from examples --- cloud/docker/docker_image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/docker/docker_image.py b/cloud/docker/docker_image.py index 7d97d984190..a7450b0b23a 100644 --- a/cloud/docker/docker_image.py +++ b/cloud/docker/docker_image.py @@ -224,7 +224,6 @@ tag: v1 push: yes load_path: my_sinatra.tar - push: True - name: Build image and with buildargs docker_image: From 8d1fe3b444737b26d6c634fc55010239ad342174 Mon Sep 17 00:00:00 2001 From: einarc Date: Fri, 11 Nov 2016 10:10:22 -0800 Subject: [PATCH 642/770] Avoid `TypeError` when desired_capacity is not provided to `ec2_asg` module (#5501) Moving the "check if min_size/max_size/desired_capacity..." code to execute BEFORE the desired_capacity code is used in the following operation: num_new_inst_needed = desired_capacity - len(new_instances) Otherwise the following exception occurs when desired_capacity is not specified and you're replacing instances: num_new_inst_needed = desired_capacity - len(new_instances) TypeError: unsupported operand type(s) for -: 'NoneType' and 'int' Stack Trace: An exception occurred during task execution. The full traceback is: Traceback (most recent call last): File "/var/lib/awx/.ansible/tmp/ansible-tmp-1478229985.74-62334493713074/ec2_asg", line 3044, in main() File "/var/lib/awx/.ansible/tmp/ansible-tmp-1478229985.74-62334493713074/ec2_asg", line 3038, in main replace_changed, asg_properties=replace(connection, module) File "/var/lib/awx/.ansible/tmp/ansible-tmp-1478229985.74-62334493713074/ec2_asg", line 2778, in replace num_new_inst_needed = desired_capacity - len(new_instances) TypeError: unsupported operand type(s) for -: 'NoneType' and 'int' fatal: [localhost]: FAILED! => {"changed": false, "failed": true, "invocation": {"module_name": "ec2_asg"}, "module_stderr": "Traceback (most recent call last):\n File \"/var/lib/awx/.ansible/tmp/ansible-tmp-1478229985.74-62334493713074/ec2_asg\", line 3044, in \n main()\n File \"/var/lib/awx/.ansible/tmp/ansible-tmp-1478229985.74-62334493713074/ec2_asg\", line 3038, in main\n replace_changed, asg_properties=replace(connection, module)\n File \"/var/lib/awx/.ansible/tmp/ansible-tmp-1478229985.74-62334493713074/ec2_asg\", line 2778, in replace\n num_new_inst_needed = desired_capacity - len(new_instances)\nTypeError: unsupported operand type(s) for -: 'NoneType' and 'int'\n", "module_stdout": "", "msg": "MODULE FAILURE", "parsed": false} to retry, use: --limit @ --- cloud/amazon/ec2_asg.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 6cf0bc8f9b4..beb1d5ad730 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -598,6 +598,14 @@ def replace(connection, module): instances = props['instances'] if replace_instances: instances = replace_instances + + #check if min_size/max_size/desired capacity have been specified and if not use ASG values + if min_size is None: + min_size = as_group.min_size + if max_size is None: + max_size = as_group.max_size + if desired_capacity is None: + desired_capacity = as_group.desired_capacity # check to see if instances are replaceable if checking launch configs new_instances, old_instances = get_instances_by_lc(props, lc_check, instances) @@ -620,14 +628,7 @@ def replace(connection, module): if not old_instances: changed = False return(changed, props) - - #check if min_size/max_size/desired capacity have been specified and if not use ASG values - if min_size is None: - min_size = as_group.min_size - if max_size is None: - max_size = as_group.max_size - if desired_capacity is None: - desired_capacity = as_group.desired_capacity + # set temporary settings and wait for them to be reached # This should get overwritten if the number of instances left is less than the batch size. From 1f0d62b36d1e8d905503772d1e5a75926af033de Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 11 Nov 2016 19:39:23 +0000 Subject: [PATCH 643/770] Use native YAML (#5571) --- system/authorized_key.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index 084ce95f186..0b15d2fd0a3 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -92,10 +92,14 @@ EXAMPLES = ''' # Example using key data from a local file on the management machine -- authorized_key: user=charlie key="{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" +- authorized_key: + user: charlie + key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" # Using github url as key source -- authorized_key: user=charlie key=https://github.com/charlie.keys +- authorized_key: + user: charlie + key: https://github.com/charlie.keys # Using alternate directory locations: - authorized_key: @@ -114,19 +118,28 @@ # Using key_options: - authorized_key: user: charlie - key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" + key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" key_options: 'no-port-forwarding,from="10.0.1.1"' # Using validate_certs: -- authorized_key: user=charlie key=https://github.com/user.keys validate_certs=no +- authorized_key: + user: charlie + key: https://github.com/user.keys + validate_certs: no # Set up authorized_keys exclusively with one key -- authorized_key: user=root key="{{ item }}" state=present exclusive=yes +- authorized_key: + user: root + key: '{{ item }}' + state: present + exclusive: yes with_file: - public_keys/doe-jane # Copies the key from the user who is running ansible to the remote machine user ubuntu -- authorized_key: user=ubuntu key="{{ lookup('file', lookup('env','HOME') + '/.ssh/id_rsa.pub') }}" +- authorized_key: + user: ubuntu + key: "{{ lookup('file', lookup('env','HOME') + '/.ssh/id_rsa.pub') }}" become: yes ''' From 8c762d7b02f0b904da7dade882e0588fb5e97f13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Mon, 14 Nov 2016 09:32:42 +0100 Subject: [PATCH 644/770] stat: doc: add version for new returns (#5594) --- files/stat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/stat.py b/files/stat.py index c07cf699d89..33a2fc3a67d 100644 --- a/files/stat.py +++ b/files/stat.py @@ -303,17 +303,17 @@ type: string sample: us-ascii readable: - description: Tells you if the invoking user has the right to read the path + description: Tells you if the invoking user has the right to read the path, added in version 2.2 returned: success, path exists and user can read the path type: boolean sample: False writeable: - description: Tells you if the invoking user has the right to write the path + description: Tells you if the invoking user has the right to write the path, added in version 2.2 returned: success, path exists and user can write the path type: boolean sample: False executable: - description: Tells you if the invoking user has the execute the path + description: Tells you if the invoking user has the execute the path, added in version 2.2 returned: success, path exists and user can execute the path type: boolean sample: False From 4863335cfac0266dca8e0e92157c57019546fb61 Mon Sep 17 00:00:00 2001 From: Gyorgy Szombathelyi Date: Fri, 11 Nov 2016 14:21:41 +0100 Subject: [PATCH 645/770] Ini_file: fix regression with the create option The new create option with the default value 'no' changes the behavior from the previous Ansible releases. Change the default to 'yes' to create missing ini files by default. Fixes: #5488 --- files/ini_file.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 4f74bda5fad..4be87f63ab2 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -80,10 +80,10 @@ create: required: false choices: [ "yes", "no" ] - default: "no" + default: "yes" description: - - If specified, the file will be created if it does not already exist. - By default it will fail if the file is missing. + - If set to 'no', the module will fail if the file does not already exist. + By default it will create the file if it is missing. version_added: "2.2" notes: - While it is possible to add an I(option) without specifying a I(value), this makes @@ -252,7 +252,7 @@ def main(): backup = dict(default='no', type='bool'), state = dict(default='present', choices=['present', 'absent']), no_extra_spaces = dict(required=False, default=False, type='bool'), - create=dict(default=False, type='bool') + create=dict(default=True, type='bool') ), add_file_common_args = True, supports_check_mode = True From 88f1e67b1f4693a830e0f501efdcefb3aa9da0e8 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Nov 2016 12:15:10 -0500 Subject: [PATCH 646/770] added file flags/attributes to stat (#5358) depends on http://github.com/ansible/ansible/issue/18213 also documented return version of fields added in 2.2 added get_mime to keep consistency changed default mime behaviour --- files/stat.py | 66 +++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/files/stat.py b/files/stat.py index 33a2fc3a67d..bccb2c0c67e 100644 --- a/files/stat.py +++ b/files/stat.py @@ -53,16 +53,24 @@ default: sha1 aliases: [ 'checksum_algo', 'checksum' ] version_added: "2.0" - mime: + get_mime: description: - Use file magic and return data about the nature of the file. this uses the 'file' utility found on most Linux/Unix systems. - This will add both `mime_type` and 'charset' fields to the return, if possible. + - In 2.3 this option changed from 'mime' to 'get_mime' and the default changed to 'Yes' required: false choices: [ Yes, No ] - default: No + default: Yes version_added: "2.1" - aliases: [ 'mime_type', 'mime-type' ] + aliases: [ 'mime', 'mime_type', 'mime-type' ] + get_attributes: + description: + - Get file attributes using lsattr tool if present. + required: false + default: True + version_added: "2.3" + aliases: [ 'attributes', 'attr' ] author: "Bruce Pennypacker (@bpennypacker)" ''' @@ -107,7 +115,7 @@ RETURN = ''' stat: - description: dictionary containing all the stat data + description: dictionary containing all the stat data, some platforms might add additional fields returned: success type: dictionary contains: @@ -307,16 +315,25 @@ returned: success, path exists and user can read the path type: boolean sample: False + version_added: 2.2 writeable: description: Tells you if the invoking user has the right to write the path, added in version 2.2 returned: success, path exists and user can write the path type: boolean sample: False + version_added: 2.2 executable: description: Tells you if the invoking user has the execute the path, added in version 2.2 returned: success, path exists and user can execute the path type: boolean sample: False + version_added: 2.2 + attributes: + description: list of file attributes + returned: success, path exists and user can execute the path + type: boolean + sample: [ immutable, extent ] + version_added: 2.3 ''' import errno @@ -326,11 +343,10 @@ import stat # import module snippets -from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.basic import AnsibleModule, format_attributes from ansible.module_utils.pycompat24 import get_exception from ansible.module_utils._text import to_bytes - def format_output(module, path, st): mode = st.st_mode @@ -380,7 +396,7 @@ def format_output(module, path, st): ('st_birthtime', 'birthtime'), # RISCOS ('st_ftype', 'file_type'), - ('st_attrs', 'attributes'), + ('st_attrs', 'attrs'), ('st_obtype', 'object_type'), # OS X ('st_rsize', 'real_size'), @@ -401,10 +417,11 @@ def main(): follow=dict(default='no', type='bool'), get_md5=dict(default='yes', type='bool'), get_checksum=dict(default='yes', type='bool'), + get_mime=dict(default=True, type='bool', aliases=['mime', 'mime_type', 'mime-type']), + get_attributes=dict(default=True, type='bool', aliases=['attributes', 'attr']), checksum_algorithm=dict(default='sha1', type='str', choices=['sha1', 'sha224', 'sha256', 'sha384', 'sha512'], aliases=['checksum_algo', 'checksum']), - mime=dict(default=False, type='bool', aliases=['mime_type', 'mime-type']), ), supports_check_mode=True ) @@ -412,7 +429,8 @@ def main(): path = module.params.get('path') b_path = to_bytes(path, errors='surrogate_or_strict') follow = module.params.get('follow') - get_mime = module.params.get('mime') + get_mime = module.params.get('get_mime') + get_attr = module.params.get('get_attributes') get_md5 = module.params.get('get_md5') get_checksum = module.params.get('get_checksum') checksum_algorithm = module.params.get('checksum_algorithm') @@ -469,15 +487,27 @@ def main(): # try to get mime data if requested if get_mime: output['mimetype'] = output['charset'] = 'unknown' - filecmd = [module.get_bin_path('file', True), '-i', path] - try: - rc, out, err = module.run_command(filecmd) - if rc == 0: - mimetype, charset = out.split(':')[1].split(';') - output['mimetype'] = mimetype.strip() - output['charset'] = charset.split('=')[1].strip() - except: - pass + mimecmd = module.get_bin_path('file') + if mimecmd: + mimecmd = [mimecmd, '-i', path] + try: + rc, out, err = module.run_command(mimecmd) + if rc == 0: + mimetype, charset = out.split(':')[1].split(';') + output['mimetype'] = mimetype.strip() + output['charset'] = charset.split('=')[1].strip() + except: + pass + + # try to get attr data + if get_attr: + output['version'] = None + output['attributes'] = [] + output['attr_flags'] = '' + out = module.get_file_attributes(path) + for x in ('version', 'attributes', 'attr_flags'): + if x in out: + output[x] = out[x] module.exit_json(changed=False, stat=output) From 75411fa9e3fc325bf093a4b73fc351765d9e4f08 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 14 Nov 2016 12:15:22 -0500 Subject: [PATCH 647/770] Revert "stat: doc: add version for new returns" --- files/stat.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/files/stat.py b/files/stat.py index bccb2c0c67e..7f522abb0ca 100644 --- a/files/stat.py +++ b/files/stat.py @@ -311,19 +311,19 @@ type: string sample: us-ascii readable: - description: Tells you if the invoking user has the right to read the path, added in version 2.2 + description: Tells you if the invoking user has the right to read the path returned: success, path exists and user can read the path type: boolean sample: False version_added: 2.2 writeable: - description: Tells you if the invoking user has the right to write the path, added in version 2.2 + description: Tells you if the invoking user has the right to write the path returned: success, path exists and user can write the path type: boolean sample: False version_added: 2.2 executable: - description: Tells you if the invoking user has the execute the path, added in version 2.2 + description: Tells you if the invoking user has the execute the path returned: success, path exists and user can execute the path type: boolean sample: False From d9ff373cec5ecd3bc90c021dd746e552aedac99b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:12:53 -0400 Subject: [PATCH 648/770] Change examples syntax on script module --- commands/script.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/commands/script.py b/commands/script.py index 1d07bbad80e..be8d9a1b437 100644 --- a/commands/script.py +++ b/commands/script.py @@ -47,7 +47,7 @@ notes: - It is usually preferable to write Ansible modules than pushing scripts. Convert your script to an Ansible module for bonus points! - The ssh connection plugin will force psuedo-tty allocation via -tt when scripts are executed. psuedo-ttys do not have a stderr channel and all stderr is sent to stdout. If you depend on separated stdout and stderr result keys, please switch to a copy+command set of tasks instead of using script. -author: +author: - Ansible Core Team - Michael DeHaan """ @@ -57,8 +57,12 @@ - script: /some/local/script.sh --some-arguments 1234 # Run a script that creates a file, but only if the file is not yet created -- script: /some/local/create_file.sh --some-arguments 1234 creates=/the/created/file.txt +- script: /some/local/create_file.sh --some-arguments 1234 + args: + creates: /the/created/file.txt # Run a script that removes a file, but only if the file is not yet removed -- script: /some/local/remove_file.sh --some-arguments 1234 removes=/the/removed/file.txt +- script: /some/local/remove_file.sh --some-arguments 1234 + args: + removes: /the/removed/file.txt ''' From 5fcd8ecf104e2a7dac643a22617e2e235a9ff58f Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:18:17 -0400 Subject: [PATCH 649/770] Change examples syntax on shell module --- commands/shell.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/commands/shell.py b/commands/shell.py index 96bbae5e296..ca17ddda775 100644 --- a/commands/shell.py +++ b/commands/shell.py @@ -68,11 +68,11 @@ playbooks will follow the trend of using M(command) unless M(shell) is explicitly required. When running ad-hoc commands, use your best judgement. - - To sanitize any variables passed to the shell module, you should use + - To sanitize any variables passed to the shell module, you should use "{{ var | quote }}" instead of just "{{ var }}" to make sure they don't include evil things like semicolons. requirements: [ ] -author: +author: - Ansible Core Team - Michael DeHaan ''' @@ -83,7 +83,9 @@ - shell: somescript.sh >> somelog.txt # Change the working directory to somedir/ before executing the command. -- shell: somescript.sh >> somelog.txt chdir=somedir/ +- shell: somescript.sh >> somelog.txt + args: + chdir: somedir/ # You can also use the 'args' form to provide the options. This command # will change the working directory to somedir/ and will only run when @@ -146,4 +148,4 @@ returned: always type: list of strings sample: [u'Clustering node rabbit@slave1 with rabbit@master ...'] -''' \ No newline at end of file +''' From 74f6c3f94a123786545b3a42894b36312e2f3e59 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:19:37 -0400 Subject: [PATCH 650/770] Change examples syntax on mysql_db module --- database/mysql/mysql_db.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index f98547b905b..2b6a19d57a3 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -79,11 +79,18 @@ EXAMPLES = ''' # Create a new database with name 'bobdata' -- mysql_db: name=bobdata state=present +- mysql_db: + name: bobdata + state: present # Copy database dump file to remote host and restore it to database 'my_db' -- copy: src=dump.sql.bz2 dest=/tmp -- mysql_db: name=my_db state=import target=/tmp/dump.sql.bz2 +- copy: + src: dump.sql.bz2 + dest: /tmp +- mysql_db: + name: my_db + state: import + target: /tmp/dump.sql.bz2 # Dumps all databases to hostname.sql - mysql_db: state=dump name=all target=/tmp/{{ inventory_hostname }}.sql From 800438a201b53d1ebf1680f831de7ad5909ce5da Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:20:09 -0400 Subject: [PATCH 651/770] Change examples syntax on mysql_user module --- database/mysql/mysql_user.py | 70 +++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 12 deletions(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 010cdce6ae1..87e8318adc7 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -111,43 +111,89 @@ EXAMPLES = """ # Removes anonymous user account for localhost -- mysql_user: name='' host=localhost state=absent +- mysql_user: + name: '' + host: localhost + state: absent # Removes all anonymous user accounts -- mysql_user: name='' host_all=yes state=absent +- mysql_user: + name: '' + host_all: yes + state: absent # Create database user with name 'bob' and password '12345' with all database privileges -- mysql_user: name=bob password=12345 priv=*.*:ALL state=present +- mysql_user: + name: bob + password: 12345 + priv: '*.*:ALL' + state: present # Create database user with name 'bob' and previously hashed mysql native password '*EE0D72C1085C46C5278932678FBE2C6A782821B4' with all database privileges -- mysql_user: name=bob password='*EE0D72C1085C46C5278932678FBE2C6A782821B4' encrypted=yes priv=*.*:ALL state=present +- mysql_user: + name: bob + password: '*EE0D72C1085C46C5278932678FBE2C6A782821B4' + encrypted: yes + priv: '*.*:ALL' + state: present # Creates database user 'bob' and password '12345' with all database privileges and 'WITH GRANT OPTION' -- mysql_user: name=bob password=12345 priv=*.*:ALL,GRANT state=present +- mysql_user: + name: bob + password: 12345 + priv: '*.*:ALL,GRANT' + state: present # Modify user Bob to require SSL connections. Note that REQUIRESSL is a special privilege that should only apply to *.* by itself. -- mysql_user: name=bob append_privs=true priv=*.*:REQUIRESSL state=present +- mysql_user: + name: bob + append_privs: true + priv: '*.*:REQUIRESSL' + state: present # Ensure no user named 'sally'@'localhost' exists, also passing in the auth credentials. -- mysql_user: login_user=root login_password=123456 name=sally state=absent +- mysql_user: + login_user: root + login_password: 123456 + name: sally + state: absent # Ensure no user named 'sally' exists at all -- mysql_user: name=sally host_all=yes state=absent +- mysql_user: + name: sally + host_all: yes + state: absent # Specify grants composed of more than one word -- mysql_user: name=replication password=12345 priv="*.*:REPLICATION CLIENT" state=present +- mysql_user: + name: replication + password: 12345 + priv: "*.*:REPLICATION CLIENT" + state: present # Revoke all privileges for user 'bob' and password '12345' -- mysql_user: name=bob password=12345 priv=*.*:USAGE state=present +- mysql_user: + name: bob + password: 12345 + priv: "*.*:USAGE" + state: present # Example privileges string format mydb.*:INSERT,UPDATE/anotherdb.*:SELECT/yetanotherdb.*:ALL # Example using login_unix_socket to connect to server -- mysql_user: name=root password=abc123 login_unix_socket=/var/run/mysqld/mysqld.sock +- mysql_user: + name: root + password: abc123 + login_unix_socket: /var/run/mysqld/mysqld.sock # Example of skipping binary logging while adding user 'bob' -- mysql_user: name=bob password=12345 priv=*.*:USAGE state=present sql_log_bin=no +- mysql_user: + name: bob + password: 12345 + priv: "*.*:USAGE" + state: present + sql_log_bin: no # Example .my.cnf file for setting the root password From f3c132cc2525fefcdf038f12e521dede2522095b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:20:41 -0400 Subject: [PATCH 652/770] Change examples syntax on mysql_variables module --- database/mysql/mysql_variables.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/database/mysql/mysql_variables.py b/database/mysql/mysql_variables.py index 5cb6bf6fe5b..014b768dabc 100644 --- a/database/mysql/mysql_variables.py +++ b/database/mysql/mysql_variables.py @@ -44,10 +44,13 @@ ''' EXAMPLES = ''' # Check for sync_binlog setting -- mysql_variables: variable=sync_binlog +- mysql_variables: + variable: sync_binlog # Set read_only variable to 1 -- mysql_variables: variable=read_only value=1 +- mysql_variables: + variable: read_only + value: 1 ''' From 605c4df078a5b3a5531e4d1f362709012d518f90 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:21:05 -0400 Subject: [PATCH 653/770] > Change examples syntax on postgresql_db module --- database/postgresql/postgresql_db.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/database/postgresql/postgresql_db.py b/database/postgresql/postgresql_db.py index 64871ed1734..70cc96dc8fe 100755 --- a/database/postgresql/postgresql_db.py +++ b/database/postgresql/postgresql_db.py @@ -95,16 +95,18 @@ EXAMPLES = ''' # Create a new database with name "acme" -- postgresql_db: name=acme +- postgresql_db: + name: acme # Create a new database with name "acme" and specific encoding and locale # settings. If a template different from "template0" is specified, encoding # and locale settings must match those of the template. -- postgresql_db: name=acme - encoding='UTF-8' - lc_collate='de_DE.UTF-8' - lc_ctype='de_DE.UTF-8' - template='template0' +- postgresql_db: + name: acme + encoding: UTF-8 + lc_collate: de_DE.UTF-8 + lc_ctype: de_DE.UTF-8 + template: template0 ''' try: From 67c02b63467de80b1fc9d62dcae601904de5c72b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:34:28 -0400 Subject: [PATCH 654/770] Change examples syntax on postgresql_privs module --- database/postgresql/postgresql_privs.py | 120 ++++++++++++------------ 1 file changed, 60 insertions(+), 60 deletions(-) diff --git a/database/postgresql/postgresql_privs.py b/database/postgresql/postgresql_privs.py index 74b2630b747..ea49a55fc6f 100644 --- a/database/postgresql/postgresql_privs.py +++ b/database/postgresql/postgresql_privs.py @@ -143,90 +143,90 @@ # On database "library": # GRANT SELECT, INSERT, UPDATE ON TABLE public.books, public.authors # TO librarian, reader WITH GRANT OPTION -- postgresql_privs: > - database=library - state=present - privs=SELECT,INSERT,UPDATE - type=table - objs=books,authors - schema=public - roles=librarian,reader - grant_option=yes +- postgresql_privs: + database: library + state: present + privs: SELECT,INSERT,UPDATE + type: table + objs: books,authors + schema: public + roles: librarian,reader + grant_option: yes # Same as above leveraging default values: -- postgresql_privs: > - db=library - privs=SELECT,INSERT,UPDATE - objs=books,authors - roles=librarian,reader - grant_option=yes +- postgresql_privs: + db: library + privs: SELECT,INSERT,UPDATE + objs: books,authors + roles: librarian,reader + grant_option: yes # REVOKE GRANT OPTION FOR INSERT ON TABLE books FROM reader # Note that role "reader" will be *granted* INSERT privilege itself if this -# isn't already the case (since state=present). -- postgresql_privs: > - db=library - state=present - priv=INSERT - obj=books - role=reader - grant_option=no +# isn't already the case (since state: present). +- postgresql_privs: + db: library + state: present + priv: INSERT + obj: books + role: reader + grant_option: no # REVOKE INSERT, UPDATE ON ALL TABLES IN SCHEMA public FROM reader # "public" is the default schema. This also works for PostgreSQL 8.x. -- postgresql_privs: > - db=library - state=absent - privs=INSERT,UPDATE - objs=ALL_IN_SCHEMA - role=reader +- postgresql_privs: + db: library + state: absent + privs: INSERT,UPDATE + objs: ALL_IN_SCHEMA + role: reader # GRANT ALL PRIVILEGES ON SCHEMA public, math TO librarian -- postgresql_privs: > - db=library - privs=ALL - type=schema - objs=public,math - role=librarian +- postgresql_privs: + db: library + privs: ALL + type: schema + objs: public,math + role: librarian # GRANT ALL PRIVILEGES ON FUNCTION math.add(int, int) TO librarian, reader # Note the separation of arguments with colons. -- postgresql_privs: > - db=library - privs=ALL - type=function - obj=add(int:int) - schema=math - roles=librarian,reader +- postgresql_privs: + db: library + privs: ALL + type: function + obj: add(int:int) + schema: math + roles: librarian,reader # GRANT librarian, reader TO alice, bob WITH ADMIN OPTION # Note that group role memberships apply cluster-wide and therefore are not # restricted to database "library" here. -- postgresql_privs: > - db=library - type=group - objs=librarian,reader - roles=alice,bob - admin_option=yes +- postgresql_privs: + db: library + type: group + objs: librarian,reader + roles: alice,bob + admin_option: yes # GRANT ALL PRIVILEGES ON DATABASE library TO librarian -# Note that here "db=postgres" specifies the database to connect to, not the +# Note that here "db: postgres" specifies the database to connect to, not the # database to grant privileges on (which is specified via the "objs" param) -- postgresql_privs: > - db=postgres - privs=ALL - type=database - obj=library - role=librarian +- postgresql_privs: + db: postgres + privs: ALL + type: database + obj: library + role: librarian # GRANT ALL PRIVILEGES ON DATABASE library TO librarian # If objs is omitted for type "database", it defaults to the database # to which the connection is established -- postgresql_privs: > - db=library - privs=ALL - type=database - role=librarian +- postgresql_privs: + db: library + privs: ALL + type: database + role: librarian """ try: From c20e4b12e3a85e41c60adbaceb406e837c6c82d8 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:35:34 -0400 Subject: [PATCH 655/770] Change examples syntax on postgresql_user module --- database/postgresql/postgresql_user.py | 29 +++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index d8b0b8bb20a..803751ea4ae 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -142,22 +142,41 @@ EXAMPLES = ''' # Create django user and grant access to database and products table -- postgresql_user: db=acme name=django password=ceec4eif7ya priv=CONNECT/products:ALL +- postgresql_user: + db: acme + name: django + password: ceec4eif7ya + priv: CONNECT/products:ALL # Create rails user, grant privilege to create other databases and demote rails from super user status -- postgresql_user: name=rails password=secret role_attr_flags=CREATEDB,NOSUPERUSER +- postgresql_user: + name: rails + password: secret + role_attr_flags: CREATEDB,NOSUPERUSER # Remove test user privileges from acme -- postgresql_user: db=acme name=test priv=ALL/products:ALL state=absent fail_on_user=no +- postgresql_user: + db: acme + name: test + priv: "ALL/products:ALL" + state: absent + fail_on_user: no # Remove test user from test database and the cluster -- postgresql_user: db=test name=test priv=ALL state=absent +- postgresql_user: + db: test + name: test + priv: ALL + state: absent # Example privileges string format INSERT,UPDATE/table:SELECT/anothertable:ALL # Remove an existing user's password -- postgresql_user: db=test user=test password=NULL +- postgresql_user: + db: test + user: test + password: NULL ''' import re From 5ef188bcf15f8668f70f7eb96557bd240c2952f5 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:36:47 -0400 Subject: [PATCH 656/770] Change examples syntax on acl module --- files/acl.py | 29 ++++++++++++++++++++++++----- 1 file changed, 24 insertions(+), 5 deletions(-) diff --git a/files/acl.py b/files/acl.py index 2b898452592..4974b6bbfcc 100644 --- a/files/acl.py +++ b/files/acl.py @@ -97,19 +97,38 @@ EXAMPLES = ''' # Grant user Joe read access to a file -- acl: name=/etc/foo.conf entity=joe etype=user permissions="r" state=present +- acl: + name: /etc/foo.conf + entity: joe + etype: user + permissions: r + state: present # Removes the acl for Joe on a specific file -- acl: name=/etc/foo.conf entity=joe etype=user state=absent +- acl: + name: /etc/foo.conf + entity: joe + etype: user + state: absent # Sets default acl for joe on foo.d -- acl: name=/etc/foo.d entity=joe etype=user permissions=rw default=yes state=present +- acl: + name: /etc/foo.d + entity: joe + etype: user + permissions: rw + default: yes + state: present # Same as previous but using entry shorthand -- acl: name=/etc/foo.d entry="default:user:joe:rw-" state=present +- acl: + name: /etc/foo.d + entry: "default:user:joe:rw-" + state: present # Obtain the acl for a specific file -- acl: name=/etc/foo.conf +- acl: + name: /etc/foo.conf register: acl_info ''' From 7fc150c33b702e95aa371d7d4132b0d3320f1408 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:39:56 -0400 Subject: [PATCH 657/770] Change examples syntax on assemble module --- files/assemble.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/files/assemble.py b/files/assemble.py index 39edbdd36a4..d1d1bb34d35 100644 --- a/files/assemble.py +++ b/files/assemble.py @@ -95,13 +95,21 @@ EXAMPLES = ''' # Example from Ansible Playbooks -- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf +- assemble: + src: /etc/someapp/fragments + dest: /etc/someapp/someapp.conf # When a delimiter is specified, it will be inserted in between each fragment -- assemble: src=/etc/someapp/fragments dest=/etc/someapp/someapp.conf delimiter='### START FRAGMENT ###' +- assemble: + src: /etc/someapp/fragments + dest: /etc/someapp/someapp.conf + delimiter: '### START FRAGMENT ###' # Copy a new "sshd_config" file into place, after passing validation with sshd -- assemble: src=/etc/ssh/conf.d/ dest=/etc/ssh/sshd_config validate='/usr/sbin/sshd -t -f %s' +- assemble: + src: /etc/ssh/conf.d/ + dest: /etc/ssh/sshd_config + validate: '/usr/sbin/sshd -t -f %s' ''' import codecs From 501403a164a2b6b42518d8f670d539ce680b9dc3 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:40:10 -0400 Subject: [PATCH 658/770] Change examples syntax on copy module --- files/copy.py | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/files/copy.py b/files/copy.py index d42c285ecc2..000b8ff269e 100644 --- a/files/copy.py +++ b/files/copy.py @@ -102,19 +102,43 @@ EXAMPLES = ''' # Example from Ansible Playbooks -- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode=0644 +- copy: + src: /srv/myfiles/foo.conf + dest: /etc/foo.conf + owner: foo + group: foo + mode: 0644 # The same example as above, but using a symbolic mode equivalent to 0644 -- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode="u=rw,g=r,o=r" +- copy: + src: /srv/myfiles/foo.conf + dest: /etc/foo.conf + owner: foo + group: foo + mode: "u=rw,g=r,o=r" # Another symbolic mode example, adding some permissions and removing others -- copy: src=/srv/myfiles/foo.conf dest=/etc/foo.conf owner=foo group=foo mode="u+rw,g-wx,o-rwx" +- copy: + src: /srv/myfiles/foo.conf + dest: /etc/foo.conf + owner: foo + group: foo + mode: "u+rw,g-wx,o-rwx" # Copy a new "ntp.conf file into place, backing up the original if it differs from the copied version -- copy: src=/mine/ntp.conf dest=/etc/ntp.conf owner=root group=root mode=644 backup=yes +- copy: + src: /mine/ntp.conf + dest: /etc/ntp.conf + owner: root + group: root + mode: 0644 + backup: yes # Copy a new "sudoers" file into place, after passing validation with visudo -- copy: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s' +- copy: + src: /mine/sudoers + dest: /etc/sudoers + validate: 'visudo -cf %s' ''' RETURN = ''' From 07d1c1c3f7eb65eb42b8266a2bed80d435148f73 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:41:08 -0400 Subject: [PATCH 659/770] Change examples syntax on fetch module --- files/fetch.py | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/files/fetch.py b/files/fetch.py index ad34964217e..9b3f07827be 100644 --- a/files/fetch.py +++ b/files/fetch.py @@ -65,7 +65,7 @@ will use the basename of the source file, similar to the copy module. Obviously this is only handy if the filenames are unique. requirements: [] -author: +author: - "Ansible Core Team" - "Michael DeHaan" notes: @@ -79,14 +79,25 @@ EXAMPLES = ''' # Store file into /tmp/fetched/host.example.com/tmp/somefile -- fetch: src=/tmp/somefile dest=/tmp/fetched +- fetch: + src: /tmp/somefile + dest: /tmp/fetched # Specifying a path directly -- fetch: src=/tmp/somefile dest=/tmp/prefix-{{ inventory_hostname }} flat=yes +- fetch: + src: /tmp/somefile + dest: /tmp/prefix-{{ inventory_hostname }} + flat: yes # Specifying a destination path -- fetch: src=/tmp/uniquefile dest=/tmp/special/ flat=yes +- fetch: + src: /tmp/uniquefile + dest: /tmp/special/ + flat: yes # Storing in a path relative to the playbook -- fetch: src=/tmp/uniquefile dest=special/prefix-{{ inventory_hostname }} flat=yes +- fetch: + src: /tmp/uniquefile + dest: special/prefix-{{ inventory_hostname }} + flat: yes ''' From 52274a400f1239038222e738b24779b2fa7d727b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:42:27 -0400 Subject: [PATCH 660/770] Change examples syntax on file module --- files/file.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/files/file.py b/files/file.py index f19a96355b7..ed796cb1f77 100644 --- a/files/file.py +++ b/files/file.py @@ -88,21 +88,41 @@ EXAMPLES = ''' # change file ownership, group and mode. When specifying mode using octal numbers, first digit should always be 0. -- file: path=/etc/foo.conf owner=foo group=foo mode=0644 -- file: src=/file/to/link/to dest=/path/to/symlink owner=foo group=foo state=link -- file: src=/tmp/{{ item.src }} dest={{ item.dest }} state=link +- file: + path: /etc/foo.conf + owner: foo + group: foo + mode: 0644 +- file: + src: /file/to/link/to + dest: /path/to/symlink + owner: foo + group: foo + state: link +- file: + src: /tmp/{{ item.src }} + dest: "{{ item.dest }}" + state: link with_items: - { src: 'x', dest: 'y' } - { src: 'z', dest: 'k' } # touch a file, using symbolic modes to set the permissions (equivalent to 0644) -- file: path=/etc/foo.conf state=touch mode="u=rw,g=r,o=r" +- file: + path: /etc/foo.conf + state: touch + mode: "u=rw,g=r,o=r" # touch the same file, but add/remove some permissions -- file: path=/etc/foo.conf state=touch mode="u+rw,g-wx,o-rwx" +- file: path=/etc/foo.conf + state: touch + mode: "u+rw,g-wx,o-rwx" # create a directory if it doesn't exist -- file: path=/etc/some_directory state=directory mode=0755 +- file: + path: /etc/some_directory + state: directory + mode: 0755 ''' From 939da41dbd6b9f13de684030c05c4767d1fa9bdc Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 15:43:02 -0400 Subject: [PATCH 661/770] Change examples syntax on find module --- files/find.py | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/files/find.py b/files/find.py index 9eae9e1bcc6..590a8771178 100644 --- a/files/find.py +++ b/files/find.py @@ -118,19 +118,37 @@ EXAMPLES = ''' # Recursively find /tmp files older than 2 days -- find: paths="/tmp" age="2d" recurse=yes +- find: + paths: "/tmp" + age: "2d" + recurse: yes # Recursively find /tmp files older than 4 weeks and equal or greater than 1 megabyte -- find: paths="/tmp" age="4w" size="1m" recurse=yes +- find: + paths: "/tmp" + age: "4w" + size: "1m" + recurse: yes # Recursively find /var/tmp files with last access time greater than 3600 seconds -- find: paths="/var/tmp" age="3600" age_stamp=atime recurse=yes +- find: + paths: "/var/tmp" + age: "3600" + age_stamp: atime + recurse: yes # find /var/log files equal or greater than 10 megabytes ending with .old or .log.gz -- find: paths="/var/tmp" patterns="*.old,*.log.gz" size="10m" +- find: + paths: "/var/tmp" + patterns: "*.old,*.log.gz" + size: "10m" # find /var/log files equal or greater than 10 megabytes ending with .old or .log.gz via regex -- find: paths="/var/tmp" patterns="^.*?\.(?:old|log\.gz)$" size="10m" use_regex=True +- find: + paths: "/var/tmp" + patterns: "^.*?\.(?:old|log\.gz)$" + size: "10m" + use_regex: True ''' RETURN = ''' From 83f42ecfd4784f4555055da4e4b5bd2bbcb04d1b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 16:25:34 -0400 Subject: [PATCH 662/770] Change examples syntax on ini_file module --- files/ini_file.py | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/files/ini_file.py b/files/ini_file.py index 4be87f63ab2..69f092a1630 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -95,13 +95,20 @@ EXAMPLES = ''' # Ensure "fav=lemonade is in section "[drinks]" in specified file -- ini_file: dest=/etc/conf section=drinks option=fav value=lemonade mode=0600 backup=yes +- ini_file: + dest: /etc/conf + section: drinks + option: fav + value: lemonade + mode: 0600 + backup: yes -- ini_file: dest=/etc/anotherconf - section=drinks - option=temperature - value=cold - backup=yes +- ini_file: + dest: /etc/anotherconf + section: drinks + option: temperature + value: cold + backup: yes ''' import os From 942eb37a5427835a87bf2ae884f278073ccb57aa Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 16:26:24 -0400 Subject: [PATCH 663/770] Change examples syntax on lineinfile module --- files/lineinfile.py | 63 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/files/lineinfile.py b/files/lineinfile.py index 045827f7fb8..4b6088c223a 100644 --- a/files/lineinfile.py +++ b/files/lineinfile.py @@ -120,26 +120,61 @@ """ EXAMPLES = r""" -- lineinfile: dest=/etc/selinux/config regexp=^SELINUX= line=SELINUX=enforcing - -- lineinfile: dest=/etc/sudoers state=absent regexp="^%wheel" - -- lineinfile: dest=/etc/hosts regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' owner=root group=root mode=0644 - -- lineinfile: dest=/etc/httpd/conf/httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080" - -- lineinfile: dest=/etc/services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default" +- lineinfile: + dest: /etc/selinux/config + regexp: '^SELINUX=' + line: 'SELINUX=enforcing' + +- lineinfile: + dest: /etc/sudoers + state: absent + regexp: '^%wheel' + +- lineinfile: + dest: /etc/hosts + regexp: '^127\.0\.0\.1' + line: '127.0.0.1 localhost' + owner: root + group: root + mode: 0644 + +- lineinfile: + dest: /etc/httpd/conf/httpd.conf + regexp: '^Listen ' + insertafter: '^#Listen ' + line: 'Listen 8080' + +- lineinfile: + dest: /etc/services + regexp: '^# port for http' + insertbefore: '^www.*80/tcp' + line: '# port for http by default' # Add a line to a file if it does not exist, without passing regexp -- lineinfile: dest=/tmp/testfile line="192.168.1.99 foo.lab.net foo" +- lineinfile: + dest: /tmp/testfile + line: '192.168.1.99 foo.lab.net foo' # Fully quoted because of the ': ' on the line. See the Gotchas in the YAML docs. -- lineinfile: "dest=/etc/sudoers state=present regexp='^%wheel' line='%wheel ALL=(ALL) NOPASSWD: ALL'" - -- lineinfile: dest=/opt/jboss-as/bin/standalone.conf regexp='^(.*)Xms(\d+)m(.*)$' line='\1Xms${xms}m\3' backrefs=yes +- lineinfile: " + dest: /etc/sudoers + state: present + regexp: '^%wheel' + line: '%wheel ALL=(ALL) NOPASSWD: ALL' + +- lineinfile: + dest: /opt/jboss-as/bin/standalone.conf + regexp: '^(.*)Xms(\d+)m(.*)$' + line: '\1Xms${xms}m\3' + backrefs: yes # Validate the sudoers file before saving -- lineinfile: dest=/etc/sudoers state=present regexp='^%ADMIN ALL\=' line='%ADMIN ALL=(ALL) NOPASSWD:ALL' validate='visudo -cf %s' +- lineinfile: + dest: /etc/sudoers + state: present + regexp: '^%ADMIN ALL=' + line: '%ADMIN ALL=(ALL) NOPASSWD: ALL' + validate: 'visudo -cf %s' """ import re From 5ac9e53f973e616e1bf60dc0f24c968ffbc29f03 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:36:22 -0400 Subject: [PATCH 664/770] Change example syntax on replace module --- files/replace.py | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/files/replace.py b/files/replace.py index ef66c223911..d4e3ab9f0c5 100644 --- a/files/replace.py +++ b/files/replace.py @@ -77,11 +77,24 @@ """ EXAMPLES = r""" -- replace: dest=/etc/hosts regexp='(\s+)old\.host\.name(\s+.*)?$' replace='\1new.host.name\2' backup=yes - -- replace: dest=/home/jdoe/.ssh/known_hosts regexp='^old\.host\.name[^\n]*\n' owner=jdoe group=jdoe mode=644 - -- replace: dest=/etc/apache/ports regexp='^(NameVirtualHost|Listen)\s+80\s*$' replace='\1 127.0.0.1:8080' validate='/usr/sbin/apache2ctl -f %s -t' +- replace: + dest: /etc/hosts + regexp: '(\s+)old\.host\.name(\s+.*)?$' + replace: '\1new.host.name\2' + backup: yes + +- replace: + dest: /home/jdoe/.ssh/known_hosts + regexp: '^old\.host\.name[^\n]*\n' + owner: jdoe + group: jdoe + mode: 0644 + +- replace: + dest: /etc/apache/ports + regexp: '^(NameVirtualHost|Listen)\s+80\s*$' + replace: '\1 127.0.0.1:8080' + validate: '/usr/sbin/apache2ctl -f %s -t' """ def write_changes(module,contents,dest): From 15890a46262a25074a5bb77f591de931ddf6eb58 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:36:40 -0400 Subject: [PATCH 665/770] Change example syntax on stat module --- files/stat.py | 39 ++++++++++++++++++++++++++++----------- 1 file changed, 28 insertions(+), 11 deletions(-) diff --git a/files/stat.py b/files/stat.py index 7f522abb0ca..363e94ad3f7 100644 --- a/files/stat.py +++ b/files/stat.py @@ -77,9 +77,11 @@ EXAMPLES = ''' # Obtain the stats of /etc/foo.conf, and check that the file still belongs # to 'root'. Fail otherwise. -- stat: path=/etc/foo.conf +- stat: + path: /etc/foo.conf register: st -- fail: msg="Whoops! file ownership has changed" +- fail: + msg: "Whoops! file ownership has changed" when: st.stat.pw_name != 'root' # Determine if a path exists and is a symlink. Note that if the path does @@ -87,30 +89,45 @@ # therefore, we must test whether it is defined. # Run this to understand the structure, the skipped ones do not pass the # check performed by 'when' -- stat: path=/path/to/something +- stat: + path: /path/to/something register: sym -- debug: msg="islnk isn't defined (path doesn't exist)" + +- debug: + msg: "islnk isn't defined (path doesn't exist)" when: sym.stat.islnk is not defined -- debug: msg="islnk is defined (path must exist)" + +- debug: + msg: "islnk is defined (path must exist)" when: sym.stat.islnk is defined -- debug: msg="Path exists and is a symlink" + +- debug: + msg: "Path exists and is a symlink" when: sym.stat.islnk is defined and sym.stat.islnk -- debug: msg="Path exists and isn't a symlink" + +- debug: + msg: "Path exists and isn't a symlink" when: sym.stat.islnk is defined and sym.stat.islnk == False # Determine if a path exists and is a directory. Note that we need to test # both that p.stat.isdir actually exists, and also that it's set to true. -- stat: path=/path/to/something +- stat: + path: /path/to/something register: p -- debug: msg="Path exists and is a directory" +- debug: + msg: "Path exists and is a directory" when: p.stat.isdir is defined and p.stat.isdir # Don't do md5 checksum -- stat: path=/path/to/myhugefile get_md5=no +- stat: + path: /path/to/myhugefile + get_md5: no # Use sha256 to calculate checksum -- stat: path=/path/to/something checksum_algorithm=sha256 +- stat: + path: /path/to/something + checksum_algorithm: sha256 ''' RETURN = ''' From cc8cdd9506d283937afc906e7d3bf3afa7ad4eb4 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:37:40 -0400 Subject: [PATCH 666/770] > Change example syntax on synchronize module --- files/synchronize.py | 81 ++++++++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 22 deletions(-) diff --git a/files/synchronize.py b/files/synchronize.py index b48a1f1a25f..89541840098 100644 --- a/files/synchronize.py +++ b/files/synchronize.py @@ -185,60 +185,97 @@ EXAMPLES = ''' # Synchronization of src on the control machine to dest on the remote hosts -synchronize: src=some/relative/path dest=/some/absolute/path +- synchronize: + src: some/relative/path + dest: /some/absolute/path # Synchronization using rsync protocol (push) -synchronize: src=some/relative/path/ dest=rsync://somehost.com/path/ +- synchronize: + src: some/relative/path/ + dest: rsync://somehost.com/path/ # Synchronization using rsync protocol (pull) -synchronize: mode=pull src=rsync://somehost.com/path/ dest=/some/absolute/path/ +- synchronize: + mode: pull + src: rsync://somehost.com/path/ + dest: /some/absolute/path/ # Synchronization using rsync protocol on delegate host (push) -synchronize: > - src=/some/absolute/path/ dest=rsync://somehost.com/path/ - delegate_to: delegate.host +- synchronize: + src: /some/absolute/path/ + dest: rsync://somehost.com/path/ + delegate_to: delegate.host # Synchronization using rsync protocol on delegate host (pull) -synchronize: > - mode=pull src=rsync://somehost.com/path/ dest=/some/absolute/path/ - delegate_to: delegate.host +- synchronize: + mode: pull + src: rsync://somehost.com/path/ + dest: /some/absolute/path/ + delegate_to: delegate.host # Synchronization without any --archive options enabled -synchronize: src=some/relative/path dest=/some/absolute/path archive=no +- synchronize: + src: some/relative/path + dest: /some/absolute/path + archive: no # Synchronization with --archive options enabled except for --recursive -synchronize: src=some/relative/path dest=/some/absolute/path recursive=no +- synchronize: + src: some/relative/path + dest: /some/absolute/path + recursive: no # Synchronization with --archive options enabled except for --times, with --checksum option enabled -synchronize: src=some/relative/path dest=/some/absolute/path checksum=yes times=no +- synchronize: + src: some/relative/path + dest: /some/absolute/path + checksum: yes + times: no # Synchronization without --archive options enabled except use --links -synchronize: src=some/relative/path dest=/some/absolute/path archive=no links=yes +- synchronize: + src: some/relative/path + dest: /some/absolute/path + archive: no + links: yes # Synchronization of two paths both on the control machine -local_action: synchronize src=some/relative/path dest=/some/absolute/path +- synchronize + src: some/relative/path + dest: /some/absolute/path + delegate_to: localhost # Synchronization of src on the inventory host to the dest on the localhost in pull mode -synchronize: mode=pull src=some/relative/path dest=/some/absolute/path +- synchronize: + mode: pull + src: some/relative/path + dest: /some/absolute/path # Synchronization of src on delegate host to dest on the current inventory host. -synchronize: +- synchronize: src: /first/absolute/path dest: /second/absolute/path -delegate_to: delegate.host + delegate_to: delegate.host # Synchronize two directories on one remote host. -synchronize: +- synchronize: src: /first/absolute/path dest: /second/absolute/path -delegate_to: "{{ inventory_hostname }}" + delegate_to: "{{ inventory_hostname }}" # Synchronize and delete files in dest on the remote host that are not found in src of localhost. -synchronize: src=some/relative/path dest=/some/absolute/path delete=yes recursive=yes +- synchronize: + src: some/relative/path + dest: /some/absolute/path + delete: yes + recursive: yes # Synchronize using an alternate rsync command # This specific command is granted su privileges on the destination -synchronize: src=some/relative/path dest=/some/absolute/path rsync_path="su -c rsync" +- synchronize: + src: some/relative/path + dest: /some/absolute/path + rsync_path: "su -c rsync" # Example .rsync-filter file in the source directory - var # exclude any path whose last part is 'var' @@ -246,7 +283,7 @@ + /var/conf # include /var/conf even though it was previously excluded # Synchronize passing in extra rsync options -synchronize: +- synchronize: src: /tmp/helloworld dest: /var/www/helloworld rsync_opts: From 2ae8840c2c95400e6b612ffb25fcc8c2e44e0f3a Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:43:54 -0400 Subject: [PATCH 667/770] Change example syntax on template module --- files/template.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/files/template.py b/files/template.py index 5ee903a778f..c7ff655cdcd 100644 --- a/files/template.py +++ b/files/template.py @@ -25,12 +25,12 @@ (U(http://jinja.pocoo.org/docs/)) - documentation on the template formatting can be found in the Template Designer Documentation (U(http://jinja.pocoo.org/docs/templates/)). - - "Six additional variables can be used in templates: C(ansible_managed) + - "Six additional variables can be used in templates: C(ansible_managed) (configurable via the C(defaults) section of C(ansible.cfg)) contains a string which can be used to describe the template name, host, modification time of the - template file and the owner uid, C(template_host) contains the node name of + template file and the owner uid, C(template_host) contains the node name of the template's machine, C(template_uid) the owner, C(template_path) the - absolute path of the template, C(template_fullpath) is the absolute path of the + absolute path of the template, C(template_fullpath) is the absolute path of the template, and C(template_run_date) is the date that the template was rendered. Note that including a string that uses a date in the template will result in the template being marked 'changed' each time." @@ -77,11 +77,24 @@ EXAMPLES = ''' # Example from Ansible Playbooks -- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode=0644 +- template: + src: /mytemplates/foo.j2 + dest: /etc/file.conf + owner: bin + group: wheel + mode: 0644 # The same example, but using symbolic modes equivalent to 0644 -- template: src=/mytemplates/foo.j2 dest=/etc/file.conf owner=bin group=wheel mode="u=rw,g=r,o=r" +- template: + src: /mytemplates/foo.j2 + dest: /etc/file.conf + owner: bin + group: wheel + mode: "u=rw,g=r,o=r" # Copy a new "sudoers" file into place, after passing validation with visudo -- template: src=/mine/sudoers dest=/etc/sudoers validate='visudo -cf %s' +- template: + src: /mine/sudoers + dest: /etc/sudoers + validate: 'visudo -cf %s' ''' From 80959112b57a7cdb398ce4a5c526d24653d227aa Mon Sep 17 00:00:00 2001 From: John R Barker Date: Mon, 14 Nov 2016 20:12:46 +0000 Subject: [PATCH 668/770] Need to quote if there is a `:` --- database/postgresql/postgresql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index 803751ea4ae..8cc8765c1f8 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -146,7 +146,7 @@ db: acme name: django password: ceec4eif7ya - priv: CONNECT/products:ALL + priv: 'CONNECT/products:ALL' # Create rails user, grant privilege to create other databases and demote rails from super user status - postgresql_user: From 8c51545906ae046bb298cb3c1b6eac6672f800b8 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Mon, 14 Nov 2016 20:13:45 +0000 Subject: [PATCH 669/770] double quotes for consistency --- database/postgresql/postgresql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index 8cc8765c1f8..532e8645327 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -146,7 +146,7 @@ db: acme name: django password: ceec4eif7ya - priv: 'CONNECT/products:ALL' + priv: "CONNECT/products:ALL" # Create rails user, grant privilege to create other databases and demote rails from super user status - postgresql_user: From a4cddac368976f7fd7f97f5af0cf59d7fd032dfd Mon Sep 17 00:00:00 2001 From: Rezart Qelibari Date: Tue, 15 Nov 2016 01:52:25 +0100 Subject: [PATCH 670/770] Update system/group.py module. Add ability to add system groups with next free system gid (< 500) on macOS. --- system/group.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/system/group.py b/system/group.py index f6628727808..efff0f2e3dd 100644 --- a/system/group.py +++ b/system/group.py @@ -269,6 +269,11 @@ def group_add(self, **kwargs): cmd += [ '-o', 'create' ] if self.gid is not None: cmd += [ '-i', self.gid ] + elif 'system' in kwargs and kwargs['system'] == True: + gid = self.get_lowest_available_system_gid() + if gid != False: + self.gid = str(gid) + cmd += [ '-i', self.gid ] cmd += [ '-L', self.name ] (rc, out, err) = self.execute_command(cmd) return (rc, out, err) @@ -291,6 +296,26 @@ def group_mod(self, gid=None): (rc, out, err) = self.execute_command(cmd) return (rc, out, err) return (None, '', '') + + def get_lowest_available_system_gid(self): + # check for lowest available system gid (< 500) + try: + cmd = [self.module.get_bin_path('dscl', True)] + cmd += [ '/Local/Default', '-list', '/Groups', 'PrimaryGroupID'] + (rc, out, err) = self.execute_command(cmd) + lines = out.splitlines() + highest = 0 + for group_info in lines: + parts = group_info.split(' ') + if len(parts) > 1: + gid = int(parts[-1]) + if gid > highest and gid < 500: + highest = gid + if highest == 0 or highest == 499: + return False + return (highest + 1) + except: + return False class OpenBsdGroup(Group): """ From c2db9fa4148b6ba62ec7bc6b991b28946ea173a5 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:44:47 -0400 Subject: [PATCH 671/770] Change example syntax on unarchive module --- files/unarchive.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/files/unarchive.py b/files/unarchive.py index b51d3979c66..3a69cc33b67 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -114,13 +114,21 @@ EXAMPLES = ''' # Example from Ansible Playbooks -- unarchive: src=foo.tgz dest=/var/lib/foo +- unarchive: + src: foo.tgz + dest: /var/lib/foo # Unarchive a file that is already on the remote machine -- unarchive: src=/tmp/foo.zip dest=/usr/local/bin remote_src=yes +- unarchive: + src: /tmp/foo.zip + dest: /usr/local/bin + remote_src: yes # Unarchive a file that needs to be downloaded (added in 2.0) -- unarchive: src=https://example.com/example.zip dest=/usr/local/bin remote_src=yes +- unarchive: + src: "https://example.com/example.zip" + dest: /usr/local/bin + remote_src: yes ''' import re @@ -339,7 +347,7 @@ def is_unarchived(self): # Check first and seventh field in order to skip header/footer if len(pcs[0]) != 7 and len(pcs[0]) != 10: continue if len(pcs[6]) != 15: continue - + # Possible entries: # -rw-rws--- 1.9 unx 2802 t- defX 11-Aug-91 13:48 perms.2660 # -rw-a-- 1.0 hpf 5358 Tl i4:3 4-Dec-91 11:33 longfilename.hpfs From b5f09cb9b00661fbe73748298e2ac7e7c65e1a04 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:45:22 -0400 Subject: [PATCH 672/770] Change example syntax on xattr module --- files/xattr.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/files/xattr.py b/files/xattr.py index 8f12c853a28..02af9f974ff 100644 --- a/files/xattr.py +++ b/files/xattr.py @@ -63,13 +63,20 @@ EXAMPLES = ''' # Obtain the extended attributes of /etc/foo.conf -- xattr: name=/etc/foo.conf +- xattr: + name: /etc/foo.conf # Sets the key 'foo' to value 'bar' -- xattr: path=/etc/foo.conf key=user.foo value=bar +- xattr: + path: /etc/foo.conf + key: user.foo + value: bar # Removes the key 'foo' -- xattr: name=/etc/foo.conf key=user.foo state=absent +- xattr: + name: /etc/foo.conf + key: user.foo + state: absent ''' import operator From 0aeee0e7c23637fc6530b958b72c2a2e91a7423d Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:53:07 -0400 Subject: [PATCH 673/770] Change example syntax on add_host module --- inventory/add_host.py | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/inventory/add_host.py b/inventory/add_host.py index d2873e28f85..91363121f5c 100644 --- a/inventory/add_host.py +++ b/inventory/add_host.py @@ -44,18 +44,24 @@ EXAMPLES = ''' # add host to group 'just_created' with variable foo=42 -- add_host: name={{ ip_from_ec2 }} groups=just_created foo=42 +- add_host: + name: "{{ ip_from_ec2 }}" + groups: just_created + foo: 42 # add a host with a non-standard port local to your machines -- add_host: name={{ new_ip }}:{{ new_port }} +- add_host: + name: "{{ new_ip }}:{{ new_port }}" # add a host alias that we reach through a tunnel (Ansible <= 1.9) -- add_host: hostname={{ new_ip }} - ansible_ssh_host={{ inventory_hostname }} - ansible_ssh_port={{ new_port }} +- add_host: + hostname: "{{ new_ip }}" + ansible_ssh_host: "{{ inventory_hostname }}" + ansible_ssh_port: "{{ new_port }}" # add a host alias that we reach through a tunnel (Ansible >= 2.0) -- add_host: hostname={{ new_ip }} - ansible_host={{ inventory_hostname }} - ansible_port={{ new_port }} +- add_host: + hostname: "{{ new_ip }}" + ansible_host: "{{ inventory_hostname }}" + ansible_port: "{{ new_port }}" ''' From e5446db4b2137822e8ebba38c1e60744a5fc8c86 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:54:04 -0400 Subject: [PATCH 674/770] Change example syntax on group_by module --- inventory/group_by.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/inventory/group_by.py b/inventory/group_by.py index 4bfd20206be..28e9a41ebc8 100644 --- a/inventory/group_by.py +++ b/inventory/group_by.py @@ -34,7 +34,10 @@ EXAMPLES = ''' # Create groups based on the machine architecture -- group_by: key=machine_{{ ansible_machine }} +- group_by: + key: machine_{{ ansible_machine }} + # Create groups like 'kvm-host' -- group_by: key=virt_{{ ansible_virtualization_type }}_{{ ansible_virtualization_role }} +- group_by: + key: virt_{{ ansible_virtualization_type }}_{{ ansible_virtualization_role }} ''' From 21f6aa076c9fe2ce2efe367ab848dad8bbcbd32b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:55:51 -0400 Subject: [PATCH 675/770] Change example syntax on apt module --- packaging/os/apt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 20306cd6deb..5d20dbee86b 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -177,7 +177,7 @@ apt: upgrade: dist update_cache: yes - dpkg_options: force-confold,force-confdef + dpkg_options: 'force-confold,force-confdef' - name: Install a .deb package apt: From 5995bf27bab36f25d4179401be436a30f02adaf9 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:57:00 -0400 Subject: [PATCH 676/770] Change example syntax on apt_key module --- packaging/os/apt_key.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index 516255e1a41..753fb830e11 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -83,28 +83,47 @@ EXAMPLES = ''' # Add an apt key by id from a keyserver -- apt_key: keyserver=keyserver.ubuntu.com id=36A1D7869245C8950F966E92D8576A8BA88D21E9 +- apt_key: + keyserver: keyserver.ubuntu.com + id: 36A1D7869245C8950F966E92D8576A8BA88D21E9 # Add an Apt signing key, uses whichever key is at the URL -- apt_key: url=https://ftp-master.debian.org/keys/archive-key-6.0.asc state=present +- apt_key: + url: "https://ftp-master.debian.org/keys/archive-key-6.0.asc" + state: present # Add an Apt signing key, will not download if present -- apt_key: id=473041FA url=https://ftp-master.debian.org/keys/archive-key-6.0.asc state=present +- apt_key: + id: 473041FA + url: "https://ftp-master.debian.org/keys/archive-key-6.0.asc" + state: present # Remove an Apt signing key, uses whichever key is at the URL -- apt_key: url=https://ftp-master.debian.org/keys/archive-key-6.0.asc state=absent +- apt_key: + url: "https://ftp-master.debian.org/keys/archive-key-6.0.asc" + state: absent # Remove a Apt specific signing key, leading 0x is valid -- apt_key: id=0x473041FA state=absent +- apt_key: + id: 0x473041FA + state: absent # Add a key from a file on the Ansible server -- apt_key: data="{{ lookup('file', 'apt.gpg') }}" state=present +- apt_key: + data: "{{ lookup('file', 'apt.gpg') }}" + state: present # Add an Apt signing key to a specific keyring file -- apt_key: id=473041FA url=https://ftp-master.debian.org/keys/archive-key-6.0.asc keyring=/etc/apt/trusted.gpg.d/debian.gpg state=present +- apt_key: + id: 473041FA + url: "https://ftp-master.debian.org/keys/archive-key-6.0.asc" + keyring: /etc/apt/trusted.gpg.d/debian.gpg # Add Apt signing key on remote server to keyring -- apt_key: id=473041FA file=/tmp/apt.gpg state=present +- apt_key: + id: 473041FA + file: /tmp/apt.gpg + state: present ''' From aa18bf8933e58b16a81b53bd694a0a156eaf82c1 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:58:03 -0400 Subject: [PATCH 677/770] Change example syntax on apt_repository module --- packaging/os/apt_repository.py | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index dac098dab27..aedf1cb2fba 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -84,22 +84,36 @@ EXAMPLES = ''' # Add specified repository into sources list. -apt_repository: repo='deb http://archive.canonical.com/ubuntu hardy partner' state=present +- apt_repository: + repo: 'deb http://archive.canonical.com/ubuntu hardy partner' + state: present # Add specified repository into sources list using specified filename. -apt_repository: repo='deb http://dl.google.com/linux/chrome/deb/ stable main' state=present filename='google-chrome' +- apt_repository: + repo: 'deb http://dl.google.com/linux/chrome/deb/ stable main' + state: present + filename: 'google-chrome' # Add source repository into sources list. -apt_repository: repo='deb-src http://archive.canonical.com/ubuntu hardy partner' state=present +- apt_repository: + repo: 'deb-src http://archive.canonical.com/ubuntu hardy partner' + state: present # Remove specified repository from sources list. -apt_repository: repo='deb http://archive.canonical.com/ubuntu hardy partner' state=absent +- apt_repository: + repo: 'deb http://archive.canonical.com/ubuntu hardy partner' + state: absent # Add nginx stable repository from PPA and install its signing key. # On Ubuntu target: -apt_repository: repo='ppa:nginx/stable' +- apt_repository: + repo: 'ppa:nginx/stable' + # On Debian target -apt_repository: repo='ppa:nginx/stable' codename='trusty' +- apt_repository: + repo: 'ppa:nginx/stable' + codename: 'trusty' + repo: 'ppa:nginx/stable' ''' import glob From 3f224de12aee6222a88c32700f1e86fac0a9d857 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 17:59:13 -0400 Subject: [PATCH 678/770] Change example syntax on apt_rpm module --- packaging/os/apt_rpm.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/packaging/os/apt_rpm.py b/packaging/os/apt_rpm.py index 59eccfee6ee..e8a702e9a09 100755 --- a/packaging/os/apt_rpm.py +++ b/packaging/os/apt_rpm.py @@ -50,13 +50,25 @@ EXAMPLES = ''' # install package foo -- apt_rpm: pkg=foo state=present +- apt_rpm: + pkg: foo + state: present + # remove package foo -- apt_rpm: pkg=foo state=absent -# description: remove packages foo and bar -- apt_rpm: pkg=foo,bar state=absent -# description: update the package database and install bar (bar will be the updated if a newer version exists) -- apt_rpm: name=bar state=present update_cache=yes +- apt_rpm: + pkg: foo + state: absent + +# description: remove packages foo and bar +- apt_rpm: + pkg: foo,bar + state: absent + +# description: update the package database and install bar (bar will be the updated if a newer version exists) +- apt_rpm: + name: bar + state: present + update_cache: yes ''' From 34fb8c3ec285078ecf68e011331958281ee9142f Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 18:00:10 -0400 Subject: [PATCH 679/770] Change example syntax on package module --- packaging/os/package.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packaging/os/package.py b/packaging/os/package.py index 2ae7c7fbc8e..d40ed3d4eee 100644 --- a/packaging/os/package.py +++ b/packaging/os/package.py @@ -53,9 +53,13 @@ ''' EXAMPLES = ''' - name: install the latest version of ntpdate - package: name=ntpdate state=latest + package: + name: ntpdate + state: latest # This uses a variable as this changes per distribution. - name: remove the apache package - package: name={{apache}} state=absent + package: + name: "{{ apache }}" + state: absent ''' From 8533529c2bd79e8d30e5528c0bd5e7d8ab52d33b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 18:03:19 -0400 Subject: [PATCH 680/770] Change example syntax on redhat_subscription module --- packaging/os/redhat_subscription.py | 37 +++++++++++++++++++---------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/packaging/os/redhat_subscription.py b/packaging/os/redhat_subscription.py index 2bef4cbb56c..ea56ac55100 100644 --- a/packaging/os/redhat_subscription.py +++ b/packaging/os/redhat_subscription.py @@ -114,30 +114,41 @@ EXAMPLES = ''' # Register as user (joe_user) with password (somepass) and auto-subscribe to available content. -- redhat_subscription: state=present username=joe_user password=somepass autosubscribe=true +- redhat_subscription: + state: present + username: joe_user + password: somepass + autosubscribe: true # Same as above but with pulling existing system data. -- redhat_subscription: state=present username=joe_user password=somepass - consumer_id=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +- redhat_subscription: + state: present + username: joe_user + password: somepass + consumer_id: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx # Register with activationkey (1-222333444) and consume subscriptions matching # the names (Red hat Enterprise Server) and (Red Hat Virtualization) -- redhat_subscription: state=present - activationkey=1-222333444 - pool='^(Red Hat Enterprise Server|Red Hat Virtualization)$' +- redhat_subscription: + state: present + activationkey: 1-222333444 + pool: '^(Red Hat Enterprise Server|Red Hat Virtualization)$' # Update the consumed subscriptions from the previous example (remove the Red # Hat Virtualization subscription) -- redhat_subscription: state=present - activationkey=1-222333444 - pool='^Red Hat Enterprise Server$' +- redhat_subscription: + state: present + activationkey: 1-222333444 + pool: '^Red Hat Enterprise Server$' # Register as user credentials into given environment (against Red Hat # Satellite 6.x), and auto-subscribe to available content. -- redhat_subscription: state=present - username=joe_user password=somepass - environment=Library - autosubscribe=true +- redhat_subscription: + state: present + username: joe_user + password: somepass + environment: Library + autosubscribe: yes ''' import os From db5076cd8a0013cf4ec7cda211e845213ea7475a Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 18:04:15 -0400 Subject: [PATCH 681/770] Change example syntax on rhn_subscription module --- packaging/os/rhn_channel.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packaging/os/rhn_channel.py b/packaging/os/rhn_channel.py index 0071183158e..9ec24483ef7 100644 --- a/packaging/os/rhn_channel.py +++ b/packaging/os/rhn_channel.py @@ -26,7 +26,7 @@ version_added: "1.1" author: "Vincent Van der Kussen (@vincentvdk)" notes: - - this module fetches the system id from RHN. + - this module fetches the system id from RHN. requirements: - none options: @@ -46,7 +46,7 @@ required: false default: present url: - description: + description: - The full url to the RHN/Satellite api required: true user: @@ -60,7 +60,12 @@ ''' EXAMPLES = ''' -- rhn_channel: name=rhel-x86_64-server-v2vwin-6 sysname=server01 url=https://rhn.redhat.com/rpc/api user=rhnuser password=guessme +- rhn_channel: + name: rhel-x86_64-server-v2vwin-6 + sysname: server01 + url: 'https://rhn.redhat.com/rpc/api' + user: rhnuser + password: guessme ''' import xmlrpclib From f6025edad254f768f69b52a16e1c764200a2e58b Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 18:05:56 -0400 Subject: [PATCH 682/770] Change example syntax on rhn_register module --- packaging/os/rhn_register.py | 38 ++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index f30cf09084d..8908e44857b 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -83,30 +83,44 @@ EXAMPLES = ''' # Unregister system from RHN. -- rhn_register: state=absent username=joe_user password=somepass +- rhn_register: + state: absent + username: joe_user + password: somepass # Register as user (joe_user) with password (somepass) and auto-subscribe to available content. -- rhn_register: state=present username=joe_user password=somepass +- rhn_register: + state: present + username: joe_user + password: somepass # Register with activationkey (1-222333444) and enable extended update support. -- rhn_register: state=present activationkey=1-222333444 enable_eus=true +- rhn_register: + state: present + activationkey: 1-222333444 + enable_eus: true # Register with activationkey (1-222333444) and set a profilename which may differ from the hostname. -- rhn_register: state=present activationkey=1-222333444 profilename=host.example.com.custom +- rhn_register: + state: present + activationkey: 1-222333444 + profilename: host.example.com.custom # Register as user (joe_user) with password (somepass) against a satellite # server specified by (server_url). -- rhn_register: > - state=present - username=joe_user - password=somepass - server_url=https://xmlrpc.my.satellite/XMLRPC +- rhn_register: + state: present + username: joe_user + password: somepass' + server_url: 'https://xmlrpc.my.satellite/XMLRPC' # Register as user (joe_user) with password (somepass) and enable # channels (rhel-x86_64-server-6-foo-1) and (rhel-x86_64-server-6-bar-1). -- rhn_register: state=present username=joe_user - password=somepass - channels=rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1 +- rhn_register: + state: present + username: joe_user + password: somepass + channels: rhel-x86_64-server-6-foo-1,rhel-x86_64-server-6-bar-1 ''' import sys From 68c78cf40dfe19bccf89fdc6c3896ef1d84edb93 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 18:06:26 -0400 Subject: [PATCH 683/770] Change example syntax on rpm_key module --- packaging/os/rpm_key.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packaging/os/rpm_key.py b/packaging/os/rpm_key.py index 4899d5cddfd..19f364192f7 100644 --- a/packaging/os/rpm_key.py +++ b/packaging/os/rpm_key.py @@ -52,13 +52,19 @@ EXAMPLES = ''' # Example action to import a key from a url -- rpm_key: state=present key=http://apt.sw.be/RPM-GPG-KEY.dag.txt +- rpm_key: + state: present + key: 'http://apt.sw.be/RPM-GPG-KEY.dag.txt' # Example action to import a key from a file -- rpm_key: state=present key=/path/to/key.gpg +- rpm_key: + state: present + key: /path/to/key.gpg # Example action to ensure a key is not present in the db -- rpm_key: state=absent key=DEADB33F +- rpm_key: + state: absent + key: DEADB33F ''' import re import os.path From 7363fd198bd73ad5b7ed6a7fd6ca03b33bb6ef83 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 12 Oct 2016 18:07:26 -0400 Subject: [PATCH 684/770] Change example syntax on yum module --- packaging/os/yum.py | 37 ++++++++++++++++++++++++++++--------- 1 file changed, 28 insertions(+), 9 deletions(-) diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 7356880f45d..13cc4d612d5 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -156,31 +156,50 @@ EXAMPLES = ''' - name: install the latest version of Apache - yum: name=httpd state=latest + yum: + name: httpd + state: latest - name: remove the Apache package - yum: name=httpd state=absent + yum: + name: httpd + state: absent - name: install the latest version of Apache from the testing repo - yum: name=httpd enablerepo=testing state=present + yum: + name: httpd + enablerepo: testing + state: present - name: install one specific version of Apache - yum: name=httpd-2.2.29-1.4.amzn1 state=present + yum: + name: httpd-2.2.29-1.4.amzn1 + state: present - name: upgrade all packages - yum: name=* state=latest + yum: + name: '*' + state: latest - name: install the nginx rpm from a remote repo - yum: name=http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present + yum: + name: 'http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm' + state: present - name: install nginx rpm from a local file - yum: name=/usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm state=present + yum: + name: /usr/local/src/nginx-release-centos-6-0.el6.ngx.noarch.rpm + state: present - name: install the 'Development tools' package group - yum: name="@Development tools" state=present + yum: + name: "@Development tools" + state: present - name: install the 'Gnome desktop' environment group - yum: name="@^gnome-desktop-environment" state=present + yum: + name: "@^gnome-desktop-environment" + state: present ''' # 64k. Number of bytes to read at a time when manually downloading pkgs via a url From 94392dd87aac71465b28a54c18ef9b4f0163b856 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Oct 2016 09:35:10 -0400 Subject: [PATCH 685/770] Change example syntax on easy_install module --- packaging/language/easy_install.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packaging/language/easy_install.py b/packaging/language/easy_install.py index 017f6b818a6..96d21ea8f35 100644 --- a/packaging/language/easy_install.py +++ b/packaging/language/easy_install.py @@ -90,10 +90,14 @@ EXAMPLES = ''' # Examples from Ansible Playbooks -- easy_install: name=pip state=latest +- easy_install: + name: pip + state: latest # Install Bottle into the specified virtualenv. -- easy_install: name=bottle virtualenv=/webapps/myapp/venv +- easy_install: + name: bottle + virtualenv: /webapps/myapp/venv ''' def _is_package_installed(module, name, easy_install, executable_arguments): From 58eb966613e35dedaa9acb1a56b58985de178f9d Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Oct 2016 09:35:37 -0400 Subject: [PATCH 686/770] Change example syntax on gem module --- packaging/language/gem.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/packaging/language/gem.py b/packaging/language/gem.py index acd088dc0c5..188841593e7 100644 --- a/packaging/language/gem.py +++ b/packaging/language/gem.py @@ -97,13 +97,21 @@ EXAMPLES = ''' # Installs version 1.0 of vagrant. -- gem: name=vagrant version=1.0 state=present +- gem: + name: vagrant + version: 1.0 + state: present # Installs latest available version of rake. -- gem: name=rake state=latest +- gem: + name: rake + state: latest # Installs rake version 1.0 from a local gem on disk. -- gem: name=rake gem_source=/path/to/gems/rake-1.0.gem state=present +- gem: + name: rake + gem_source: /path/to/gems/rake-1.0.gem + state: present ''' import re From 8fdf02d46842e155645bb6b86458e8e92f436189 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Oct 2016 09:36:56 -0400 Subject: [PATCH 687/770] Change example syntax on pip module --- packaging/language/pip.py | 55 +++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 14 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index 7c8aa8cb78e..e49e275036a 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -142,46 +142,73 @@ EXAMPLES = ''' # Install (Bottle) python package. -- pip: name=bottle +- pip: + name: bottle # Install (Bottle) python package on version 0.11. -- pip: name=bottle version=0.11 +- pip: + name: bottle + version: 0.11 # Install (MyApp) using one of the remote protocols (bzr+,hg+,git+,svn+). You do not have to supply '-e' option in extra_args. -- pip: name='svn+http://myrepo/svn/MyApp#egg=MyApp' +- pip: + name: 'svn+http://myrepo/svn/MyApp#' + egg: MyApp' # Install MyApp using one of the remote protocols (bzr+,hg+,git+) in a non editable way. -- pip: name='git+http://myrepo/app/MyApp' editable=false +- pip: + name: 'git+http://myrepo/app/MyApp' + editable: false # Install (MyApp) from local tarball -- pip: name='file:///path/to/MyApp.tar.gz' +- pip: + name: 'file:///path/to/MyApp.tar.gz' # Install (Bottle) into the specified (virtualenv), inheriting none of the globally installed modules -- pip: name=bottle virtualenv=/my_app/venv +- pip: + name: bottle + virtualenv: /my_app/venv # Install (Bottle) into the specified (virtualenv), inheriting globally installed modules -- pip: name=bottle virtualenv=/my_app/venv virtualenv_site_packages=yes +- pip: + name: bottle + virtualenv: /my_app/venv + virtualenv_site_packages: yes # Install (Bottle) into the specified (virtualenv), using Python 2.7 -- pip: name=bottle virtualenv=/my_app/venv virtualenv_command=virtualenv-2.7 +- pip: + name: bottle + virtualenv: /my_app/venv + virtualenv_command: virtualenv-2.7 # Install specified python requirements. -- pip: requirements=/my_app/requirements.txt +- pip: + requirements: /my_app/requirements.txt # Install specified python requirements in indicated (virtualenv). -- pip: requirements=/my_app/requirements.txt virtualenv=/my_app/venv +- pip: + requirements: /my_app/requirements.txt + virtualenv: /my_app/venv # Install specified python requirements and custom Index URL. -- pip: requirements=/my_app/requirements.txt extra_args='-i https://example.com/pypi/simple' +- pip: + requirements: /my_app/requirements.txt + extra_args: '-i https://example.com/pypi/simple' # Install (Bottle) for Python 3.3 specifically,using the 'pip-3.3' executable. -- pip: name=bottle executable=pip-3.3 +- pip: + name: bottle + executable: pip-3.3 # Install (Bottle), forcing reinstallation if it's already installed -- pip: name=bottle state=forcereinstall +- pip: + name: bottle + state: forcereinstall # Install (Bottle) while ensuring the umask is 0022 (to ensure other users can use it) -- pip: name=bottle umask=0022 +- pip: + name: bottle + umask: 0022 become: True ''' From c0dadd7535b9988ed4d433a29787b1db6202cfba Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Oct 2016 09:39:15 -0400 Subject: [PATCH 688/770] Change example syntax on git module --- source_control/git.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index 06965f03a98..a7cb4ae65c6 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -194,22 +194,35 @@ EXAMPLES = ''' # Example git checkout from Ansible Playbooks -- git: repo=git://foosball.example.org/path/to/repo.git - dest=/srv/checkout - version=release-0.22 +- git: + repo: 'git://foosball.example.org/path/to/repo.git' + dest: /srv/checkout + version: release-0.22 # Example read-write git checkout from github -- git: repo=ssh://git@github.com/mylogin/hello.git dest=/home/mylogin/hello +- git: + repo: 'ssh://git@github.com/mylogin/hello.git' + dest: /home/mylogin/hello # Example just ensuring the repo checkout exists -- git: repo=git://foosball.example.org/path/to/repo.git dest=/srv/checkout update=no +- git: + repo: 'git://foosball.example.org/path/to/repo.git' + dest: /srv/checkout + update: no # Example just get information about the repository whether or not it has # already been cloned locally. -- git: repo=git://foosball.example.org/path/to/repo.git dest=/srv/checkout clone=no update=no +- git: + repo: 'git://foosball.example.org/path/to/repo.git' + dest: /srv/checkout + clone: no + update: no # Example checkout a github repo and use refspec to fetch all pull requests -- git: repo=https://github.com/ansible/ansible-examples.git dest=/src/ansible-examples refspec=+refs/pull/*:refs/heads/* +- git: + repo: 'https://github.com/ansible/ansible-examples.git' + dest: /src/ansible-examples + refspec: '+refs/pull/*:refs/heads/*' ''' import os From 4f3bf3afa5759a2ea86e2789063ffed65a4ba1bc Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Oct 2016 09:40:34 -0400 Subject: [PATCH 689/770] Change example syntax on hg module --- source_control/hg.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/source_control/hg.py b/source_control/hg.py index e0e50e2c795..effc8a7c8c9 100644 --- a/source_control/hg.py +++ b/source_control/hg.py @@ -95,11 +95,19 @@ EXAMPLES = ''' # Ensure the current working copy is inside the stable branch and deletes untracked files if any. -- hg: repo=https://bitbucket.org/user/repo1 dest=/home/user/repo1 revision=stable purge=yes +- hg: + repo: 'https://bitbucket.org/user/repo1' + dest: /home/user/repo1 + revision: stable + purge: yes # Example just get information about the repository whether or not it has # already been cloned locally. -- hg: repo=git://bitbucket.org/user/repo dest=/srv/checkout clone=no update=no +- hg: + repo: 'git://bitbucket.org/user/repo' + dest: /srv/checkout + clone: no + update: no ''' import os From 955b3f573307e0d6be6c8defd90194f24abcee3d Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Thu, 13 Oct 2016 09:41:12 -0400 Subject: [PATCH 690/770] Change example syntax on subversion module --- source_control/subversion.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/source_control/subversion.py b/source_control/subversion.py index 449cd3cdbe1..49a0e22fd77 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -104,14 +104,22 @@ EXAMPLES = ''' # Checkout subversion repository to specified folder. -- subversion: repo=svn+ssh://an.example.org/path/to/repo dest=/src/checkout +- subversion: + repo: 'svn+ssh://an.example.org/path/to/repo' + dest: /src/checkout # Export subversion directory to folder -- subversion: repo=svn+ssh://an.example.org/path/to/repo dest=/src/export export=True +- subversion: + repo: 'svn+ssh://an.example.org/path/to/repo' + dest: /src/export # Example just get information about the repository whether or not it has # already been cloned locally. -- subversion: repo=svn+ssh://an.example.org/path/to/repo dest=/srv/checkout checkout=no update=no +- subversion: + repo: 'svn+ssh://an.example.org/path/to/repo' + dest: /srv/checkout + checkout: no + update: no ''' import re From 05e866e4d01e3b9c2893e0a3dbb053cb7311dcd1 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 15 Nov 2016 15:21:47 -0500 Subject: [PATCH 691/770] Examples syntax batch4 (#5620) * Change example syntax on authorized_key module * Change example syntax on cron module * Change example syntax on group module * Change example syntax on hostname module * Change example syntax on seboolean module * Change example syntax on selinux module * Change example syntax on service module * Change example syntax on sysctl module * Change example syntax on systemd module * Change example syntax on user module * Change example syntax on debug module * Change example syntax on fail module * Change example syntax on include module * Change example syntax on include_role module * Change example syntax on include_vars module * Change example syntax on pause module * Change example syntax on wait_for module * Change example syntax on apache2_module module * > Change example syntax on django_manage module * Change example syntax on htpasswd module --- system/authorized_key.py | 12 ++++--- system/cron.py | 47 ++++++++++++++++++++++------ system/group.py | 6 ++-- system/hostname.py | 3 +- system/seboolean.py | 5 ++- system/selinux.py | 16 ++++++++-- system/service.py | 30 +++++++++++++----- system/sysctl.py | 24 ++++++++++---- system/systemd.py | 22 ++++++++++--- system/user.py | 31 ++++++++++++++---- utilities/logic/debug.py | 16 +++++++--- utilities/logic/fail.py | 3 +- utilities/logic/include.py | 17 +++++++--- utilities/logic/include_role.py | 3 +- utilities/logic/include_vars.py | 10 +++--- utilities/logic/pause.py | 6 ++-- utilities/logic/wait_for.py | 38 ++++++++++++++++------ web_infrastructure/apache2_module.py | 8 +++-- web_infrastructure/django_manage.py | 30 ++++++++++++------ web_infrastructure/htpasswd.py | 21 +++++++++++-- 20 files changed, 261 insertions(+), 87 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index 0b15d2fd0a3..6cfd856e9a3 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -99,18 +99,20 @@ # Using github url as key source - authorized_key: user: charlie - key: https://github.com/charlie.keys + key: 'https://github.com/charlie.keys' # Using alternate directory locations: - authorized_key: user: charlie key: "{{ lookup('file', '/home/charlie/.ssh/id_rsa.pub') }}" - path: '/etc/ssh/authorized_keys/charlie' + path: /etc/ssh/authorized_keys/charlie manage_dir: no # Using with_file - name: Set up authorized_keys for the deploy user - authorized_key: user=deploy key="{{ item }}" + authorized_key: + user: deploy + key: "{{ item }}" with_file: - public_keys/doe-jane - public_keys/doe-john @@ -124,13 +126,13 @@ # Using validate_certs: - authorized_key: user: charlie - key: https://github.com/user.keys + key: 'https://github.com/user.keys' validate_certs: no # Set up authorized_keys exclusively with one key - authorized_key: user: root - key: '{{ item }}' + key: "{{ item }}" state: present exclusive: yes with_file: diff --git a/system/cron.py b/system/cron.py index 2ab23ef6e17..9dd5b5c9b3c 100644 --- a/system/cron.py +++ b/system/cron.py @@ -171,32 +171,59 @@ EXAMPLES = ''' # Ensure a job that runs at 2 and 5 exists. # Creates an entry like "0 5,2 * * ls -alh > /dev/null" -- cron: name="check dirs" minute="0" hour="5,2" job="ls -alh > /dev/null" +- cron: + name: "check dirs" + minute: "0" + hour: "5,2" + job: "ls -alh > /dev/null" # Ensure an old job is no longer present. Removes any job that is prefixed # by "#Ansible: an old job" from the crontab -- cron: name="an old job" state=absent +- cron: + name: "an old job" + state: absent # Creates an entry like "@reboot /some/job.sh" -- cron: name="a job for reboot" special_time=reboot job="/some/job.sh" +- cron: + name: "a job for reboot" + special_time: reboot + job: "/some/job.sh" # Creates an entry like "PATH=/opt/bin" on top of crontab -- cron: name=PATH env=yes value=/opt/bin +- cron: + name: PATH + env: yes + value: /opt/bin # Creates an entry like "APP_HOME=/srv/app" and insert it after PATH # declaration -- cron: name=APP_HOME env=yes value=/srv/app insertafter=PATH +- cron: + name: APP_HOME + env: yes + value: /srv/app + insertafter: PATH # Creates a cron file under /etc/cron.d -- cron: name="yum autoupdate" weekday="2" minute=0 hour=12 - user="root" job="YUMINTERACTIVE=0 /usr/sbin/yum-autoupdate" - cron_file=ansible_yum-autoupdate +- cron: + name: yum autoupdate + weekday: 2 + minute: 0 + hour: 12 + user: root + job: "YUMINTERACTIVE: 0 /usr/sbin/yum-autoupdate" + cron_file: ansible_yum-autoupdate # Removes a cron file from under /etc/cron.d -- cron: name="yum autoupdate" cron_file=ansible_yum-autoupdate state=absent +- cron: + name: "yum autoupdate" + cron_file: ansible_yum-autoupdate + state: absent # Removes "APP_HOME" environment variable from crontab -- cron: name=APP_HOME env=yes state=absent +- cron: + name: APP_HOME + env: yes + state: absent ''' import os diff --git a/system/group.py b/system/group.py index efff0f2e3dd..ceffd75369b 100644 --- a/system/group.py +++ b/system/group.py @@ -53,7 +53,9 @@ EXAMPLES = ''' # Example group command from Ansible Playbooks -- group: name=somegroup state=present +- group: + name: somegroup + state: present ''' import grp @@ -144,7 +146,7 @@ class SunOS(Group): This overrides the following methods from the generic class:- - group_add() - """ + """ platform = 'SunOS' distribution = None diff --git a/system/hostname.py b/system/hostname.py index 4c4285f665f..92fd6758c58 100644 --- a/system/hostname.py +++ b/system/hostname.py @@ -40,7 +40,8 @@ ''' EXAMPLES = ''' -- hostname: name=web01 +- hostname: + name: web01 ''' import socket diff --git a/system/seboolean.py b/system/seboolean.py index 4581c1333ef..973b5b2d20b 100644 --- a/system/seboolean.py +++ b/system/seboolean.py @@ -50,7 +50,10 @@ EXAMPLES = ''' # Set (httpd_can_network_connect) flag on and keep it persistent across reboots -- seboolean: name=httpd_can_network_connect state=yes persistent=yes +- seboolean: + name: httpd_can_network_connect + state: yes + persistent: yes ''' try: diff --git a/system/selinux.py b/system/selinux.py index 2debb95a475..2afd47566aa 100644 --- a/system/selinux.py +++ b/system/selinux.py @@ -49,9 +49,19 @@ ''' EXAMPLES = ''' -- selinux: policy=targeted state=enforcing -- selinux: policy=targeted state=permissive -- selinux: state=disabled +# Enable SELinux +- selinux: + policy: targeted + state: enforcing + +# Put SELinux in permissive mode, logging actions that would be blocked. +- selinux: + policy: targeted + state: permissive + +# Disable SELinux +- selinux: + state: disabled ''' import os diff --git a/system/service.py b/system/service.py index c8781b1c912..1da88f0ef34 100644 --- a/system/service.py +++ b/system/service.py @@ -78,25 +78,41 @@ EXAMPLES = ''' # Example action to start service httpd, if not running -- service: name=httpd state=started +- service: + name: httpd + state: started # Example action to stop service httpd, if running -- service: name=httpd state=stopped +- service: + name: httpd + state: stopped # Example action to restart service httpd, in all cases -- service: name=httpd state=restarted +- service: + name: httpd + state: restarted # Example action to reload service httpd, in all cases -- service: name=httpd state=reloaded +- service: + name: httpd + state: reloaded # Example action to enable service httpd, and not touch the running state -- service: name=httpd enabled=yes +- service: + name: httpd + enabled: yes # Example action to start service foo, based on running process /usr/bin/foo -- service: name=foo pattern=/usr/bin/foo state=started +- service: + name: foo + pattern: /usr/bin/foo + state: started # Example action to restart network service for interface eth0 -- service: name=network state=restarted args=eth0 +- service: + name: network + state: restarted + args: eth0 ''' diff --git a/system/sysctl.py b/system/sysctl.py index 3df8e1fef88..9a6787e2a7e 100644 --- a/system/sysctl.py +++ b/system/sysctl.py @@ -76,25 +76,37 @@ EXAMPLES = ''' # Set vm.swappiness to 5 in /etc/sysctl.conf -- sysctl: - name: vm.swappiness +- sysctl: + name: vm.swappiness value: 5 state: present # Remove kernel.panic entry from /etc/sysctl.conf - sysctl: name: kernel.panic - state: absent + state: absent sysctl_file: /etc/sysctl.conf # Set kernel.panic to 3 in /tmp/test_sysctl.conf -- sysctl: name=kernel.panic value=3 sysctl_file=/tmp/test_sysctl.conf reload=no +- sysctl: + name: kernel.panic + value: 3 + sysctl_file: /tmp/test_sysctl.conf + reload: no # Set ip forwarding on in /proc and do not reload the sysctl file -- sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes +- sysctl: + name: net.ipv4.ip_forward + value: 1 + sysctl_set: yes # Set ip forwarding on in /proc and in the sysctl file and reload if necessary -- sysctl: name="net.ipv4.ip_forward" value=1 sysctl_set=yes state=present reload=yes +- sysctl: + name: net.ipv4.ip_forward + value: 1 + sysctl_set: yes + state: present + reload: yes ''' # ============================================================== diff --git a/system/systemd.py b/system/systemd.py index fdaeae64708..4c170916614 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -72,18 +72,32 @@ EXAMPLES = ''' # Example action to start service httpd, if not running -- systemd: state=started name=httpd +- systemd: + state: started + name: httpd + # Example action to stop service cron on debian, if running -- systemd: name=cron state=stopped +- systemd: + name: cron + state: stopped + # Example action to restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes -- systemd: state=restarted daemon_reload=yes name=crond +- systemd: + state: restarted + daemon_reload: yes + name: crond + # Example action to reload service httpd, in all cases -- systemd: name=httpd state=reloaded +- systemd: + name: httpd + state: reloaded + # Example action to enable service httpd and ensure it is not masked - systemd: name: httpd enabled: yes masked: no + # Example action to enable a timer for dnf-automatic - systemd: name: dnf-automatic.timer diff --git a/system/user.py b/system/user.py index c424beea4b5..b67f962058b 100644 --- a/system/user.py +++ b/system/user.py @@ -160,7 +160,7 @@ default: rsa version_added: "0.9" description: - - Optionally specify the type of SSH key to generate. + - Optionally specify the type of SSH key to generate. Available SSH key types will depend on implementation present on target host. ssh_key_file: @@ -201,19 +201,38 @@ EXAMPLES = ''' # Add the user 'johnd' with a specific uid and a primary group of 'admin' -- user: name=johnd comment="John Doe" uid=1040 group=admin +- user: + name: johnd + comment: "John Doe" + uid: 1040 + group: admin # Add the user 'james' with a bash shell, appending the group 'admins' and 'developers' to the user's groups -- user: name=james shell=/bin/bash groups=admins,developers append=yes +- user: + name: james + shell: /bin/bash + groups: admins,developers + append: yes # Remove the user 'johnd' -- user: name=johnd state=absent remove=yes +- user: + name: johnd + state: absent + remove: yes # Create a 2048-bit SSH key for user jsmith in ~jsmith/.ssh/id_rsa -- user: name=jsmith generate_ssh_key=yes ssh_key_bits=2048 ssh_key_file=.ssh/id_rsa +- user: + name: jsmith + generate_ssh_key: yes + ssh_key_bits: 2048 + ssh_key_file: .ssh/id_rsa # added a consultant whose account you want to expire -- user: name=james18 shell=/bin/zsh groups=developers expires=1422403387 +- user: + name: james18 + shell: /bin/zsh + groups: developers + expires: 1422403387 ''' import os diff --git a/utilities/logic/debug.py b/utilities/logic/debug.py index 89d9254a08e..f28dc538337 100644 --- a/utilities/logic/debug.py +++ b/utilities/logic/debug.py @@ -44,23 +44,29 @@ required: False default: 0 version_added: "2.1" -author: +author: - "Dag Wieers (@dagwieers)" - "Michael DeHaan" ''' EXAMPLES = ''' # Example that prints the loopback address and gateway for each host -- debug: msg="System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}" +- debug: + msg: "System {{ inventory_hostname }} has uuid {{ ansible_product_uuid }}" -- debug: msg="System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}" +- debug: + msg: "System {{ inventory_hostname }} has gateway {{ ansible_default_ipv4.gateway }}" when: ansible_default_ipv4.gateway is defined - shell: /usr/bin/uptime register: result -- debug: var=result verbosity=2 +- debug: + var: result + verbosity: 2 - name: Display all variables/facts known for a host - debug: var=hostvars[inventory_hostname] verbosity=4 + debug: + var: hostvars[inventory_hostname] + verbosity: 4 ''' diff --git a/utilities/logic/fail.py b/utilities/logic/fail.py index 75a7c81d1cf..0957b597184 100644 --- a/utilities/logic/fail.py +++ b/utilities/logic/fail.py @@ -39,6 +39,7 @@ EXAMPLES = ''' # Example playbook using fail and when together -- fail: msg="The system may not be provisioned according to the CMDB status." +- fail: + msg: "The system may not be provisioned according to the CMDB status." when: cmdb_status != "to-be-staged" ''' diff --git a/utilities/logic/include.py b/utilities/logic/include.py index bdca4298bdd..e5c2face7cf 100644 --- a/utilities/logic/include.py +++ b/utilities/logic/include.py @@ -34,7 +34,8 @@ # include a play after another play - hosts: localhost tasks: - - debug: msg="play1" + - debug: + msg: "play1" - include: otherplays.yml @@ -42,15 +43,21 @@ # include task list in play - hosts: all tasks: - - debug: msg=task1 + - debug: + msg: task1 + - include: stuff.yml - - debug: msg=task10 + + - debug: + msg: task10 # dyanmic include task list in play - hosts: all tasks: - - debug: msg=task1 - - include: {{hostvar}}.yml + - debug: + msg: task1 + + - include: "{{ hostvar }}.yml" static: no when: hostvar is defined """ diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 2ddbc62355c..40208764a6f 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -59,7 +59,8 @@ ''' EXAMPLES = """ -- include_role: name=myrole +- include_role: + name: myrole - name: Run tasks/other.yml instead of 'main' include_role: diff --git a/utilities/logic/include_vars.py b/utilities/logic/include_vars.py index 87747bb7e3b..571ddf58e9f 100644 --- a/utilities/logic/include_vars.py +++ b/utilities/logic/include_vars.py @@ -61,15 +61,17 @@ name: stuff # Conditionally decide to load in variables into 'plans' when x is 0, otherwise do not. (2.2) -- include_vars: file=contingency_plan.yml name=plans +- include_vars: + file: contingency_plan.yml + name: plans when: x == 0 # Load a variable file based on the OS type, or a default if not found. - include_vars: "{{ item }}" with_first_found: - - "{{ ansible_distribution }}.yml" - - "{{ ansible_os_family }}.yml" - - "default.yml" + - "{{ ansible_distribution }}.yml" + - "{{ ansible_os_family }}.yml" + - "default.yml" # bare include (free-form) - include_vars: myvars.yml diff --git a/utilities/logic/pause.py b/utilities/logic/pause.py index 75d2db1a73a..5290a632230 100644 --- a/utilities/logic/pause.py +++ b/utilities/logic/pause.py @@ -47,11 +47,13 @@ EXAMPLES = ''' # Pause for 5 minutes to build app cache. -- pause: minutes=5 +- pause: + minutes: 5 # Pause until you can verify updates to an application were successful. - pause: # A helpful reminder of what to look out for post-update. -- pause: prompt="Make sure org.foo.FooOverload exception is not present" +- pause: + prompt: "Make sure org.foo.FooOverload exception is not present" ''' diff --git a/utilities/logic/wait_for.py b/utilities/logic/wait_for.py index d57d18d7b92..2cd99c24a1f 100644 --- a/utilities/logic/wait_for.py +++ b/utilities/logic/wait_for.py @@ -123,30 +123,50 @@ EXAMPLES = ''' # wait 300 seconds for port 8000 to become open on the host, don't start checking for 10 seconds -- wait_for: port=8000 delay=10 +- wait_for: + port: 8000 + delay: 10 # wait 300 seconds for port 8000 of any IP to close active connections, don't start checking for 10 seconds -- wait_for: host=0.0.0.0 port=8000 delay=10 state=drained +- wait_for: + host: 0.0.0.0 + port: 8000 + delay: 10 + state: drained # wait 300 seconds for port 8000 of any IP to close active connections, ignoring connections for specified hosts -- wait_for: host=0.0.0.0 port=8000 state=drained exclude_hosts=10.2.1.2,10.2.1.3 +- wait_for: + host: 0.0.0.0 + port: 8000 + state: drained + exclude_hosts: 10.2.1.2,10.2.1.3 # wait until the file /tmp/foo is present before continuing -- wait_for: path=/tmp/foo +- wait_for: + path: /tmp/foo # wait until the string "completed" is in the file /tmp/foo before continuing -- wait_for: path=/tmp/foo search_regex=completed +- wait_for: + path: /tmp/foo + search_regex: completed # wait until the lock file is removed -- wait_for: path=/var/lock/file.lock state=absent +- wait_for: + path: /var/lock/file.lock + state: absent # wait until the process is finished and pid was destroyed -- wait_for: path=/proc/3466/status state=absent +- wait_for: + path: /proc/3466/status + state: absent # wait 300 seconds for port 22 to become open and contain "OpenSSH", don't assume the inventory_hostname is resolvable # and don't start checking for 10 seconds -- local_action: wait_for port=22 host="{{ ansible_ssh_host | default(inventory_hostname) }}" search_regex=OpenSSH delay=10 - +- local_action: wait_for + port: 22 + host: "{{ ansible_ssh_host | default(inventory_hostname) }}" + search_regex: OpenSSH + delay: 10 ''' class TCPConnectionInfo(object): diff --git a/web_infrastructure/apache2_module.py b/web_infrastructure/apache2_module.py index 9f9df923e03..2e5060d3a54 100644 --- a/web_infrastructure/apache2_module.py +++ b/web_infrastructure/apache2_module.py @@ -47,10 +47,14 @@ EXAMPLES = ''' # enables the Apache2 module "wsgi" -- apache2_module: state=present name=wsgi +- apache2_module: + state: present + name: wsgi # disables the Apache2 module "wsgi" -- apache2_module: state=absent name=wsgi +- apache2_module: + state: absent + name: wsgi ''' import re diff --git a/web_infrastructure/django_manage.py b/web_infrastructure/django_manage.py index 3ce815dc582..f334f3989b2 100644 --- a/web_infrastructure/django_manage.py +++ b/web_infrastructure/django_manage.py @@ -99,24 +99,34 @@ EXAMPLES = """ # Run cleanup on the application installed in 'django_dir'. -- django_manage: command=cleanup app_path={{ django_dir }} +- django_manage: + command: cleanup + app_path: "{{ django_dir }}" # Load the initial_data fixture into the application -- django_manage: command=loaddata app_path={{ django_dir }} fixtures={{ initial_data }} +- django_manage: + command: loaddata + app_path: "{{ django_dir }}" + fixtures: "{{ initial_data }}" # Run syncdb on the application -- django_manage: > - command=syncdb - app_path={{ django_dir }} - settings={{ settings_app_name }} - pythonpath={{ settings_dir }} - virtualenv={{ virtualenv_dir }} +- django_manage: + command: syncdb + app_path: "{{ django_dir }}" + settings: "{{ settings_app_name }}" + pythonpath: "{{ settings_dir }}" + virtualenv: "{{ virtualenv_dir }}" # Run the SmokeTest test case from the main app. Useful for testing deploys. -- django_manage: command=test app_path={{ django_dir }} apps=main.SmokeTest +- django_manage: + command: test + app_path: "{{ django_dir }}" + apps: main.SmokeTest # Create an initial superuser. -- django_manage: command="createsuperuser --noinput --username=admin --email=admin@example.com" app_path={{ django_dir }} +- django_manage: + command: "createsuperuser --noinput --username=admin --email=admin@example.com" + app_path: "{{ django_dir }}" """ diff --git a/web_infrastructure/htpasswd.py b/web_infrastructure/htpasswd.py index f1a4e482f3a..84a2cbdd29c 100644 --- a/web_infrastructure/htpasswd.py +++ b/web_infrastructure/htpasswd.py @@ -74,11 +74,26 @@ EXAMPLES = """ # Add a user to a password file and ensure permissions are set -- htpasswd: path=/etc/nginx/passwdfile name=janedoe password=9s36?;fyNp owner=root group=www-data mode=0640 +- htpasswd: + path: /etc/nginx/passwdfile + name: janedoe + password: '9s36?;fyNp' + owner: root + group: www-data + mode: 0640 + # Remove a user from a password file -- htpasswd: path=/etc/apache2/passwdfile name=foobar state=absent +- htpasswd: + path: /etc/apache2/passwdfile + name: foobar + state: absent + # Add a user to a password file suitable for use by libpam-pwdfile -- htpasswd: path=/etc/mail/passwords name=alex password=oedu2eGh crypt_scheme=md5_crypt +- htpasswd: + path: /etc/mail/passwords + name: alex + password: oedu2eGh + crypt_scheme: md5_crypt """ From 52d376c88e3456535e20eb45287392bef379cc70 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 15 Nov 2016 16:00:33 -0500 Subject: [PATCH 692/770] Examples syntax batch5 (#5622) * Change example syntax on supervisorctl module * Change example syntax or _ec2_ami_search module * Change example syntax on cloudformation module * Change example syntax on ec2 module * Change example syntax on ec2_facts module * Change example syntax on ec2_eip module * Change example syntax on rds module * Change example syntax on route53 module * Change example syntax on s3 module * Change example syntax on digital_ocean module * Change example syntax on docker_service module * Change example syntax on cloudformation module * Change example syntax on gc_storage module * Change example syntax on gce module * Change example syntax on gce_mig module * Change example syntax on _glance_image module * Change example syntax on _keystone_user module * Change example syntax on _nova_keypair module * Change example syntax on _quantum_floating module * Change example syntax on _quantum_floating_ip_associate module * Change example syntax on _quantum_network module * Change example syntax on _quantum_router module * Change example syntax on _quantum_router_gateway module * Change example syntax on _quantum_router_interface module * Change example syntax on _quantum_subnet module * SQUASH _quantum_subnet * Add missing quotes --- cloud/amazon/_ec2_ami_search.py | 12 +- cloud/amazon/cloudformation.py | 18 ++- cloud/amazon/ec2.py | 17 ++- cloud/amazon/ec2_eip.py | 57 ++++++-- cloud/amazon/ec2_facts.py | 5 +- cloud/amazon/rds.py | 4 +- cloud/amazon/route53.py | 30 ++-- cloud/amazon/s3.py | 129 ++++++++++++------ cloud/digital_ocean/digital_ocean.py | 8 +- cloud/docker/docker_service.py | 21 ++- cloud/google/gc_storage.py | 63 ++++++--- cloud/google/gce.py | 11 +- cloud/google/gce_mig.py | 19 ++- cloud/openstack/_glance_image.py | 19 +-- cloud/openstack/_keystone_user.py | 24 ++-- cloud/openstack/_nova_keypair.py | 24 ++-- cloud/openstack/_quantum_floating_ip.py | 13 +- .../_quantum_floating_ip_associate.py | 16 +-- cloud/openstack/_quantum_network.py | 29 ++-- cloud/openstack/_quantum_router.py | 13 +- cloud/openstack/_quantum_router_gateway.py | 12 +- cloud/openstack/_quantum_router_interface.py | 16 ++- cloud/openstack/_quantum_subnet.py | 14 +- web_infrastructure/supervisorctl.py | 20 ++- 24 files changed, 406 insertions(+), 188 deletions(-) diff --git a/cloud/amazon/_ec2_ami_search.py b/cloud/amazon/_ec2_ami_search.py index 4e3189f5e70..eae013cc882 100644 --- a/cloud/amazon/_ec2_ami_search.py +++ b/cloud/amazon/_ec2_ami_search.py @@ -74,10 +74,18 @@ connection: local tasks: - name: Get the Ubuntu precise AMI - ec2_ami_search: distro=ubuntu release=precise region=us-west-1 store=instance-store + ec2_ami_search: + distro: ubuntu + release: precise + region: us-west-1 + store: instance-store register: ubuntu_image + - name: Start the EC2 instance - ec2: image={{ ubuntu_image.ami }} instance_type=m1.small key_name=mykey + ec2: + image: "{{ ubuntu_image.ami }}" + instance_type: m1.small + key_name: mykey ''' import csv diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index f5b713e046b..9d6a1b8ba87 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -143,9 +143,11 @@ # Use a template from a URL - name: launch ansible cloudformation example cloudformation: - stack_name="ansible-cloudformation" state=present - region=us-east-1 disable_rollback=true - template_url=https://s3.amazonaws.com/my-bucket/cloudformation.template + stack_name: "ansible-cloudformation" + state: present + region: us-east-1 + disable_rollback: true + template_url: 'https://s3.amazonaws.com/my-bucket/cloudformation.template' args: template_parameters: KeyName: jmartin @@ -158,10 +160,12 @@ # Use a template from a URL, and assume a role to execute - name: launch ansible cloudformation example with role assumption cloudformation: - stack_name="ansible-cloudformation" state=present - region=us-east-1 disable_rollback=true - template_url=https://s3.amazonaws.com/my-bucket/cloudformation.template - role_arn: arn:aws:iam::123456789012:role/cloudformation-iam-role + stack_name: "ansible-cloudformation" + state: present + region: us-east-1 + disable_rollback: true + template_url: 'https://s3.amazonaws.com/my-bucket/cloudformation.template' + role_arn: 'arn:aws:iam::123456789012:role/cloudformation-iam-role' args: template_parameters: KeyName: jmartin diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 78193b18a84..8e17b92009f 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -434,12 +434,21 @@ vpc_subnet_id: subnet-29e63245 assign_public_ip: yes register: ec2 + - name: Add new instance to host group - add_host: hostname={{ item.public_ip }} groupname=launched - with_items: '{{ec2.instances}}' + add_host: + hostname: "{{ item.public_ip }}" + groupname: launched + with_items: "{{ ec2.instances }}" + - name: Wait for SSH to come up - wait_for: host={{ item.public_dns_name }} port=22 delay=60 timeout=320 state=started - with_items: '{{ec2.instances}}' + wait_for: + host: "{{ item.public_dns_name }}" + port: 22 + delay: 60 + timeout: 320 + state: started + with_items: "{{ ec2.instances }}" - name: Configure instance(s) hosts: launched diff --git a/cloud/amazon/ec2_eip.py b/cloud/amazon/ec2_eip.py index 2757a618e65..4fb7c2ae56f 100644 --- a/cloud/amazon/ec2_eip.py +++ b/cloud/amazon/ec2_eip.py @@ -79,34 +79,67 @@ EXAMPLES = ''' - name: associate an elastic IP with an instance - ec2_eip: device_id=i-1212f003 ip=93.184.216.119 + ec2_eip: + device_id: i-1212f003 + ip: 93.184.216.119 + - name: associate an elastic IP with a device - ec2_eip: device_id=eni-c8ad70f3 ip=93.184.216.119 + ec2_eip: + device_id: eni-c8ad70f3 + ip: 93.184.216.119 + - name: disassociate an elastic IP from an instance - ec2_eip: device_id=i-1212f003 ip=93.184.216.119 state=absent + ec2_eip: + device_id: i-1212f003 + ip: 93.184.216.119 + state: absent + - name: disassociate an elastic IP with a device - ec2_eip: device_id=eni-c8ad70f3 ip=93.184.216.119 state=absent + ec2_eip: + device_id: eni-c8ad70f3 + ip: 93.184.216.119 + state: absent + - name: allocate a new elastic IP and associate it with an instance - ec2_eip: device_id=i-1212f003 + ec2_eip: + device_id: i-1212f003 + - name: allocate a new elastic IP without associating it to anything action: ec2_eip register: eip + - name: output the IP - debug: msg="Allocated IP is {{ eip.public_ip }}" + debug: + msg: "Allocated IP is {{ eip.public_ip }}" + - name: another way of allocating an elastic IP without associating it to anything - ec2_eip: state='present' + ec2_eip: + state: 'present' + - name: provision new instances with ec2 - ec2: keypair=mykey instance_type=c1.medium image=ami-40603AD1 wait=yes''' -''' group=webserver count=3 + ec2: + keypair: mykey + instance_type: c1.medium + image: ami-40603AD1 + wait: yes + group: webserver + count: 3 register: ec2 + - name: associate new elastic IPs with each of the instances - ec2_eip: "device_id={{ item }}" + ec2_eip: + device_id: "{{ item }}" with_items: "{{ ec2.instance_ids }}" + - name: allocate a new elastic IP inside a VPC in us-west-2 - ec2_eip: region=us-west-2 in_vpc=yes + ec2_eip: + region: us-west-2 + in_vpc: yes register: eip + - name: output the IP - debug: msg="Allocated IP inside a VPC is {{ eip.public_ip }}" + debug: + msg: "Allocated IP inside a VPC is {{ eip.public_ip }}" ''' try: diff --git a/cloud/amazon/ec2_facts.py b/cloud/amazon/ec2_facts.py index bcda99f16b3..f7fb86cbc90 100644 --- a/cloud/amazon/ec2_facts.py +++ b/cloud/amazon/ec2_facts.py @@ -42,10 +42,11 @@ EXAMPLES = ''' # Conditional example - name: Gather facts - action: ec2_facts + ec2_facts: - name: Conditional - action: debug msg="This instance is a t1.micro" + debug: + msg: "This instance is a t1.micro" when: ansible_ec2_instance_type == "t1.micro" ''' diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py index bde50fce371..5b2e43834f8 100644 --- a/cloud/amazon/rds.py +++ b/cloud/amazon/rds.py @@ -301,9 +301,9 @@ instance_name: MyNewInstanceName region: us-west-2 vpc_security_groups: sg-xxx945xx - -- debug: msg="The new db endpoint is {{ rds.instance.endpoint }}" +- debug: + msg: "The new db endpoint is {{ rds.instance.endpoint }}" ''' import sys diff --git a/cloud/amazon/route53.py b/cloud/amazon/route53.py index 6e8c25377dc..4f08a4f6543 100644 --- a/cloud/amazon/route53.py +++ b/cloud/amazon/route53.py @@ -217,13 +217,13 @@ # Add an alias record that points to an Amazon ELB: - route53: - command=create - zone=foo.com - record=elb.foo.com - type=A - value="{{ elb_dns_name }}" - alias=True - alias_hosted_zone_id="{{ elb_zone_id }}" + command: create + zone: foo.com + record: elb.foo.com + type: A + value: "{{ elb_dns_name }}" + alias: True + alias_hosted_zone_id: "{{ elb_zone_id }}" # Retrieve the details for elb.foo.com - route53: @@ -246,14 +246,14 @@ # Add an alias record that points to an Amazon ELB and evaluates it health: - route53: - command=create - zone=foo.com - record=elb.foo.com - type=A - value="{{ elb_dns_name }}" - alias=True - alias_hosted_zone_id="{{ elb_zone_id }}" - alias_evaluate_target_health=True + command: create + zone: foo.com + record: elb.foo.com + type: A + value: "{{ elb_dns_name }}" + alias: True + alias_hosted_zone_id: "{{ elb_zone_id }}" + alias_evaluate_target_health: True # Add an AAAA record with Hosted Zone ID. Note that because there are colons in the value # that the entire parameter list must be quoted: diff --git a/cloud/amazon/s3.py b/cloud/amazon/s3.py index bb3d0145d42..bfb4627ffa3 100755 --- a/cloud/amazon/s3.py +++ b/cloud/amazon/s3.py @@ -154,44 +154,97 @@ ''' EXAMPLES = ''' -# Simple PUT operation -- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put - -# Simple PUT operation in Ceph RGW S3 -- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put rgw=true s3_url=http://localhost:8000 - -# Simple GET operation -- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get - -# Get a specific version of an object. -- s3: bucket=mybucket object=/my/desired/key.txt version=48c9ee5131af7a716edc22df9772aa6f dest=/usr/local/myfile.txt mode=get - -# PUT/upload with metadata -- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put metadata='Content-Encoding=gzip,Cache-Control=no-cache' - -# PUT/upload with custom headers -- s3: bucket=mybucket object=/my/desired/key.txt src=/usr/local/myfile.txt mode=put headers=x-amz-grant-full-control=emailAddress=owner@example.com - -# List keys simple -- s3: bucket=mybucket mode=list - -# List keys all options -- s3: bucket=mybucket mode=list prefix=/my/desired/ marker=/my/desired/0023.txt max_keys=472 - -# Create an empty bucket -- s3: bucket=mybucket mode=create permission=public-read - -# Create a bucket with key as directory, in the EU region -- s3: bucket=mybucket object=/my/directory/path mode=create region=eu-west-1 - -# Delete a bucket and all contents -- s3: bucket=mybucket mode=delete - -# GET an object but dont download if the file checksums match. New in 2.0 -- s3: bucket=mybucket object=/my/desired/key.txt dest=/usr/local/myfile.txt mode=get overwrite=different - -# Delete an object from a bucket -- s3: bucket=mybucket object=/my/desired/key.txt mode=delobj +- name: Simple PUT operation + s3: + bucket: mybucket + object: /my/desired/key.txt + src: /usr/local/myfile.txt + mode: put + +- name: Simple PUT operation in Ceph RGW S3 + s3: + bucket: mybucket + object: /my/desired/key.txt + src: /usr/local/myfile.txt + mode: put + rgw: true + s3_url: "http://localhost:8000" + +- name: Simple GET operation + s3: + bucket: mybucket + object: /my/desired/key.txt + dest: /usr/local/myfile.txt + mode: get + +- name: Get a specific version of an object. + s3: + bucket: mybucket + object: /my/desired/key.txt + version: 48c9ee5131af7a716edc22df9772aa6f + dest: /usr/local/myfile.txt + mode: get + +- name: PUT/upload with metadata + s3: + bucket: mybucket + object: /my/desired/key.txt + src: /usr/local/myfile.txt + mode: put + metadata: 'Content-Encoding=gzip,Cache-Control=no-cache' + +- name: PUT/upload with custom headers + s3: + bucket: mybucket + object: /my/desired/key.txt + src: /usr/local/myfile.txt + mode: put + headers: 'x-amz-grant-full-control=emailAddress=owner@example.com' + +- name: List keys simple + s3: + bucket: mybucket + mode: list + +- name: List keys all options + s3: + bucket: mybucket + mode: list + prefix: /my/desired/ + marker: /my/desired/0023.txt + max_keys: 472 + +- name: Create an empty bucket + s3: + bucket: mybucket + mode: create + permission: public-read + +- name: Create a bucket with key as directory, in the EU region + s3: + bucket: mybucket + object: /my/directory/path + mode: create + region: eu-west-1 + +- name: Delete a bucket and all contents + s3: + bucket: mybucket + mode: delete + +- name: GET an object but dont download if the file checksums match. New in 2.0 + s3: + bucket: mybucket + object: /my/desired/key.txt + dest: /usr/local/myfile.txt + mode: get + overwrite: different + +- name: Delete an object from a bucket + s3: + bucket: mybucket + object: /my/desired/key.txt + mode: delobj ''' import os diff --git a/cloud/digital_ocean/digital_ocean.py b/cloud/digital_ocean/digital_ocean.py index a90f27f71e3..d2c894cc340 100644 --- a/cloud/digital_ocean/digital_ocean.py +++ b/cloud/digital_ocean/digital_ocean.py @@ -141,11 +141,13 @@ region_id: ams2 image_id: fedora-19-x64 wait_timeout: 500 - register: my_droplet -- debug: msg="ID is {{ my_droplet.droplet.id }}" -- debug: msg="IP is {{ my_droplet.droplet.ip_address }}" +- debug: + msg: "ID is {{ my_droplet.droplet.id }}" + +- debug: + msg: "IP is {{ my_droplet.droplet.ip_address }}" # Ensure a droplet is present # If droplet id already exist, will return the droplet details and changed = False diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index ad231607c09..33a31cf7679 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -190,14 +190,16 @@ project_src: flask register: output - - debug: var=output + - debug: + var: output - docker_service: project_src: flask build: no register: output - - debug: var=output + - debug: + var: output - assert: that: "not output.changed " @@ -208,7 +210,8 @@ stopped: true register: output - - debug: var=output + - debug: + var: output - assert: that: @@ -221,7 +224,8 @@ restarted: true register: output - - debug: var=output + - debug: + var: output - assert: that: @@ -239,7 +243,8 @@ web: 2 register: output - - debug: var=output + - debug: + var: output - name: Run with inline v2 compose hosts: localhost @@ -268,7 +273,8 @@ - db register: output - - debug: var=output + - debug: + var: output - assert: that: @@ -300,7 +306,8 @@ - db register: output - - debug: var=output + - debug: + var: output - assert: that: diff --git a/cloud/google/gc_storage.py b/cloud/google/gc_storage.py index 64222d47d82..a032c63c7f8 100644 --- a/cloud/google/gc_storage.py +++ b/cloud/google/gc_storage.py @@ -89,26 +89,49 @@ ''' EXAMPLES = ''' -# upload some content -- gc_storage: bucket=mybucket object=key.txt src=/usr/local/myfile.txt mode=put permission=public-read - -# upload some headers -- gc_storage: bucket=mybucket object=key.txt src=/usr/local/myfile.txt headers='{"Content-Encoding": "gzip"}' - -# download some content -- gc_storage: bucket=mybucket object=key.txt dest=/usr/local/myfile.txt mode=get - -# Download an object as a string to use else where in your playbook -- gc_storage: bucket=mybucket object=key.txt mode=get_str - -# Create an empty bucket -- gc_storage: bucket=mybucket mode=create - -# Create a bucket with key as directory -- gc_storage: bucket=mybucket object=/my/directory/path mode=create - -# Delete a bucket and all contents -- gc_storage: bucket=mybucket mode=delete +- name: Upload some content + gc_storage: + bucket: mybucket + object: key.txt + src: /usr/local/myfile.txt + mode: put + permission: public-read + +- name: Upload some headers + gc_storage: + bucket: mybucket + object: key.txt + src: /usr/local/myfile.txt + headers: '{"Content-Encoding": "gzip"}' + +- name: Download some content + gc_storage: + bucket: mybucket + object: key.txt + dest: /usr/local/myfile.txt + mode: get + +- name: Download an object as a string to use else where in your playbook + gc_storage: + bucket: mybucket + object: key.txt + mode: get_str + +- name: Create an empty bucket + gc_storage: + bucket: mybucket + mode: create + +- name: Create a bucket with key as directory + gc_storage: + bucket: mybucket + object: /my/directory/path + mode: create + +- name: Delete a bucket and all contents + gc_storage: + bucket: mybucket + mode: delete ''' import os diff --git a/cloud/google/gce.py b/cloud/google/gce.py index 9c18b71f7a7..7bf342bda5d 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -246,11 +246,18 @@ register: gce - name: Save host data - add_host: hostname={{ item.public_ip }} groupname=gce_instances_ips + add_host: + hostname: "{{ item.public_ip }}" + groupname: gce_instances_ips with_items: "{{ gce.instance_data }}" - name: Wait for SSH for instances - wait_for: delay=1 host={{ item.public_ip }} port=22 state=started timeout=30 + wait_for: + delay: 1 + host: "{{ item.public_ip }}" + port: 22 + state: started + timeout: 30 with_items: "{{ gce.instance_data }}" - name: Configure Hosts diff --git a/cloud/google/gce_mig.py b/cloud/google/gce_mig.py index 11433b35b20..37b17de0ba8 100644 --- a/cloud/google/gce_mig.py +++ b/cloud/google/gce_mig.py @@ -116,7 +116,11 @@ port: 80 - name: foobar port: 82 - - pause: seconds=30 + + - name: Pause for 30 seconds + pause: + seconds: 30 + - name: Recreate MIG Instances with Instance Template change. gce_mig: name: ansible-mig-example @@ -124,13 +128,18 @@ state: present template: my-instance-template-2-small recreate_instances: yes - - pause: seconds=30 + + - name: Pause for 30 seconds + pause: + seconds: 30 + - name: Resize MIG gce_mig: name: ansible-mig-example zone: us-central1-c state: present size: 3 + - name: Update MIG with Autoscaler gce_mig: name: ansible-mig-example @@ -150,7 +159,11 @@ target: .39 load_balancing_utilization: target: 0.4 - - pause: seconds=30 + + - name: Pause for 30 seconds + pause: + seconds: 30 + - name: Delete MIG gce_mig: name: ansible-mig-example diff --git a/cloud/openstack/_glance_image.py b/cloud/openstack/_glance_image.py index 0db7aa2c5f0..2d3c9583ffc 100644 --- a/cloud/openstack/_glance_image.py +++ b/cloud/openstack/_glance_image.py @@ -120,15 +120,16 @@ ''' EXAMPLES = ''' -# Upload an image from an HTTP URL -- glance_image: login_username=admin - login_password=passme - login_tenant_name=admin - name=cirros - container_format=bare - disk_format=qcow2 - state=present - copy_from=http:launchpad.net/cirros/trunk/0.3.0/+download/cirros-0.3.0-x86_64-disk.img +- name: Upload an image from an HTTP URL + glance_image: + login_username: admin + login_password: passme + login_tenant_name: admin + name: cirros + container_format: bare + disk_format: qcow2 + state: present + copy_from: 'http://launchpad.net/cirros/trunk/0.3.0/+download/cirros-0.3.0-x86_64-disk.img' ''' import time diff --git a/cloud/openstack/_keystone_user.py b/cloud/openstack/_keystone_user.py index e7be3a1d038..430e8bf40c4 100644 --- a/cloud/openstack/_keystone_user.py +++ b/cloud/openstack/_keystone_user.py @@ -94,14 +94,22 @@ ''' EXAMPLES = ''' -# Create a tenant -- keystone_user: tenant=demo tenant_description="Default Tenant" - -# Create a user -- keystone_user: user=john tenant=demo password=secrete - -# Apply the admin role to the john user in the demo tenant -- keystone_user: role=admin user=john tenant=demo +- name: Create a tenant + keystone_user: + tenant: demo + tenant_description: "Default Tenant" + +- name: Create a user + keystone_user: + user: john + tenant: demo + password: secrete + +- name: Apply the admin role to the john user in the demo tenant + keystone_user: + role: admin + user: john + tenant: demo ''' try: diff --git a/cloud/openstack/_nova_keypair.py b/cloud/openstack/_nova_keypair.py index 330a280fdeb..587bb5dc042 100644 --- a/cloud/openstack/_nova_keypair.py +++ b/cloud/openstack/_nova_keypair.py @@ -29,7 +29,7 @@ --- module: nova_keypair version_added: "1.2" -author: +author: - "Benno Joy (@bennojoy)" - "Michael DeHaan" deprecated: Deprecated in 2.0. Use os_keypair instead @@ -83,14 +83,22 @@ - "python-novaclient" ''' EXAMPLES = ''' -# Creates a key pair with the running users public key -- nova_keypair: state=present login_username=admin - login_password=admin login_tenant_name=admin name=ansible_key - public_key={{ lookup('file','~/.ssh/id_rsa.pub') }} +- name: Create a key pair with the running users public key + nova_keypair: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + name: ansible_key + public_key: "{{ lookup('file','~/.ssh/id_rsa.pub') }}" -# Creates a new key pair and the private key returned after the run. -- nova_keypair: state=present login_username=admin login_password=admin - login_tenant_name=admin name=ansible_key +- name: Create a new key pair and the private key returned after the run. + nova_keypair: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + name: ansible_key ''' def main(): diff --git a/cloud/openstack/_quantum_floating_ip.py b/cloud/openstack/_quantum_floating_ip.py index e6eb267e54b..d3e2c898db5 100644 --- a/cloud/openstack/_quantum_floating_ip.py +++ b/cloud/openstack/_quantum_floating_ip.py @@ -95,10 +95,15 @@ ''' EXAMPLES = ''' -# Assign a floating ip to the instance from an external network -- quantum_floating_ip: state=present login_username=admin login_password=admin - login_tenant_name=admin network_name=external_network - instance_name=vm1 internal_network_name=internal_network +- name: Assign a floating ip to the instance from an external network + quantum_floating_ip: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + network_name: external_network + instance_name: vm1 + internal_network_name: internal_network ''' def _get_ksclient(module, kwargs): diff --git a/cloud/openstack/_quantum_floating_ip_associate.py b/cloud/openstack/_quantum_floating_ip_associate.py index 9085297447e..7c7d7045cb2 100644 --- a/cloud/openstack/_quantum_floating_ip_associate.py +++ b/cloud/openstack/_quantum_floating_ip_associate.py @@ -86,14 +86,14 @@ ''' EXAMPLES = ''' -# Associate a specific floating IP with an Instance -- quantum_floating_ip_associate: - state=present - login_username=admin - login_password=admin - login_tenant_name=admin - ip_address=1.1.1.1 - instance_name=vm1 +- name: Associate a specific floating IP with an Instance + quantum_floating_ip_associate: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + ip_address: 1.1.1.1 + instance_name: vm1 ''' def _get_ksclient(module, kwargs): diff --git a/cloud/openstack/_quantum_network.py b/cloud/openstack/_quantum_network.py index 83c80438f0a..a6cf688ebc1 100644 --- a/cloud/openstack/_quantum_network.py +++ b/cloud/openstack/_quantum_network.py @@ -113,15 +113,26 @@ ''' EXAMPLES = ''' -# Create a GRE backed Quantum network with tunnel id 1 for tenant1 -- quantum_network: name=t1network tenant_name=tenant1 state=present - provider_network_type=gre provider_segmentation_id=1 - login_username=admin login_password=admin login_tenant_name=admin - -# Create an external network -- quantum_network: name=external_network state=present - provider_network_type=local router_external=yes - login_username=admin login_password=admin login_tenant_name=admin +- name: Create a GRE backed Quantum network with tunnel id 1 for tenant1 + quantum_network: + name: t1network + tenant_name: tenant1 + state: present + provider_network_type: gre + provider_segmentation_id: 1 + login_username: admin + login_password: admin + login_tenant_name: admin + +- name: Create an external network + quantum_network: + name: external_network + state: present + provider_network_type: local + router_external: yes + login_username: admin + login_password: admin + login_tenant_name: admin ''' _os_keystone = None diff --git a/cloud/openstack/_quantum_router.py b/cloud/openstack/_quantum_router.py index f1e0ed8c5b7..3a12028d272 100644 --- a/cloud/openstack/_quantum_router.py +++ b/cloud/openstack/_quantum_router.py @@ -88,12 +88,13 @@ ''' EXAMPLES = ''' -# Creates a router for tenant admin -- quantum_router: state=present - login_username=admin - login_password=admin - login_tenant_name=admin - name=router1" +- name: Create a router for tenant admin + quantum_router: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + name: router1 ''' _os_keystone = None diff --git a/cloud/openstack/_quantum_router_gateway.py b/cloud/openstack/_quantum_router_gateway.py index 0e060b50b7e..e71a171707b 100644 --- a/cloud/openstack/_quantum_router_gateway.py +++ b/cloud/openstack/_quantum_router_gateway.py @@ -83,10 +83,14 @@ ''' EXAMPLES = ''' -# Attach an external network with a router to allow flow of external traffic -- quantum_router_gateway: state=present login_username=admin login_password=admin - login_tenant_name=admin router_name=external_router - network_name=external_network +- name: Attach an external network with a router to allow flow of external traffic + quantum_router_gateway: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + router_name: external_router + network_name: external_network ''' _os_keystone = None diff --git a/cloud/openstack/_quantum_router_interface.py b/cloud/openstack/_quantum_router_interface.py index abff97e0045..aba6d0cbac4 100644 --- a/cloud/openstack/_quantum_router_interface.py +++ b/cloud/openstack/_quantum_router_interface.py @@ -88,13 +88,15 @@ ''' EXAMPLES = ''' -# Attach tenant1's subnet to the external router -- quantum_router_interface: state=present login_username=admin - login_password=admin - login_tenant_name=admin - tenant_name=tenant1 - router_name=external_route - subnet_name=t1subnet +- name: "Attach tenant1's subnet to the external router" + quantum_router_interface: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + tenant_name: tenant1 + router_name: external_route + subnet_name: t1subnet ''' diff --git a/cloud/openstack/_quantum_subnet.py b/cloud/openstack/_quantum_subnet.py index 27c2a3e8ea9..8e6dee2c08d 100644 --- a/cloud/openstack/_quantum_subnet.py +++ b/cloud/openstack/_quantum_subnet.py @@ -123,10 +123,16 @@ ''' EXAMPLES = ''' -# Create a subnet for a tenant with the specified subnet -- quantum_subnet: state=present login_username=admin login_password=admin - login_tenant_name=admin tenant_name=tenant1 - network_name=network1 name=net1subnet cidr=192.168.0.0/24" +- name: Create a subnet for a tenant with the specified subnet + quantum_subnet: + state: present + login_username: admin + login_password: admin + login_tenant_name: admin + tenant_name: tenant1 + network_name: network1 + name: net1subnet + cidr: 192.168.0.0/24 ''' _os_keystone = None diff --git a/web_infrastructure/supervisorctl.py b/web_infrastructure/supervisorctl.py index bd6baa98e3b..97cd3a0c637 100644 --- a/web_infrastructure/supervisorctl.py +++ b/web_infrastructure/supervisorctl.py @@ -83,16 +83,28 @@ EXAMPLES = ''' # Manage the state of program to be in 'started' state. -- supervisorctl: name=my_app state=started +- supervisorctl: + name: my_app + state: started # Manage the state of program group to be in 'started' state. -- supervisorctl: name='my_apps:' state=started +- supervisorctl: + name: 'my_apps:' + state: started # Restart my_app, reading supervisorctl configuration from a specified file. -- supervisorctl: name=my_app state=restarted config=/var/opt/my_project/supervisord.conf +- supervisorctl: + name: my_app + state: restarted + config: /var/opt/my_project/supervisord.conf # Restart my_app, connecting to supervisord with credentials and server URL. -- supervisorctl: name=my_app state=restarted username=test password=testpass server_url=http://localhost:9001 +- supervisorctl: + name: my_app + state: restarted + username: test + password: testpass + server_url: 'http://localhost:9001' ''' From cf76fce0ae3a01b456e2a8c5ed4dfe95d2a987da Mon Sep 17 00:00:00 2001 From: Abdul Anshad A Date: Wed, 16 Nov 2016 08:57:49 +0530 Subject: [PATCH 693/770] fixes issue #5517 (#5519) --- cloud/docker/docker_container.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index a1a595ed2b3..4bea65b5e6a 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -1201,6 +1201,12 @@ def has_different_configuration(self, image): # assuming if the container was running, it must have been detached. detach = not (config.get('AttachStderr') and config.get('AttachStdout')) + # "ExposedPorts": null returns None type & causes AttributeError - PR #5517 + if config.get('ExposedPorts') is not None: + expected_exposed = [re.sub(r'/.+$', '', p) for p in config.get('ExposedPorts', dict()).keys()] + else: + expected_exposed = [] + # Map parameters to container inspect results config_mapping = dict( image=config.get('Image'), @@ -1217,7 +1223,7 @@ def has_different_configuration(self, image): expected_env=(config.get('Env') or []), expected_entrypoint=config.get('Entrypoint'), expected_etc_hosts=host_config['ExtraHosts'], - expected_exposed=[re.sub(r'/.+$', '', p) for p in config.get('ExposedPorts', dict()).keys()], + expected_exposed=expected_exposed, groups=host_config.get('GroupAdd'), ipc_mode=host_config.get("IpcMode"), labels=config.get('Labels'), From a0545dc9f83e3d2c3c4a9924460beb76e1c9dba4 Mon Sep 17 00:00:00 2001 From: Luc Charpentier Date: Wed, 16 Nov 2016 11:28:18 +0100 Subject: [PATCH 694/770] error in docker_container example (#5631) --- cloud/docker/docker_container.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index 4bea65b5e6a..d6811e00eab 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -571,7 +571,6 @@ - name: Add container to networks docker_container: - docker_container: name: sleepy networks: - name: TestingNet From bb97885f91573a8d76c307d03cef1a54702ed3c7 Mon Sep 17 00:00:00 2001 From: Jesse Keating Date: Wed, 16 Nov 2016 05:45:16 -0800 Subject: [PATCH 695/770] Do not require password when deleting os_user (#5601) I broke backwards compat with the addition to define when a password should be updated. It was requiring that a password value be passed when deleting a user, which seems silly. This moves the argument logic out of the argument spec and into when it would be needed, when state is present. --- cloud/openstack/os_user.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/cloud/openstack/os_user.py b/cloud/openstack/os_user.py index 264eb3f8e85..831f2fa9dee 100644 --- a/cloud/openstack/os_user.py +++ b/cloud/openstack/os_user.py @@ -191,10 +191,6 @@ def main(): module_kwargs = openstack_module_kwargs() module = AnsibleModule( argument_spec, - required_if=[ - ('update_password', 'always', ['password']), - ('update_password', 'on_create', ['password']), - ], **module_kwargs) if not HAS_SHADE: @@ -219,6 +215,11 @@ def main(): domain_id = _get_domain_id(opcloud, domain) if state == 'present': + if update_password in ('always', 'on_create'): + if not password: + msg = ("update_password is %s but a password value is " + "missing") % update_password + self.fail_json(msg=msg) default_project_id = None if default_project: default_project_id = _get_default_project_id(cloud, default_project) From a257f9cc9c5e481ad7902db53fc46f4001adcbad Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Wed, 16 Nov 2016 12:24:21 -0500 Subject: [PATCH 696/770] Examples syntax batch6 (#5623) * Change example syntax on os_auth module * Change example syntax on os_client_config module * Change example syntax on os_image_facts module * Change example syntax on os_networks_facts module * Change example syntax on os_nova_flavor module * Change example syntax on os_object module * Change example syntax on os_server module * Change example syntax on os_subnet_facts module * Change example syntax on rax_files module * Change example syntax on rax_files_objects module * Change example syntax on mysql_db module * Change example syntax on file module * Change example syntax on uri module * Change example syntax on cl_bond module * Change example syntax on cl_bridge module * Change example syntax on cl_img_install module * Change example syntax on cl_interface module * Change example syntax on cl_license module * Change example syntax on cl_ports module * Remove trailing colon --- cloud/openstack/os_auth.py | 9 +- cloud/openstack/os_client_config.py | 16 +- cloud/openstack/os_image_facts.py | 11 +- cloud/openstack/os_networks_facts.py | 35 +++-- cloud/openstack/os_nova_flavor.py | 31 ++-- cloud/openstack/os_object.py | 18 ++- cloud/openstack/os_server.py | 222 +++++++++++++-------------- cloud/openstack/os_subnets_facts.py | 37 +++-- cloud/rackspace/rax_files.py | 27 +++- cloud/rackspace/rax_files_objects.py | 74 +++++++-- database/mysql/mysql_db.py | 24 ++- files/file.py | 3 +- network/basics/uri.py | 20 ++- network/cumulus/cl_bond.py | 48 +++--- network/cumulus/cl_bridge.py | 55 ++++--- network/cumulus/cl_img_install.py | 34 ++-- network/cumulus/cl_interface.py | 80 +++++----- network/cumulus/cl_license.py | 54 ++++--- network/cumulus/cl_ports.py | 4 +- 19 files changed, 470 insertions(+), 332 deletions(-) diff --git a/cloud/openstack/os_auth.py b/cloud/openstack/os_auth.py index 4f4d22eac94..f4cdea432e1 100644 --- a/cloud/openstack/os_auth.py +++ b/cloud/openstack/os_auth.py @@ -37,10 +37,13 @@ ''' EXAMPLES = ''' -# Authenticate to the cloud and retrieve the service catalog -- os_auth: +- name: Authenticate to the cloud and retrieve the service catalog + os_auth: cloud: rax-dfw -- debug: var=service_catalog + +- name: Show service catalog + debug: + var: service_catalog ''' def main(): diff --git a/cloud/openstack/os_client_config.py b/cloud/openstack/os_client_config.py index 1627bdfe322..ba524a5b0b5 100644 --- a/cloud/openstack/os_client_config.py +++ b/cloud/openstack/os_client_config.py @@ -39,15 +39,17 @@ ''' EXAMPLES = ''' -# Get list of clouds that do not support security groups -- os_client_config: -- debug: var={{ item }} - with_items: "{{ openstack.clouds|rejectattr('secgroup_source', 'none')|list() }}" +- name: Get list of clouds that do not support security groups + os_client_config: -# Get the information back just about the mordred cloud -- os_client_config: +- debug: + var: "{{ item }}" + with_items: "{{ openstack.clouds | rejectattr('secgroup_source', 'none') | list }}" + +- name: Get the information back just about the mordred cloud + os_client_config: clouds: - - mordred + - mordred ''' diff --git a/cloud/openstack/os_image_facts.py b/cloud/openstack/os_image_facts.py index 4058d4003e8..9f693c73b77 100644 --- a/cloud/openstack/os_image_facts.py +++ b/cloud/openstack/os_image_facts.py @@ -42,15 +42,18 @@ ''' EXAMPLES = ''' -# Gather facts about a previously created image named image1 -- os_image_facts: +- name: Gather facts about a previously created image named image1 + os_image_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject image: image1 -- debug: var=openstack + +- name: Show openstack facts + debug: + var: openstack ''' RETURN = ''' diff --git a/cloud/openstack/os_networks_facts.py b/cloud/openstack/os_networks_facts.py index 9db5eceaa69..c261fc32b76 100644 --- a/cloud/openstack/os_networks_facts.py +++ b/cloud/openstack/os_networks_facts.py @@ -46,30 +46,36 @@ ''' EXAMPLES = ''' -# Gather facts about previously created networks -- os_networks_facts: +- name: Gather facts about previously created networks + os_networks_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject -- debug: var=openstack_networks -# Gather facts about a previously created network by name -- os_networks_facts: +- name: Show openstack networks + debug: + var: openstack_networks + +- name: Gather facts about a previously created network by name + os_networks_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject name: network1 -- debug: var=openstack_networks -# Gather facts about a previously created network with filter (note: name and - filters parameters are Not mutually exclusive) -- os_networks_facts: +- name: Show openstack networks + debug: + var: openstack_networks + +- name: Gather facts about a previously created network with filter + # Note: name and filters parameters are Not mutually exclusive + os_networks_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject @@ -78,7 +84,10 @@ subnets: - 057d4bdf-6d4d-4728-bb0f-5ac45a6f7400 - 443d4dc0-91d4-4998-b21c-357d10433483 -- debug: var=openstack_networks + +- name: Show openstack networks + debug: + var: openstack_networks ''' RETURN = ''' diff --git a/cloud/openstack/os_nova_flavor.py b/cloud/openstack/os_nova_flavor.py index 102b2bf2aee..8dd939bc8c2 100644 --- a/cloud/openstack/os_nova_flavor.py +++ b/cloud/openstack/os_nova_flavor.py @@ -88,22 +88,21 @@ ''' EXAMPLES = ''' -# Create 'tiny' flavor with 1024MB of RAM, 1 virtual CPU, and 10GB of -# local disk, and 10GB of ephemeral. -- os_nova_flavor: - cloud=mycloud - state=present - name=tiny - ram=1024 - vcpus=1 - disk=10 - ephemeral=10 - -# Delete 'tiny' flavor -- os_nova_flavor: - cloud=mycloud - state=absent - name=tiny +- name: "Create 'tiny' flavor with 1024MB of RAM, 1 virtual CPU, and 10GB of local disk, and 10GB of ephemeral." + os_nova_flavor: + cloud: mycloud + state: present + name: tiny + ram: 1024 + vcpus: 1 + disk: 10 + ephemeral: 10 + +- name: "Delete 'tiny' flavor" + os_nova_flavor: + cloud: mycloud + state: absent + name: tiny ''' RETURN = ''' diff --git a/cloud/openstack/os_object.py b/cloud/openstack/os_object.py index d5d77e1318c..d386d85330e 100644 --- a/cloud/openstack/os_object.py +++ b/cloud/openstack/os_object.py @@ -60,11 +60,19 @@ ''' EXAMPLES = ''' -# Creates a object named 'fstab' in the 'config' container -- os_object: cloud=mordred state=present name=fstab container=config filename=/etc/fstab - -# Deletes a container called config and all of its contents -- os_object: cloud=rax-iad state=absent container=config +- name: "Create a object named 'fstab' in the 'config' container" + os_object: + cloud: mordred + state: present + name: fstab + container: config + filename: /etc/fstab + +- name: Delete a container called config and all of its contents + os_object: + cloud: rax-iad + state: absent + container: config ''' diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index 12d8724e4ac..f4d546d211d 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -203,12 +203,11 @@ ''' EXAMPLES = ''' -# Creates a new instance and attaches to a network and passes metadata to -# the instance -- os_server: +- name: Create a new instance and attaches to a network and passes metadata to the instance + os_server: state: present auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' username: admin password: admin project_name: admin @@ -224,99 +223,98 @@ hostname: test1 group: uge_master -# Creates a new instance in HP Cloud AE1 region availability zone az2 and +# Create a new instance in HP Cloud AE1 region availability zone az2 and # automatically assigns a floating IP - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance - os_server: - state: present - auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ - username: username - password: Equality7-2521 - project_name: username-project1 - name: vm1 - region_name: region-b.geo-1 - availability_zone: az2 - image: 9302692b-b787-4b52-a3a6-daebb79cb498 - key_name: test - timeout: 200 - flavor: 101 - security_groups: default - auto_ip: yes - -# Creates a new instance in named cloud mordred availability zone az2 + - name: launch an instance + os_server: + state: present + auth: + auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + username: username + password: Equality7-2521 + project_name: username-project1 + name: vm1 + region_name: region-b.geo-1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + security_groups: default + auto_ip: yes + +# Create a new instance in named cloud mordred availability zone az2 # and assigns a pre-known floating IP - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance - os_server: - state: present - cloud: mordred - name: vm1 - availability_zone: az2 - image: 9302692b-b787-4b52-a3a6-daebb79cb498 - key_name: test - timeout: 200 - flavor: 101 - floating_ips: - - 12.34.56.79 - -# Creates a new instance with 4G of RAM on Ubuntu Trusty, ignoring + - name: launch an instance + os_server: + state: present + cloud: mordred + name: vm1 + availability_zone: az2 + image: 9302692b-b787-4b52-a3a6-daebb79cb498 + key_name: test + timeout: 200 + flavor: 101 + floating_ips: + - 12.34.56.79 + +# Create a new instance with 4G of RAM on Ubuntu Trusty, ignoring # deprecated images - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance - os_server: - name: vm1 - state: present - cloud: mordred - region_name: region-b.geo-1 - image: Ubuntu Server 14.04 - image_exclude: deprecated - flavor_ram: 4096 - -# Creates a new instance with 4G of RAM on Ubuntu Trusty on a Performance node + - name: launch an instance + os_server: + name: vm1 + state: present + cloud: mordred + region_name: region-b.geo-1 + image: Ubuntu Server 14.04 + image_exclude: deprecated + flavor_ram: 4096 + +# Create a new instance with 4G of RAM on Ubuntu Trusty on a Performance node - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance - os_server: - name: vm1 - cloud: rax-dfw - state: present - image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) - flavor_ram: 4096 - flavor_include: Performance + - name: launch an instance + os_server: + name: vm1 + cloud: rax-dfw + state: present + image: Ubuntu 14.04 LTS (Trusty Tahr) (PVHVM) + flavor_ram: 4096 + flavor_include: Performance # Creates a new instance and attaches to multiple network - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance with a string - os_server: - auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ - username: admin - password: admin - project_name: admin - name: vm1 - image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 - key_name: ansible_key - timeout: 200 - flavor: 4 - nics: "net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f,net-id=542f0430-62fe-11e5-9d70-feff819cdc9f..." - -# Creates a new instance and attaches to a network and passes metadata to -# the instance -- os_server: + - name: launch an instance with a string + os_server: + auth: + auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + nics: "net-id=4cb08b20-62fe-11e5-9d70-feff819cdc9f,net-id=542f0430-62fe-11e5-9d70-feff819cdc9f..." + +- name: Creates a new instance and attaches to a network and passes metadata to the instance + os_server: state: present auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ + auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' username: admin password: admin project_name: admin @@ -330,51 +328,51 @@ - net-name: another_network meta: "hostname=test1,group=uge_master" -# Creates a new instance and attaches to a specific network -- os_server: - state: present - auth: - auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ - username: admin - password: admin - project_name: admin - name: vm1 - image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 - key_name: ansible_key - timeout: 200 - flavor: 4 - network: another_network - -# Creates a new instance with 4G of RAM on a 75G Ubuntu Trusty volume +- name: Creates a new instance and attaches to a specific network + os_server: + state: present + auth: + auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + username: admin + password: admin + project_name: admin + name: vm1 + image: 4f905f38-e52a-43d2-b6ec-754a13ffb529 + key_name: ansible_key + timeout: 200 + flavor: 4 + network: another_network + +# Create a new instance with 4G of RAM on a 75G Ubuntu Trusty volume - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance - os_server: - name: vm1 - state: present - cloud: mordred - region_name: ams01 - image: Ubuntu Server 14.04 - flavor_ram: 4096 - boot_from_volume: True - volume_size: 75 + - name: launch an instance + os_server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + boot_from_volume: True + volume_size: 75 # Creates a new instance with 2 volumes attached - name: launch a compute instance hosts: localhost tasks: - - name: launch an instance - os_server: - name: vm1 - state: present - cloud: mordred - region_name: ams01 - image: Ubuntu Server 14.04 - flavor_ram: 4096 - volumes: - - photos - - music + - name: launch an instance + os_server: + name: vm1 + state: present + cloud: mordred + region_name: ams01 + image: Ubuntu Server 14.04 + flavor_ram: 4096 + volumes: + - photos + - music ''' diff --git a/cloud/openstack/os_subnets_facts.py b/cloud/openstack/os_subnets_facts.py index 8d853de76de..dce24362f78 100644 --- a/cloud/openstack/os_subnets_facts.py +++ b/cloud/openstack/os_subnets_facts.py @@ -46,36 +46,45 @@ ''' EXAMPLES = ''' -# Gather facts about previously created subnets -- os_subnets_facts: +- name: Gather facts about previously created subnets + os_subnets_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject -- debug: var=openstack_subnets -# Gather facts about a previously created subnet by name -- os_subnets_facts: +- name: Show openstack subnets + debug: + var: openstack_subnets + +- name: Gather facts about a previously created subnet by name + os_subnets_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject - name: subnet1 -- debug: var=openstack_subnets + name: subnet1 + +- name: Show openstack subnets + debug: + var: openstack_subnets -# Gather facts about a previously created subnet with filter (note: name and - filters parameters are Not mutually exclusive) -- os_subnets_facts: +- name: Gather facts about a previously created subnet with filter + # Note: name and filters parameters are not mutually exclusive + os_subnets_facts: auth: - auth_url: https://your_api_url.com:9000/v2.0 + auth_url: 'https://your_api_url.com:9000/v2.0' username: user password: password project_name: someproject filters: tenant_id: 55e2ce24b2a245b09f181bf025724cbe -- debug: var=openstack_subnets + +- name: Show openstack subnets + debug: + var: openstack_subnets ''' RETURN = ''' diff --git a/cloud/rackspace/rax_files.py b/cloud/rackspace/rax_files.py index 48d5db21284..77ab70d8e8c 100644 --- a/cloud/rackspace/rax_files.py +++ b/cloud/rackspace/rax_files.py @@ -86,10 +86,12 @@ gather_facts: no tasks: - name: "List all containers" - rax_files: state=list + rax_files: + state: list - name: "Create container called 'mycontainer'" - rax_files: container=mycontainer + rax_files: + container: mycontainer - name: "Create container 'mycontainer2' with metadata" rax_files: @@ -99,19 +101,30 @@ file_for: someuser@example.com - name: "Set a container's web index page" - rax_files: container=mycontainer web_index=index.html + rax_files: + container: mycontainer + web_index: index.html - name: "Set a container's web error page" - rax_files: container=mycontainer web_error=error.html + rax_files: + container: mycontainer + web_error: error.html - name: "Make container public" - rax_files: container=mycontainer public=yes + rax_files: + container: mycontainer + public: yes - name: "Make container public with a 24 hour TTL" - rax_files: container=mycontainer public=yes ttl=86400 + rax_files: + container: mycontainer + public: yes + ttl: 86400 - name: "Make container private" - rax_files: container=mycontainer private=yes + rax_files: + container: mycontainer + private: yes - name: "Test Cloud Files Containers Metadata Storage" hosts: local diff --git a/cloud/rackspace/rax_files_objects.py b/cloud/rackspace/rax_files_objects.py index d89a8067093..d0175996b14 100644 --- a/cloud/rackspace/rax_files_objects.py +++ b/cloud/rackspace/rax_files_objects.py @@ -102,28 +102,50 @@ gather_facts: False tasks: - name: "Get objects from test container" - rax_files_objects: container=testcont dest=~/Downloads/testcont + rax_files_objects: + container: testcont + dest: ~/Downloads/testcont - name: "Get single object from test container" - rax_files_objects: container=testcont src=file1 dest=~/Downloads/testcont + rax_files_objects: + container: testcont + src: file1 + dest: ~/Downloads/testcont - name: "Get several objects from test container" - rax_files_objects: container=testcont src=file1,file2,file3 dest=~/Downloads/testcont + rax_files_objects: + container: testcont + src: file1,file2,file3 + dest: ~/Downloads/testcont - name: "Delete one object in test container" - rax_files_objects: container=testcont method=delete dest=file1 + rax_files_objects: + container: testcont + method: delete + dest: file1 - name: "Delete several objects in test container" - rax_files_objects: container=testcont method=delete dest=file2,file3,file4 + rax_files_objects: + container: testcont + method: delete + dest: file2,file3,file4 - name: "Delete all objects in test container" - rax_files_objects: container=testcont method=delete + rax_files_objects: + container: testcont + method: delete - name: "Upload all files to test container" - rax_files_objects: container=testcont method=put src=~/Downloads/onehundred + rax_files_objects: + container: testcont + method: put + src: ~/Downloads/onehundred - name: "Upload one file to test container" - rax_files_objects: container=testcont method=put src=~/Downloads/testcont/file1 + rax_files_objects: + container: testcont + method: put + src: ~/Downloads/testcont/file1 - name: "Upload one file to test container with metadata" rax_files_objects: @@ -135,14 +157,25 @@ who_uploaded_this: someuser@example.com - name: "Upload one file to test container with TTL of 60 seconds" - rax_files_objects: container=testcont method=put src=~/Downloads/testcont/file3 expires=60 + rax_files_objects: + container: testcont + method: put + src: ~/Downloads/testcont/file3 + expires: 60 - name: "Attempt to get remote object that does not exist" - rax_files_objects: container=testcont method=get src=FileThatDoesNotExist.jpg dest=~/Downloads/testcont + rax_files_objects: + container: testcont + method: get + src: FileThatDoesNotExist.jpg + dest: ~/Downloads/testcont ignore_errors: yes - name: "Attempt to delete remote object that does not exist" - rax_files_objects: container=testcont method=delete dest=FileThatDoesNotExist.jpg + rax_files_objects: + container: testcont + method: delete + dest: FileThatDoesNotExist.jpg ignore_errors: yes - name: "Test Cloud Files Objects Metadata" @@ -150,10 +183,16 @@ gather_facts: false tasks: - name: "Get metadata on one object" - rax_files_objects: container=testcont type=meta dest=file2 + rax_files_objects: + container: testcont + type: meta + dest: file2 - name: "Get metadata on several objects" - rax_files_objects: container=testcont type=meta src=file2,file1 + rax_files_objects: + container: testcont + type: meta + src: file2,file1 - name: "Set metadata on an object" rax_files_objects: @@ -167,7 +206,10 @@ clear_meta: true - name: "Verify metadata is set" - rax_files_objects: container=testcont type=meta src=file17 + rax_files_objects: + container: testcont + type: meta + src: file17 - name: "Delete metadata" rax_files_objects: @@ -180,7 +222,9 @@ key2: '' - name: "Get metadata on all objects" - rax_files_objects: container=testcont type=meta + rax_files_objects: + container: testcont + type: meta ''' try: diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index 2b6a19d57a3..437288425f8 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -78,25 +78,33 @@ ''' EXAMPLES = ''' -# Create a new database with name 'bobdata' -- mysql_db: +- name: Create a new database with name 'bobdata' + mysql_db: name: bobdata state: present # Copy database dump file to remote host and restore it to database 'my_db' -- copy: +- name: Copy database dump file + copy: src: dump.sql.bz2 dest: /tmp -- mysql_db: +- name: Restore database + mysql_db: name: my_db state: import target: /tmp/dump.sql.bz2 -# Dumps all databases to hostname.sql -- mysql_db: state=dump name=all target=/tmp/{{ inventory_hostname }}.sql +- name: Dump all databases to hostname.sql + mysql_db: + state: dump + name: all + target: /tmp/{{ inventory_hostname }}.sql -# Imports file.sql similar to mysql -u -p < hostname.sql -- mysql_db: state=import name=all target=/tmp/{{ inventory_hostname }}.sql +- name: Import file.sql similar to mysql -u -p < hostname.sql + mysql_db: + state: import + name: all + target: /tmp/{{ inventory_hostname }}.sql ''' import os diff --git a/files/file.py b/files/file.py index ed796cb1f77..25b008598b3 100644 --- a/files/file.py +++ b/files/file.py @@ -114,7 +114,8 @@ mode: "u=rw,g=r,o=r" # touch the same file, but add/remove some permissions -- file: path=/etc/foo.conf +- file: + path: /etc/foo.conf state: touch mode: "u+rw,g-wx,o-rwx" diff --git a/network/basics/uri.py b/network/basics/uri.py index afff08757b4..c66ce399c9c 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -153,20 +153,24 @@ ''' EXAMPLES = ''' -# Check that you can connect (GET) to a page and it returns a status 200 -- uri: url=http://www.example.com +- name: Check that you can connect (GET) to a page and it returns a status 200 + uri: + url: 'http://www.example.com' # Check that a page returns a status 200 and fail if the word AWESOME is not # in the page contents. -- action: uri url=http://www.example.com return_content=yes +- uri: + url: http://www.example.com + return_content: yes register: webpage -- action: fail +- name: Fail if AWESOME is not in the page content + fail: when: "'AWESOME' not in webpage.content" -# Create a JIRA issue -- uri: +- name: Create a JIRA issue + uri: url: https://your.jira.example.com/rest/api/2/issue/ method: POST user: your_username @@ -193,8 +197,8 @@ return_content: yes HEADER_Cookie: "{{login.set_cookie}}" -# Queue build of a project in Jenkins: -- uri: +- name: Queue build of a project in Jenkins + uri: url: "http://{{ jenkins.host }}/job/{{ jenkins.job }}/build?token={{ jenkins.token }}" method: GET user: "{{ jenkins.user }}" diff --git a/network/cumulus/cl_bond.py b/network/cumulus/cl_bond.py index cc8930ce8be..958edc7fe30 100644 --- a/network/cumulus/cl_bond.py +++ b/network/cumulus/cl_bond.py @@ -146,33 +146,39 @@ EXAMPLES = ''' # Options ['virtual_mac', 'virtual_ip'] are required together # configure a bond interface with IP address -cl_bond: name=bond0 slaves="swp4-5" ipv4=10.1.1.1/24 -notify: reload networking +- cl_bond: + name: bond0 + slaves: "swp4-5" + ipv4: 10.1.1.1/24 + notify: reload networking # configure bond as a dual-connected clag bond -cl_bond: name=bond1 slaves="swp1s0 swp2s0" clag_id=1 -notify: reload networking +- cl_bond: + name: bond1 + slaves: "swp1s0 swp2s0" + clag_id: 1 + notify: reload networking # define cl_bond once in tasks file # then write interface config in variables file # with just the options you want. -cl_bond: - name: "{{ item.key }}" - slaves: "{{ item.value.slaves }}" - clag_id: "{{ item.value.clag_id|default(omit) }}" - ipv4: "{{ item.value.ipv4|default(omit) }}" - ipv6: "{{ item.value.ipv6|default(omit) }}" - alias_name: "{{ item.value.alias_name|default(omit) }}" - addr_method: "{{ item.value.addr_method|default(omit) }}" - mtu: "{{ item.value.mtu|default(omit) }}" - vids: "{{ item.value.vids|default(omit) }}" - virtual_ip: "{{ item.value.virtual_ip|default(omit) }}" - virtual_mac: "{{ item.value.virtual_mac|default(omit) }}" - mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}" - mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}" - mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}" -with_dict: "{{ cl_bonds }}" -notify: reload networking +- cl_bond: + name: "{{ item.key }}" + slaves: "{{ item.value.slaves }}" + clag_id: "{{ item.value.clag_id|default(omit) }}" + ipv4: "{{ item.value.ipv4|default(omit) }}" + ipv6: "{{ item.value.ipv6|default(omit) }}" + alias_name: "{{ item.value.alias_name|default(omit) }}" + addr_method: "{{ item.value.addr_method|default(omit) }}" + mtu: "{{ item.value.mtu|default(omit) }}" + vids: "{{ item.value.vids|default(omit) }}" + virtual_ip: "{{ item.value.virtual_ip|default(omit) }}" + virtual_mac: "{{ item.value.virtual_mac|default(omit) }}" + mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}" + mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}" + mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}" + with_dict: "{{ cl_bonds }}" + notify: reload networking # In vars file # ============ diff --git a/network/cumulus/cl_bridge.py b/network/cumulus/cl_bridge.py index 752d4fdd6e4..abeb0758a78 100644 --- a/network/cumulus/cl_bridge.py +++ b/network/cumulus/cl_bridge.py @@ -101,40 +101,47 @@ EXAMPLES = ''' # Options ['virtual_mac', 'virtual_ip'] are required together # configure a bridge vlan aware bridge. -cl_bridge: name=br0 ports='swp1-12' vlan_aware='yes' -notify: reload networking +- cl_bridge: + name: br0 + ports: 'swp1-12' + vlan_aware: 'yes' + notify: reload networking # configure bridge interface to define a default set of vlans -cl_bridge: name=bridge ports='swp1-12' vlan_aware='yes' vids='1-100' -notify: reload networking +- cl_bridge: + name: bridge + ports: 'swp1-12' + vlan_aware: 'yes' + vids: '1-100' + notify: reload networking # define cl_bridge once in tasks file # then write interface config in variables file # with just the options you want. -cl_bridge: - name: "{{ item.key }}" - ports: "{{ item.value.ports }}" - vlan_aware: "{{ item.value.vlan_aware|default(omit) }}" - ipv4: "{{ item.value.ipv4|default(omit) }}" - ipv6: "{{ item.value.ipv6|default(omit) }}" - alias_name: "{{ item.value.alias_name|default(omit) }}" - addr_method: "{{ item.value.addr_method|default(omit) }}" - mtu: "{{ item.value.mtu|default(omit) }}" - vids: "{{ item.value.vids|default(omit) }}" - virtual_ip: "{{ item.value.virtual_ip|default(omit) }}" - virtual_mac: "{{ item.value.virtual_mac|default(omit) }}" - mstpctl_treeprio: "{{ item.value.mstpctl_treeprio|default(omit) }}" -with_dict: "{{ cl_bridges }}" -notify: reload networking +- cl_bridge: + name: "{{ item.key }}" + ports: "{{ item.value.ports }}" + vlan_aware: "{{ item.value.vlan_aware|default(omit) }}" + ipv4: "{{ item.value.ipv4|default(omit) }}" + ipv6: "{{ item.value.ipv6|default(omit) }}" + alias_name: "{{ item.value.alias_name|default(omit) }}" + addr_method: "{{ item.value.addr_method|default(omit) }}" + mtu: "{{ item.value.mtu|default(omit) }}" + vids: "{{ item.value.vids|default(omit) }}" + virtual_ip: "{{ item.value.virtual_ip|default(omit) }}" + virtual_mac: "{{ item.value.virtual_mac|default(omit) }}" + mstpctl_treeprio: "{{ item.value.mstpctl_treeprio|default(omit) }}" + with_dict: "{{ cl_bridges }}" + notify: reload networking # In vars file # ============ cl_bridge: - br0: - alias_name: 'vlan aware bridge' - ports: ['swp1', 'swp3'] - vlan_aware: true - vids: ['1-100'] + br0: + alias_name: 'vlan aware bridge' + ports: ['swp1', 'swp3'] + vlan_aware: true + vids: ['1-100'] ''' RETURN = ''' diff --git a/network/cumulus/cl_img_install.py b/network/cumulus/cl_img_install.py index 00e4f9034c2..cfd5f4548a0 100644 --- a/network/cumulus/cl_img_install.py +++ b/network/cumulus/cl_img_install.py @@ -59,32 +59,40 @@ ## Download and install the image from a webserver. - - name: install image using using http url. Switch slots so the subsequent - will load the new version - cl_img_install: version=2.0.1 - src='http://10.1.1.1/CumulusLinux-2.0.1.bin' - switch_slot=yes +- name: Install image using using http url. Switch slots so the subsequent will load the new version + cl_img_install: + version: 2.0.1 + src: 'http://10.1.1.1/CumulusLinux-2.0.1.bin' + switch_slot: yes ## Copy the software from the ansible server to the switch. ## The module will get the code version from the filename ## The code will be installed in the alternate slot but the slot will not be primary ## A subsequent reload will not run the new code - - name: download cumulus linux to local system - get_url: src=ftp://cumuluslinux.bin dest=/root/CumulusLinux-2.0.1.bin +- name: Download cumulus linux to local system + get_url: + src: 'ftp://cumuluslinux.bin' + dest: /root/CumulusLinux-2.0.1.bin - - name: install image from local filesystem. Get version from the filename - cl_img_install: src='/root/CumulusLinux-2.0.1.bin' +- name: Install image from local filesystem. Get version from the filename. + cl_img_install: + src: /root/CumulusLinux-2.0.1.bin ## If the image name has been changed from the original name, use the `version` option ## to inform the module exactly what code version is been installed - - name: download cumulus linux to local system - get_url: src=ftp://CumulusLinux-2.0.1.bin dest=/root/image.bin +- name: Download cumulus linux to local system + get_url: + src: 'ftp://CumulusLinux-2.0.1.bin' + dest: /root/image.bin - - name: install image and switch slots. only reboot needed - cl_img_install: version=2.0.1 src=/root/image.bin switch_slot=yes' +- name: install image and switch slots. only reboot needed + cl_img_install: + version: 2.0.1 + src: /root/image.bin + switch_slot: yes ''' RETURN = ''' diff --git a/network/cumulus/cl_interface.py b/network/cumulus/cl_interface.py index 241a69c7eff..475f9ce21dc 100644 --- a/network/cumulus/cl_interface.py +++ b/network/cumulus/cl_interface.py @@ -114,45 +114,55 @@ EXAMPLES = ''' # Options ['virtual_mac', 'virtual_ip'] are required together -# configure a front panel port with an IP -cl_interface: name=swp1 ipv4=10.1.1.1/24 -notify: reload networking - -# configure front panel to use DHCP -cl_interface: name=swp2 addr_family=dhcp -notify: reload networking - -# configure a SVI for vlan 100 interface with an IP -cl_interface: name=bridge.100 ipv4=10.1.1.1/24 -notify: reload networking - -# configure subinterface with an IP -cl_interface: name=bond0.100 alias_name='my bond' ipv4=10.1.1.1/24 -notify: reload networking +- name: Configure a front panel port with an IP + cl_interface: + name: swp1 + ipv4: 10.1.1.1/24 + notify: reload networking + +- name: Configure front panel to use DHCP + cl_interface: + name: swp2 + addr_family: dhcp + notify: reload networking + +- name: Configure a SVI for vlan 100 interface with an IP + cl_interface: + name: bridge.100 + ipv4: 10.1.1.1/24 + notify: reload networking + +- name: Configure subinterface with an IP + cl_interface: + name: bond0.100 + alias_name: 'my bond' + ipv4: 10.1.1.1/24 + notify: reload networking # define cl_interfaces once in tasks # then write interfaces in variables file # with just the options you want. -cl_interface: - name: "{{ item.key }}" - ipv4: "{{ item.value.ipv4|default(omit) }}" - ipv6: "{{ item.value.ipv6|default(omit) }}" - alias_name: "{{ item.value.alias_name|default(omit) }}" - addr_method: "{{ item.value.addr_method|default(omit) }}" - speed: "{{ item.value.link_speed|default(omit) }}" - mtu: "{{ item.value.mtu|default(omit) }}" - clagd_enable: "{{ item.value.clagd_enable|default(omit) }}" - clagd_peer_ip: "{{ item.value.clagd_peer_ip|default(omit) }}" - clagd_sys_mac: "{{ item.value.clagd_sys_mac|default(omit) }}" - clagd_priority: "{{ item.value.clagd_priority|default(omit) }}" - vids: "{{ item.value.vids|default(omit) }}" - virtual_ip: "{{ item.value.virtual_ip|default(omit) }}" - virtual_mac: "{{ item.value.virtual_mac|default(omit) }}" - mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork|default('no') }}" - mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge|default('no') }}" - mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard|default('no') }}" -with_dict: "{{ cl_interfaces }}" -notify: reload networking + - name: Create interfaces + cl_interface: + name: "{{ item.key }}" + ipv4: "{{ item.value.ipv4 | default(omit) }}" + ipv6: "{{ item.value.ipv6 | default(omit) }}" + alias_name: "{{ item.value.alias_name | default(omit) }}" + addr_method: "{{ item.value.addr_method | default(omit) }}" + speed: "{{ item.value.link_speed | default(omit) }}" + mtu: "{{ item.value.mtu | default(omit) }}" + clagd_enable: "{{ item.value.clagd_enable | default(omit) }}" + clagd_peer_ip: "{{ item.value.clagd_peer_ip | default(omit) }}" + clagd_sys_mac: "{{ item.value.clagd_sys_mac | default(omit) }}" + clagd_priority: "{{ item.value.clagd_priority | default(omit) }}" + vids: "{{ item.value.vids | default(omit) }}" + virtual_ip: "{{ item.value.virtual_ip | default(omit) }}" + virtual_mac: "{{ item.value.virtual_mac | default(omit) }}" + mstpctl_portnetwork: "{{ item.value.mstpctl_portnetwork | default('no') }}" + mstpctl_portadminedge: "{{ item.value.mstpctl_portadminedge | default('no') }}" + mstpctl_bpduguard: "{{ item.value.mstpctl_bpduguard | default('no') }}" + with_dict: "{{ cl_interfaces }}" + notify: reload networking # In vars file diff --git a/network/cumulus/cl_license.py b/network/cumulus/cl_license.py index 7690d8ac1ce..8097cda5f56 100644 --- a/network/cumulus/cl_license.py +++ b/network/cumulus/cl_license.py @@ -55,33 +55,37 @@ ''' EXAMPLES = ''' -Example playbook using the cl_license module to manage licenses on Cumulus Linux - ---- - - hosts: all - tasks: - - name: install license using http url - cl_license: src='http://10.1.1.1/license.txt' - notify: restart switchd - - - name: Triggers switchd to be restarted right away, before play, or role - is over. This is desired behaviour - meta: flush_handlers - - - name: configure interfaces - template: src=interfaces.j2 dest=/etc/network/interfaces - notify: restart networking - - handlers: - - name: restart switchd - service: name=switchd state=restarted - - name: restart networking - service: name=networking state=reloaded - ----- +# Example playbook using the cl_license module to manage licenses on Cumulus Linux + +- hosts: all + tasks: + - name: install license using http url + cl_license: + src: 'http://10.1.1.1/license.txt' + notify: restart switchd + + - name: Triggers switchd to be restarted right away, before play, or role + is over. This is desired behaviour + meta: flush_handlers + + - name: Configure interfaces + template: + src: interfaces.j2 + dest: /etc/network/interfaces + notify: restart networking + + handlers: + - name: restart switchd + service: + name: switchd + state: restarted + - name: restart networking + service: + name: networking + state: reloaded # Force all switches to accept a new license. Typically not needed -ansible -m cl_license -a "src='http://10.1.1.1/new_lic' force=yes" -u root all +ansible -m cl_license -a "src='http://10.1.1.1/new_lic' force=yes" -u root all ---- diff --git a/network/cumulus/cl_ports.py b/network/cumulus/cl_ports.py index 02728203400..9ed48089cc4 100644 --- a/network/cumulus/cl_ports.py +++ b/network/cumulus/cl_ports.py @@ -48,7 +48,9 @@ ## Unganged port config using simple args - name: configure ports.conf setup - cl_ports: speed_4_by_10g="swp1, swp32" speed_40g="swp2-31" + cl_ports: + speed_4_by_10g: "swp1, swp32" + speed_40g: "swp2-31" notify: restart switchd ## Unganged port configuration on certain ports using complex args From 9f200a9cb90d42b63ab506e3956340dfabf51f24 Mon Sep 17 00:00:00 2001 From: David Wittman Date: Wed, 2 Dec 2015 11:27:44 -0600 Subject: [PATCH 697/770] Add 'link' file_type to find_module - Adds the 'link' file_type for finding symbolic or hard links - Use `os.lstat` instead of `os.stat` to prevent the following of links when statting the file. --- files/find.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/files/find.py b/files/find.py index 590a8771178..2a7c6b6d824 100644 --- a/files/find.py +++ b/files/find.py @@ -66,7 +66,7 @@ required: false description: - Type of file to select - choices: [ "file", "directory" ] + choices: [ "file", "directory", "link" ] default: "file" recurse: required: false @@ -275,7 +275,7 @@ def main(): paths = dict(required=True, aliases=['name','path'], type='list'), patterns = dict(default=['*'], type='list', aliases=['pattern']), contains = dict(default=None, type='str'), - file_type = dict(default="file", choices=['file', 'directory'], type='str'), + file_type = dict(default="file", choices=['file', 'directory', 'link'], type='str'), age = dict(default=None, type='str'), age_stamp = dict(default="mtime", choices=['atime','mtime','ctime'], type='str'), size = dict(default=None, type='str'), @@ -331,7 +331,7 @@ def main(): continue try: - st = os.stat(fsname) + st = os.lstat(fsname) except: msg+="%s was skipped as it does not seem to be a valid file or it cannot be accessed\n" % fsname continue @@ -354,6 +354,11 @@ def main(): r['checksum'] = module.sha1(fsname) filelist.append(r) + elif stat.S_ISLNK(st.st_mode) and params['file_type'] == 'link': + if pfilter(fsobj, params['patterns'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']): + r.update(statinfo(st)) + filelist.append(r) + if not params['recurse']: break else: From c411d518b96f373931119af0c4cd1799fcb86499 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 17 Nov 2016 15:53:43 +0100 Subject: [PATCH 698/770] Performance improvement using in-operator for hash lookups Just a small cleanup for the existing occurrences. Using the in-operator for hash lookups is faster than using .has_key() http://stackoverflow.com/questions/1323410/has-key-or-in --- packaging/os/rhn_register.py | 2 +- system/user.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index 8908e44857b..2ecf3e0fec7 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -163,7 +163,7 @@ def load_config(self): def get_option_default(self, key, default=''): # ignore pep8 W601 errors for this line # setting this to use 'in' does not work in the rhn library - if self.has_key(key): + if key in self: return self[key] else: return default diff --git a/system/user.py b/system/user.py index b67f962058b..dd079b64f2d 100644 --- a/system/user.py +++ b/system/user.py @@ -1716,7 +1716,7 @@ def create_user(self, command_name='dscl'): self.chown_homedir(int(self.uid), int(self.group), self.home) for field in self.fields: - if self.__dict__.has_key(field[0]) and self.__dict__[field[0]]: + if field[0] in self.__dict__ and self.__dict__[field[0]]: cmd = self._get_dscl() cmd += [ '-create', '/Users/%s' % self.name, field[1], self.__dict__[field[0]]] @@ -1753,7 +1753,7 @@ def modify_user(self): self._make_group_numerical() for field in self.fields: - if self.__dict__.has_key(field[0]) and self.__dict__[field[0]]: + if field[0] in self.__dict__ and self.__dict__[field[0]]: current = self._get_user_property(field[1]) if current is None or current != self.__dict__[field[0]]: cmd = self._get_dscl() From f08ba8c7d6843d6459b471d26289b63d2862d5a5 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Thu, 17 Nov 2016 15:19:14 +0100 Subject: [PATCH 699/770] Performance improvement using in-operator on dicts Just a small cleanup for the existing occurrences. Using the in-operator for hash lookups is faster than using .keys() http://stackoverflow.com/questions/29314269/why-do-key-in-dict-and-key-in-dict-keys-have-the-same-output --- cloud/google/gce.py | 2 +- network/nxos/nxos_bgp.py | 2 +- network/nxos/nxos_bgp_af.py | 2 +- network/nxos/nxos_feature.py | 4 ++-- network/nxos/nxos_igmp_interface.py | 2 +- network/nxos/nxos_ip_interface.py | 2 +- network/nxos/nxos_static_route.py | 2 +- network/nxos/nxos_vpc_interface.py | 2 +- packaging/os/yum.py | 2 +- 9 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index 7bf342bda5d..e1682e9c424 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -449,7 +449,7 @@ def create_instances(module, gce, instance_names, number): bad_perms = [] if service_account_permissions: for perm in service_account_permissions: - if perm not in gce.SA_SCOPES_MAP.keys(): + if perm not in gce.SA_SCOPES_MAP: bad_perms.append(perm) if len(bad_perms) > 0: module.fail_json(msg='bad permissions: %s' % str(bad_perms)) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 5ec5135b03c..c937fb8dbe0 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -757,7 +757,7 @@ def state_present(module, existing, proposed, candidate): elif value is False: commands.append('no {0}'.format(key)) elif value == 'default': - if key in PARAM_TO_DEFAULT_KEYMAP.keys(): + if key in PARAM_TO_DEFAULT_KEYMAP: commands.append('{0} {1}'.format(key, PARAM_TO_DEFAULT_KEYMAP[key])) elif existing_commands.get(key): existing_value = existing_commands.get(key) diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 0eb920f3c72..1f02b113cbf 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -889,7 +889,7 @@ def state_present(module, existing, proposed, candidate): commands.append('no {0}'.format(key)) elif value == 'default': - if key in PARAM_TO_DEFAULT_KEYMAP.keys(): + if key in PARAM_TO_DEFAULT_KEYMAP: commands.append('{0} {1}'.format(key, PARAM_TO_DEFAULT_KEYMAP[key])) elif existing_commands.get(key): diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index 15d49cc73c0..bdf7980b153 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -355,7 +355,7 @@ def get_available_features(feature, module): if 'enabled' in state: state = 'enabled' - if feature not in available_features.keys(): + if feature not in available_features: available_features[feature] = state else: if (available_features[feature] == 'disabled' and @@ -443,7 +443,7 @@ def main(): state = module.params['state'].lower() available_features = get_available_features(feature, module) - if feature not in available_features.keys(): + if feature not in available_features: module.fail_json( msg='Invalid feature name.', features_currently_supported=available_features, diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index 1f5aa5d9a88..902d5808ccf 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -823,7 +823,7 @@ def main(): if state == 'absent': for each in CANNOT_ABSENT: - if each in proposed.keys(): + if each in proposed: module.fail_json(msg='only params: oif_prefix, oif_source, ' 'oif_routemap can be used when ' 'state=absent') diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index eef79543d56..25d50cbc357 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -560,7 +560,7 @@ def get_config_ip_commands(delta, interface, existing, version): # loop used in the situation that just an IP address or just a # mask is changing, not both. for each in ['addr', 'mask']: - if each not in delta.keys(): + if each not in delta: delta[each] = existing[each] if version == 'v4': diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index 10a1f849ada..a0e2a78bb11 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -321,7 +321,7 @@ def get_existing(module, prefix, warnings): group_route = match_route.groupdict() for key in key_map: - if key not in group_route.keys(): + if key not in group_route: group_route[key] = '' group_route['prefix'] = prefix group_route['vrf'] = module.params['vrf'] diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index dcc1c213dbf..0f930cacf8f 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -501,7 +501,7 @@ def main(): if vpc: mapping = get_existing_portchannel_to_vpc_mappings(module) - if vpc in mapping.keys() and portchannel != mapping[vpc].strip('Po'): + if vpc in mapping and portchannel != mapping[vpc].strip('Po'): module.fail_json(msg="This vpc is already configured on " "another portchannel. Remove it first " "before trying to assign it here. ", diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 13cc4d612d5..cc9b004033a 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -877,7 +877,7 @@ def latest(module, items, repoq, yum_basecmd, conf_file, en_repos, dis_repos): # or virtual provides (like "python-*" or "smtp-daemon") while # updates contains name only. this_name_only = '-'.join(this.split('-')[:-2]) - if spec in pkgs['update'] and this_name_only in updates.keys(): + if spec in pkgs['update'] and this_name_only in updates: nothing_to_do = False will_update.add(spec) # Massage the updates list From 5fc0d816017ab2935597cb16274634ffd48ab2e1 Mon Sep 17 00:00:00 2001 From: jctanner Date: Thu, 17 Nov 2016 16:24:52 -0500 Subject: [PATCH 700/770] user: port has_key to py3 syntax (#5641) From d3543ff67c788531cadfcaee19395bbb70125364 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 17 Nov 2016 14:04:22 -0800 Subject: [PATCH 701/770] Older versions of rhn-client-tools don't understand containment tests. --- packaging/os/rhn_register.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index 2ecf3e0fec7..19618df1069 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -161,9 +161,10 @@ def load_config(self): # configuration. Yeah, I know this should be subclassed ... but, oh # well def get_option_default(self, key, default=''): - # ignore pep8 W601 errors for this line - # setting this to use 'in' does not work in the rhn library - if key in self: + # the class in rhn-client-tools that this comes from didn't + # implement __contains__(). That's why we check if the key is + # present in the dictionary that is the actual storage + if key in self.dict: return self[key] else: return default From 2a9ddb1c70cab02af8341f674265384af250452b Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Thu, 17 Nov 2016 14:05:55 -0800 Subject: [PATCH 702/770] Clarify the comment --- packaging/os/rhn_register.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index 19618df1069..1a99fed8273 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -162,8 +162,8 @@ def load_config(self): # well def get_option_default(self, key, default=''): # the class in rhn-client-tools that this comes from didn't - # implement __contains__(). That's why we check if the key is - # present in the dictionary that is the actual storage + # implement __contains__() until 2.5.x. That's why we check if + # the key is present in the dictionary that is the actual storage if key in self.dict: return self[key] else: From 9914626ce2dda7892119443e7ae95d71058345e5 Mon Sep 17 00:00:00 2001 From: Monty Taylor Date: Fri, 18 Nov 2016 08:51:28 -0600 Subject: [PATCH 703/770] Add a few lines about testing (#5662) --- cloud/openstack/README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cloud/openstack/README.md b/cloud/openstack/README.md index 4a872b11954..36cdcd383fe 100644 --- a/cloud/openstack/README.md +++ b/cloud/openstack/README.md @@ -54,3 +54,11 @@ Libraries users as a primary audience, they are for intra-server communication. The python-openstacksdk is the future there, and shade will migrate to it when its ready in a manner that is not noticable to ansible users. + +Testing +------- + +* Integration testing is currently done in OpenStack's CI system in + http://git.openstack.org/cgit/openstack-infra/shade/tree/shade/tests/ansible +* Testing in shade produces an obvious chicken-and-egg scenario. Work is under + way to trigger from and report on PRs directly. From 89b47ae7964adcd09598690092b8194f63030045 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Fri, 18 Nov 2016 09:56:39 -0500 Subject: [PATCH 704/770] Revert "ios_mods - added stdout to exception output. Removed to_lines()" (#5663) --- network/ios/ios_command.py | 11 ++++++++--- network/ios/ios_config.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/network/ios/ios_command.py b/network/ios/ios_command.py index c5923e4e2b0..4204a73a682 100644 --- a/network/ios/ios_command.py +++ b/network/ios/ios_command.py @@ -148,6 +148,12 @@ VALID_KEYS = ['command', 'prompt', 'response'] +def to_lines(stdout): + for item in stdout: + if isinstance(item, string_types): + item = str(item).split('\n') + yield item + def parse_commands(module): for cmd in module.params['commands']: if isinstance(cmd, string_types): @@ -210,9 +216,7 @@ def main(): module.fail_json(msg=str(exc), failed_conditions=exc.failed_conditions) except NetworkError: exc = get_exception() - module.disconnect() - module.fail_json(msg=str(exc), stdout=exc.kwargs.get('stdout')) - + module.fail_json(msg=str(exc)) result = dict(changed=False, stdout=list()) @@ -224,6 +228,7 @@ def main(): result['stdout'].append(output) result['warnings'] = warnings + result['stdout_lines'] = list(to_lines(result['stdout'])) module.exit_json(**result) diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 4f6786ee23e..6c07d2cfdc0 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -391,7 +391,7 @@ def main(): except NetworkError: exc = get_exception() module.disconnect() - module.fail_json(msg=str(exc), stdout=exc.kwargs.get('stdout')) + module.fail_json(msg=str(exc)) module.disconnect() module.exit_json(**result) From b835ae271734821c8e41ce8cd278a01266a2247b Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 9 Nov 2016 13:51:26 -0500 Subject: [PATCH 705/770] Several systemd fixes Allow some operations on missing services Better sysv handling Rearranged error reporting fixed load error catching and order logic also minor doc/comment updates added warnings --- system/systemd.py | 109 +++++++++++++++++++++++++--------------------- 1 file changed, 59 insertions(+), 50 deletions(-) diff --git a/system/systemd.py b/system/systemd.py index 4c170916614..f8ee07d9916 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -72,14 +72,10 @@ EXAMPLES = ''' # Example action to start service httpd, if not running -- systemd: - state: started - name: httpd +- systemd: state=started name=httpd # Example action to stop service cron on debian, if running -- systemd: - name: cron - state: stopped +- systemd: name=cron state=stopped # Example action to restart service cron on centos, in all cases, also issue daemon-reload to pick up config changes - systemd: @@ -235,16 +231,15 @@ } ''' -import os -import glob from ansible.module_utils.basic import AnsibleModule -from ansible.module_utils._text import to_bytes, to_native +from ansible.module_utils.service import sysv_exists, sysv_is_enabled, fail_if_missing +from ansible.module_utils._text import to_native # =========================================== # Main control flow def main(): - # init + # initialize module = AnsibleModule( argument_spec = dict( name = dict(required=True, type='str', aliases=['unit', 'service']), @@ -258,7 +253,6 @@ def main(): required_one_of=[['state', 'enabled', 'masked', 'daemon_reload']], ) - # initialize systemctl = module.get_bin_path('systemctl') if module.params['user']: systemctl = systemctl + " --user" @@ -269,6 +263,7 @@ def main(): 'name': unit, 'changed': False, 'status': {}, + 'warnings': [], } # Run daemon-reload first, if requested @@ -277,44 +272,55 @@ def main(): if rc != 0: module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err)) - #TODO: check if service exists + # check service data (rc, out, err) = module.run_command("%s show '%s'" % (systemctl, unit)) if rc != 0: module.fail_json(msg='failure %d running systemctl show for %r: %s' % (rc, unit, err)) + found = False + is_initd = sysv_exists(unit) + is_systemd = False + # load return of systemctl show into dictionary for easy access and return - k = None multival = [] - for line in to_native(out).split('\n'): # systemd can have multiline values delimited with {} - if line.strip(): - if k is None: - if '=' in line: - k,v = line.split('=', 1) - if v.lstrip().startswith('{'): - if not v.rstrip().endswith('}'): - multival.append(line) - continue - result['status'][k] = v.strip() - k = None - else: - if line.rstrip().endswith('}'): - result['status'][k] = '\n'.join(multival).strip() - multival = [] - k = None + if out: + k = None + for line in to_native(out).split('\n'): # systemd can have multiline values delimited with {} + if line.strip(): + if k is None: + if '=' in line: + k,v = line.split('=', 1) + if v.lstrip().startswith('{'): + if not v.rstrip().endswith('}'): + multival.append(line) + continue + result['status'][k] = v.strip() + k = None else: - multival.append(line) + if line.rstrip().endswith('}'): + result['status'][k] = '\n'.join(multival).strip() + multival = [] + k = None + else: + multival.append(line) + + is_systemd = 'LoadState' in result['status'] and result['status']['LoadState'] != 'not-found' + + # Check for loading error + if is_systemd and 'LoadError' in result['status']: + module.fail_json(msg="Error loading unit file '%s': %s" % (unit, result['status']['LoadError'])) - if 'LoadState' in result['status'] and result['status']['LoadState'] == 'not-found': - module.fail_json(msg='Could not find the requested service "%r": %s' % (unit, err)) - elif 'LoadError' in result['status']: - module.fail_json(msg="Failed to get the service status '%s': %s" % (unit, result['status']['LoadError'])) + # Does service exist? + found = is_systemd or is_initd + if is_initd and not is_systemd: + result['warnings'].append('The service (%s) is actually an init script but the system is managed by systemd' % unit) - # mask/unmask the service, if requested + # mask/unmask the service, if requested, can operate on services before they are installed if module.params['masked'] is not None: - masked = (result['status']['LoadState'] == 'masked') + # state is not masked unless systemd affirms otherwise + masked = ('LoadState' in result['status'] and result['status']['LoadState'] == 'masked') - # Change? if masked != module.params['masked']: result['changed'] = True if module.params['masked']: @@ -325,10 +331,21 @@ def main(): if not module.check_mode: (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) if rc != 0: + # some versions of system CAN mask/unmask non existing services, we only fail on missing if they don't + fail_if_missing(module, found, unit, "cannot %s" % (action)) module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, err)) + # Enable/disable service startup at boot if requested if module.params['enabled'] is not None: + + if module.params['enabled']: + action = 'enable' + else: + action = 'disable' + + fail_if_missing(module, found, unit, "cannot %s" % (action)) + # do we need to enable the service? enabled = False (rc, out, err) = module.run_command("%s is-enabled '%s'" % (systemctl, unit)) @@ -337,11 +354,8 @@ def main(): if rc == 0: enabled = True elif rc == 1: - # Deals with init scripts # if both init script and unit file exist stdout should have enabled/disabled, otherwise use rc entries - initscript = '/etc/init.d/' + unit - if os.path.exists(initscript) and os.access(initscript, os.X_OK) and \ - (not out.startswith('disabled') or bool(glob.glob('/etc/rc?.d/S??' + unit))): + if is_initd and (not out.startswith('disabled') or sysv_is_enabled(unit)): enabled = True # default to current state @@ -350,19 +364,16 @@ def main(): # Change enable/disable if needed if enabled != module.params['enabled']: result['changed'] = True - if module.params['enabled']: - action = 'enable' - else: - action = 'disable' - if not module.check_mode: (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) if rc != 0: - module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, err)) + module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, out + err)) result['enabled'] = not enabled + # set service state if requested if module.params['state'] is not None: + fail_if_missing(module, found, unit, "cannot check nor set state") # default to desired state result['state'] = module.params['state'] @@ -373,17 +384,15 @@ def main(): if module.params['state'] == 'started': if result['status']['ActiveState'] != 'active': action = 'start' - result['changed'] = True elif module.params['state'] == 'stopped': if result['status']['ActiveState'] == 'active': action = 'stop' - result['changed'] = True else: action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded result['state'] = 'started' - result['changed'] = True if action: + result['changed'] = True if not module.check_mode: (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) if rc != 0: From 9724ed77defa17808d264ed339ec775def0ea419 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Fri, 18 Nov 2016 09:57:51 -0800 Subject: [PATCH 706/770] Prevent handle inheritance from blocking Windows async_wrapper (#5666) --- windows/async_wrapper.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/async_wrapper.ps1 b/windows/async_wrapper.ps1 index 6463ef3fa48..a79a6d6bb13 100644 --- a/windows/async_wrapper.ps1 +++ b/windows/async_wrapper.ps1 @@ -397,7 +397,7 @@ Function Start-Watchdog { $exec_cmd = [Ansible.Async.NativeProcessUtil]::SearchPath("powershell.exe") $exec_args = "`"$exec_cmd`" -NoProfile -ExecutionPolicy Bypass -EncodedCommand $encoded_command" - If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, [IntPtr]::Zero, [IntPtr]::Zero, $true, $pstartup_flags, [IntPtr]::Zero, $env:windir, [ref]$si, [ref]$pi)) { + If(-not [Ansible.Async.NativeProcessUtil]::CreateProcess($exec_cmd, $exec_args, [IntPtr]::Zero, [IntPtr]::Zero, $false, $pstartup_flags, [IntPtr]::Zero, $env:windir, [ref]$si, [ref]$pi)) { #throw New-Object System.ComponentModel.Win32Exception throw "create bang $([System.Runtime.InteropServices.Marshal]::GetLastWin32Error())" } From 514a2d40c179cc51d58972c02fe723d6b57d3b40 Mon Sep 17 00:00:00 2001 From: Kevin Kirsche Date: Fri, 18 Nov 2016 15:15:14 -0500 Subject: [PATCH 707/770] Update pull request template to provide context This updates the pull request template to provide more context about why specific things may be needed. This helps to make it feel like it is being asked for to help the team rather than arbitrary questions that don't seem applicable, such as command output for a docs change. --- .github/PULL_REQUEST_TEMPLATE.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 5cfd027103a..4bfadfb69e3 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -9,7 +9,11 @@ ##### ANSIBLE VERSION - + ``` ``` @@ -22,7 +26,11 @@ If you are fixing an existing issue, please include "Fixes #nnnn" in your commit message and your description; but you should still explain what the change does. --> - + ``` ``` From 7ed1af0a6d3f5e852a21aeb0b310ec71fddb2b28 Mon Sep 17 00:00:00 2001 From: zaiusdr Date: Thu, 10 Nov 2016 16:55:58 +0100 Subject: [PATCH 708/770] Fix wait_for Module to handle socket response as string in Python3 In Python3 socket module returns responses as bytes type. So it's necessary to convert it to string for the module work correctly. --- utilities/logic/wait_for.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/utilities/logic/wait_for.py b/utilities/logic/wait_for.py index 2cd99c24a1f..46be2ff04c2 100644 --- a/utilities/logic/wait_for.py +++ b/utilities/logic/wait_for.py @@ -27,6 +27,8 @@ import sys import time +from ansible.module_utils._text import to_native + HAS_PSUTIL = False try: import psutil @@ -509,7 +511,7 @@ def main(): if not response: # Server shutdown break - data += response + data += to_native(response, errors='surrogate_or_strict') if re.search(compiled_search_re, data): matched = True break From 9b09acc549c4422a8c4e93216136b8f5379ebe47 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Fri, 18 Nov 2016 15:51:12 -0500 Subject: [PATCH 709/770] try remount but fallback to unmount + mount (#2445) * allow mount to try remount falls back to unmount/mount * fixed fstab handling and switched to ismount custom function deals with bind mounts unlike built in * un ** args * last ** args --- system/mount.py | 44 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 36 insertions(+), 8 deletions(-) diff --git a/system/mount.py b/system/mount.py index ea76a0a9568..0af2a5af3fc 100644 --- a/system/mount.py +++ b/system/mount.py @@ -308,6 +308,15 @@ def unset_mount(module, args): return (args['name'], changed) +def _set_fstab_args(args): + result = [] + if 'fstab' in args and args['fstab'] != '/etc/fstab': + if get_platform().lower().endswith('bsd'): + result.append('-F') + else: + result.append('-T') + result.append(args['fstab']) + return result def mount(module, args): """Mount up a path or remount if needed.""" @@ -317,13 +326,9 @@ def mount(module, args): cmd = [mount_bin] if ismount(name): - cmd += ['-o', 'remount'] + return remount(module, mount_bin, args) - if args['fstab'] != '/etc/fstab': - if get_platform() == 'FreeBSD': - cmd += ['-F', args['fstab']] - elif get_platform() == 'Linux': - cmd += ['-T', args['fstab']] + cmd += _set_fstab_args(args) cmd += [name] @@ -348,6 +353,30 @@ def umount(module, dest): else: return rc, out+err +def remount(module, mount_bin, args): + ''' will try to use -o remount first and fallback to unmount/mount if unsupported''' + msg = '' + cmd = [mount_bin] + + # multiplatform remount opts + if get_platform().lower().endswith('bsd'): + cmd += ['-u'] + else: + cmd += ['-o', 'remount' ] + + cmd += _set_fstab_args(args) + cmd += [ args['name'], ] + try: + rc, out, err = module.run_command(cmd) + except: + rc = 1 + if rc != 0: + msg = out+err + if ismount(args['name']): + rc,msg = umount(module, args) + if rc == 0: + rc,msg = mount(module, args) + return rc, msg # Note if we wanted to put this into module_utils we'd have to get permission # from @jupeter -- https://github.com/ansible/ansible-modules-core/pull/2923 @@ -650,8 +679,7 @@ def main(): elif 'bind' in args.get('opts', []): changed = True - if is_bind_mounted( - module, linux_mounts, name, args['src'], args['fstype']): + if is_bind_mounted( module, linux_mounts, name, args['src'], args['fstype']): changed = False if changed and not module.check_mode: From 5c986306be5fa357eb64036915ab5eb98ad17327 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Fri, 18 Nov 2016 13:08:52 -0800 Subject: [PATCH 710/770] Fix for call to umount() in remount() --- system/mount.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/mount.py b/system/mount.py index 0af2a5af3fc..9e67e1f60a3 100644 --- a/system/mount.py +++ b/system/mount.py @@ -373,7 +373,7 @@ def remount(module, mount_bin, args): if rc != 0: msg = out+err if ismount(args['name']): - rc,msg = umount(module, args) + rc, msg = umount(module, args['name']) if rc == 0: rc,msg = mount(module, args) return rc, msg From ef2f130591ec7217a93933c527f50b52b68af109 Mon Sep 17 00:00:00 2001 From: tedder Date: Fri, 18 Nov 2016 14:46:10 -0800 Subject: [PATCH 711/770] update formatted lines to use named identifiers --- cloud/amazon/cloudformation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index ee4481eb61d..724bf391d70 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -257,11 +257,11 @@ def get_stack_events(cfn, stack_name): return ret for e in events.get('StackEvents', []): - eventline = 'StackEvent {0} {1} {2}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus']) + eventline = 'StackEvent {ResourceType} {LogicalResourceId} {ResourceStatus}'.format(**e) ret['events'].append(eventline) if e['ResourceStatus'].endswith('FAILED'): - failline = '{0} {1} {2}: {3}'.format(e['ResourceType'], e['LogicalResourceId'], e['ResourceStatus'], e['ResourceStatusReason']) + failline = '{ResourceType} {LogicalResourceId} {ResourceStatus}: {ResourceStatusReason}'.format(**e) ret['log'].append(failline) return ret From b3477690a3a5d5edd214e34053c4334602465fa4 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Mon, 21 Nov 2016 09:37:30 +0000 Subject: [PATCH 712/770] win_msi - Don't list choices twice (#5684) * win_msi - Dont list choices twise http://docs.ansible.com/ansible/win_msi_module.html shows Choices: True True False False As the yes/no are expanded to true/false by the docs generation * Update win_msi.py --- windows/win_msi.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/windows/win_msi.py b/windows/win_msi.py index d426c164506..78555ea90af 100644 --- a/windows/win_msi.py +++ b/windows/win_msi.py @@ -55,9 +55,7 @@ - Specify whether to wait for install or uninstall to complete before continuing. choices: - true - - yes - false - - no default: false author: "Matt Martz (@sivel)" ''' From 3acd6f45194e6248518aee6d600798c8d0d57b4d Mon Sep 17 00:00:00 2001 From: jctanner Date: Mon, 21 Nov 2016 09:05:53 -0500 Subject: [PATCH 713/770] Set b_src to abspath of b_path so that symlinks work again. (#5678) Fixes #5653 --- files/file.py | 1 + 1 file changed, 1 insertion(+) diff --git a/files/file.py b/files/file.py index 25b008598b3..b7044ca2a18 100644 --- a/files/file.py +++ b/files/file.py @@ -238,6 +238,7 @@ def main(): if follow and state == 'link': # use the current target of the link as the source src = to_native(os.path.realpath(b_path), errors='strict') + b_src = to_bytes(os.path.realpath(b_path), errors='strict') else: module.fail_json(msg='src and dest are required for creating links') From 5902bf57915e4adc600745d6c0f36d787f051c73 Mon Sep 17 00:00:00 2001 From: Rob Cutmore Date: Mon, 21 Nov 2016 09:42:15 -0500 Subject: [PATCH 714/770] Git: indicate if remote url was changed or not (#5677) --- source_control/git.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index a7cb4ae65c6..7b076576896 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -984,8 +984,7 @@ def main(): remote_url_changed = remote_url and remote_url != repo and remote_url != unfrackgitpath(repo) else: remote_url_changed = set_remote_url(git_path, module, repo, dest, remote) - if remote_url_changed: - result.update(remote_url_changed=True) + result.update(remote_url_changed=remote_url_changed) if need_fetch: if module.check_mode: From 7903b39fc0c1e7559c98b838e4da5bc157a24412 Mon Sep 17 00:00:00 2001 From: Dag Wieers Date: Fri, 18 Nov 2016 14:36:31 +0100 Subject: [PATCH 715/770] Ensure proper error when fetch_url returns status -1 When using a file:// or ftp:// URL the normal provisions that a non-200 status code means error have been disabled. But the common error status -1 from fetch_url is not properly returning an error message. This fix ensures that if the status code returns -1, we return a proper error message. This fixes #3563 --- network/basics/get_url.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/network/basics/get_url.py b/network/basics/get_url.py index d2c8da127f4..e7a5860b310 100644 --- a/network/basics/get_url.py +++ b/network/basics/get_url.py @@ -227,10 +227,14 @@ def url_get(module, url, dest, use_proxy, last_mod_time, force, timeout=10, head if info['status'] == 304: module.exit_json(url=url, dest=dest, changed=False, msg=info.get('msg', '')) - # create a temporary file and copy content to do checksum-based replacement + # Exceptions in fetch_url may result in a status -1, the ensures a proper error to the user in all cases + if info['status'] == -1: + module.fail_json(msg=info['msg'], url=url, dest=dest) + if info['status'] != 200 and not url.startswith('file:/') and not (url.startswith('ftp:/') and info.get('msg', '').startswith('OK')): module.fail_json(msg="Request failed", status_code=info['status'], response=info['msg'], url=url, dest=dest) + # create a temporary file and copy content to do checksum-based replacement if tmp_dest != '': # tmp_dest should be an existing dir tmp_dest_is_dir = os.path.isdir(tmp_dest) From 1074586ac71d8d03b6b1f488aa96ee472024b3fa Mon Sep 17 00:00:00 2001 From: Orion Poplawski Date: Mon, 21 Nov 2016 15:20:53 -0700 Subject: [PATCH 716/770] Be able to find all contents of a directory (#3711) --- files/find.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/files/find.py b/files/find.py index 2a7c6b6d824..f11a68cf584 100644 --- a/files/find.py +++ b/files/find.py @@ -66,7 +66,7 @@ required: false description: - Type of file to select - choices: [ "file", "directory", "link" ] + choices: [ "file", "directory", "link", "any" ] default: "file" recurse: required: false @@ -275,7 +275,7 @@ def main(): paths = dict(required=True, aliases=['name','path'], type='list'), patterns = dict(default=['*'], type='list', aliases=['pattern']), contains = dict(default=None, type='str'), - file_type = dict(default="file", choices=['file', 'directory', 'link'], type='str'), + file_type = dict(default="file", choices=['file', 'directory', 'link', 'any'], type='str'), age = dict(default=None, type='str'), age_stamp = dict(default="mtime", choices=['atime','mtime','ctime'], type='str'), size = dict(default=None, type='str'), @@ -337,7 +337,11 @@ def main(): continue r = {'path': fsname} - if stat.S_ISDIR(st.st_mode) and params['file_type'] == 'directory': + if params['file_type'] == 'any': + if pfilter(fsobj, params['patterns'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']): + r.update(statinfo(st)) + filelist.append(r) + elif stat.S_ISDIR(st.st_mode) and params['file_type'] == 'directory': if pfilter(fsobj, params['patterns'], params['use_regex']) and agefilter(st, now, age, params['age_stamp']): r.update(statinfo(st)) From c836818705d8963455e27ce8cc60fe656dcf5301 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 21 Nov 2016 17:21:16 -0500 Subject: [PATCH 717/770] updated desc --- files/find.py | 1 + 1 file changed, 1 insertion(+) diff --git a/files/find.py b/files/find.py index f11a68cf584..d2a5ca6570d 100644 --- a/files/find.py +++ b/files/find.py @@ -66,6 +66,7 @@ required: false description: - Type of file to select + - 'link' and 'any' were added in version 2.3 choices: [ "file", "directory", "link", "any" ] default: "file" recurse: From 551f09e6c502aba94dc9621f2090a2bed51b71c5 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Mon, 21 Nov 2016 17:47:12 -0500 Subject: [PATCH 718/770] fix yaml format, clarify docs --- files/find.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/files/find.py b/files/find.py index d2a5ca6570d..3fdf2646269 100644 --- a/files/find.py +++ b/files/find.py @@ -66,7 +66,7 @@ required: false description: - Type of file to select - - 'link' and 'any' were added in version 2.3 + - The 'link' and 'any' choices were added in version 2.3 choices: [ "file", "directory", "link", "any" ] default: "file" recurse: From 7434c14c4300cb4511f4fe4374b747ab8148d502 Mon Sep 17 00:00:00 2001 From: Ganesh Nalawade Date: Tue, 22 Nov 2016 21:28:14 +0530 Subject: [PATCH 719/770] Fix 5646 junos_config issue if config format is text (#5658) If 'src_format' is not mentioned in playbook and config is in text format a list object is passed to 'guess_format' function instead of string, hence TypeError execption is seen. Fix is to pass string object instead of list. --- network/junos/junos_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index f0814a0b56b..af0134764f8 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -242,7 +242,7 @@ def load_config(module, result): kwargs['commit'] = not module.check_mode if module.params['src']: - config_format = module.params['src_format'] or guess_format(candidate) + config_format = module.params['src_format'] or guess_format(str(candidate)) elif module.params['lines']: config_format = 'set' kwargs['config_format'] = config_format From f5658f4e5048ae6f2cf1d8eb434be3602fc6bb3a Mon Sep 17 00:00:00 2001 From: Kevin Kirsche Date: Tue, 22 Nov 2016 10:59:12 -0500 Subject: [PATCH 720/770] Remove set from junos_facts as it errors out (#5670) Fix #5636 per @ganeshnalawade --- network/junos/junos_facts.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/network/junos/junos_facts.py b/network/junos/junos_facts.py index 1e6973ddd23..d321b9f1780 100644 --- a/network/junos/junos_facts.py +++ b/network/junos/junos_facts.py @@ -44,12 +44,12 @@ - The C(config_format) argument is used to specify the desired format of the configuration file. Devices support three configuration file formats. By default, the configuration - from the device is returned as text. The other options include - set and xml. If the xml option is chosen, the configuration file - is returned as both xml and json. + from the device is returned as text. The other option xml. + If the xml option is chosen, the configuration file is + returned as both xml and json. required: false default: text - choices: ['xml', 'text', 'set'] + choices: ['xml', 'text'] requirements: - junos-eznc notes: @@ -68,10 +68,10 @@ junos_facts: config: yes -- name: collect default set of facts and configuration in set format +- name: collect default set of facts and configuration in text format junos_facts: config: yes - config_format: set + config_format: text - name: collect default set of facts and configuration in XML and JSON format junos_facts: @@ -95,7 +95,7 @@ def main(): """ spec = dict( config=dict(type='bool'), - config_format=dict(default='text', choices=['xml', 'set', 'text']), + config_format=dict(default='text', choices=['xml', 'text']), transport=dict(default='netconf', choices=['netconf']) ) @@ -116,7 +116,7 @@ def main(): config_format = module.params['config_format'] resp_config = module.config.get_config(config_format=config_format) - if config_format in ['text', 'set']: + if config_format in ['text']: facts['config'] = resp_config elif config_format == "xml": facts['config'] = xml_to_string(resp_config) From bb901bbfcc8edaa9a2da3a17d661658845e18762 Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 22 Nov 2016 11:00:01 -0500 Subject: [PATCH 721/770] vyos_command Document ANSIBLE_VYOS_TERMINAL_LENGTH (#5676) * Update documentation for vyos_command Add information on new environment variable added in #18546. Add note on command that should not be run via Ansible. * White space changes Two spaces after period. --- network/vyos/vyos_command.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/network/vyos/vyos_command.py b/network/vyos/vyos_command.py index 4c24e9e70d5..cced7b2cc5d 100644 --- a/network/vyos/vyos_command.py +++ b/network/vyos/vyos_command.py @@ -28,6 +28,10 @@ to validate key parameters before returning successfully. If the conditional statements are not met in the wait period, the task fails. + - Certain C(show) commands in VyOS produce many lines of output and + use a custom pager that can cause this module to hang. If the + value of the environment variable C(ANSIBLE_VYOS_TERMINAL_LENGTH) + is not set, the default number of 10000 is used. extends_documentation_fragment: vyos options: commands: @@ -43,15 +47,15 @@ - Specifies what to evaluate from the output of the command and what conditionals to apply. This argument will cause the task to wait for a particular conditional to be true - before moving forward. If the conditional is not true - by the configured I(retries), the task fails. See examples. + before moving forward. If the conditional is not true + by the configured I(retries), the task fails. See examples. required: false default: null aliases: ['waitfor'] match: description: - The I(match) argument is used in conjunction with the - I(wait_for) argument to specify the match policy. Valid + I(wait_for) argument to specify the match policy. Valid values are C(all) or C(any). If the value is set to C(all) then all conditionals in the wait_for must be satisfied. If the value is set to C(any) then only one of the values must be @@ -75,6 +79,10 @@ trying the command again. required: false default: 1 + +notes: + - Running C(show system boot-messages all) will cause the module to hang since + VyOS is using a custom pager setting to display the output of that command. """ EXAMPLES = """ From 99de7f02cd10dab087842246d6c038720942337e Mon Sep 17 00:00:00 2001 From: Sam Doran Date: Tue, 22 Nov 2016 11:07:21 -0500 Subject: [PATCH 722/770] Examples syntax batch7 (#5624) * Change example syntax on nxos_feature module * Change example syntax on nxos_hsrp module * Change example syntax on nxos_igmp module * Change example syntax on nxos_interface module * Change example syntax on nxos_interface_ospf module * Change example syntax on nxos_ip_interface module * Change example syntax on nxos_ping module * Change example syntax on nxos_switchport module * Change example syntax on nxos_vlan module * Change example syntax on nxos_vrf module * Change example syntax on nxos_vrf_interface module * Change example syntax on nxos_vrrp module * Change example syntax on meta module * Change example syntax on set_fact module * Change example syntax on win_copy module * Change example syntax on win_file module * Change example syntax on win_get_url module Remove escaping of \ characeter in Windows paths since it's no longer required for single quoted or unquoted values when using multi-line YAML syntax. * Change example syntax on win_lineinfile module * Change example syntax on win_msi module * Change example syntax on win_stat module * Remove nxos_bgp example from nxos_igmp module * Mark examples as regexp to avoid syntax error * Cleanup win_copy.py examples * Cleanup win_file.py examples * Remove quotes in win_get_url.py examples * Cleanup quotes and languare in win_lineinfile.py * Cleanup examples in win_group.py * Cleanup examples in win_service.py * Don't use : in documentation because it breaks the YAML syntax check * Cleanup win_copy.py examples * Cleanup win_copy.py examples * Minor change to fix test failure * Use single quotes --- network/nxos/nxos_bgp.py | 4 +- network/nxos/nxos_feature.py | 24 +++++++++--- network/nxos/nxos_hsrp.py | 33 ++++++++++++++--- network/nxos/nxos_igmp.py | 33 ++++++++--------- network/nxos/nxos_interface.py | 48 ++++++++++++++++++------ network/nxos/nxos_interface_ospf.py | 2 +- network/nxos/nxos_ip_interface.py | 21 +++++++++-- network/nxos/nxos_ping.py | 15 ++++++-- network/nxos/nxos_switchport.py | 46 ++++++++++++++++++----- network/nxos/nxos_vlan.py | 37 ++++++++++++++----- network/nxos/nxos_vrf.py | 8 +++- network/nxos/nxos_vrf_interface.py | 17 +++++++-- network/nxos/nxos_vrrp.py | 31 +++++++++++++--- utilities/helper/meta.py | 21 ++++++++--- utilities/logic/set_fact.py | 4 +- windows/win_copy.py | 21 +++++------ windows/win_file.py | 34 +++++++++++------ windows/win_get_url.py | 26 ++++++------- windows/win_group.py | 4 +- windows/win_lineinfile.py | 57 ++++++++++++++++++++--------- windows/win_msi.py | 17 ++++++--- windows/win_service.py | 4 +- windows/win_stat.py | 9 +++-- 23 files changed, 356 insertions(+), 160 deletions(-) diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index c937fb8dbe0..18ca8402577 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -286,8 +286,8 @@ EXAMPLES = ''' -# configure a simple asn -- nxos_bgp: +- name: Configure a simple ASN + nxos_bgp: asn: 65535 vrf: test router_id: 1.1.1.1 diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index bdf7980b153..a00c71a4852 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -41,12 +41,24 @@ ''' EXAMPLES = ''' -# Ensure lacp is enabled -- nxos_feature: feature=lacp state=enabled host={{ inventory_hostname }} -# Ensure ospf is disabled -- nxos_feature: feature=ospf state=disabled host={{ inventory_hostname }} -# Ensure vpc is enabled -- nxos_feature: feature=vpc state=enabled host={{ inventory_hostname }} +- 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 = ''' diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index d21fcaa6dd1..ec6e5893403 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -81,12 +81,33 @@ ''' EXAMPLES = ''' -# ensure hsrp is configured with following params on a SVI -- nxos_hsrp: group=10 vip=10.1.1.1 priority=150 interface=vlan10 preempt=enabled host=68.170.147.165 -# ensure hsrp is configured with following params on a SVI -- nxos_hsrp: group=10 vip=10.1.1.1 priority=150 interface=vlan10 preempt=enabled host=68.170.147.165 auth_type=text auth_string=CISCO -# removing hsrp config for given interface, group, and vip -- nxos_hsrp: group=10 interface=vlan10 vip=10.1.1.1 host=68.170.147.165 state=absent +- name: Ensure HSRP is configured with following params on a SVI + nxos_hsrp: + group: 10 + vip: 10.1.1.1 + priority: 150 + interface: vlan10 + preempt: enabled + host: 68.170.147.165 + +- name: Ensure HSRP is configured with following params on a SVI + nxos_hsrp: + group: 10 + vip: 10.1.1.1 + priority: 150 + interface: vlan10 + preempt: enabled + host: 68.170.147.165 + auth_type: text + auth_string: CISCO + +- name: Remove HSRP config for given interface, group, and VIP + nxos_hsrp: + group: 10 + interface: vlan10 + vip: 10.1.1.1 + host: 68.170.147.165 + state: absent ''' RETURN = ''' diff --git a/network/nxos/nxos_igmp.py b/network/nxos/nxos_igmp.py index 05827a34da0..d1ac322c947 100644 --- a/network/nxos/nxos_igmp.py +++ b/network/nxos/nxos_igmp.py @@ -61,24 +61,21 @@ choices: ['present', 'default'] ''' EXAMPLES = ''' -# default igmp global params (all params except restart) -- nxos_igmp: state=default host={{ inventory_hostname }} -# ensure the following igmp global config exists on the device -- nxos_igmp: flush_routes=true enforce_rtr_alert=true host={{ inventory_hostname }} -# restart the igmp process -- nxos_igmp: restart=true host={{ inventory_hostname }} -''' - -EXAMPLES = ''' -# configure a simple asn -- nxos_bgp: - asn=65535 - vrf=test - router_id=1.1.1.1 - state=present - username: "{{ un }}" - password: "{{ pwd }}" - host: "{{ inventory_hostname }}" +- name: Default igmp global params (all params except restart) + nxos_igmp: + state: default + host: "{{ inventory_hostname }}" + +- name: Ensure the following igmp global config exists on the device + nxos_igmp: + flush_routes: true + enforce_rtr_alert: true + host: "{{ inventory_hostname }}" + +- name: Restart the igmp process + nxos_igmp: + restart: true + host: "{{ inventory_hostname }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_interface.py b/network/nxos/nxos_interface.py index 7c1186ee924..ac1c773d0ed 100644 --- a/network/nxos/nxos_interface.py +++ b/network/nxos/nxos_interface.py @@ -85,23 +85,47 @@ ''' EXAMPLES = ''' -# Ensure an interface is a Layer 3 port and that it has the proper description -- nxos_interface: interface=Ethernet1/1 description='Configured by Ansible' mode=layer3 host=68.170.147.165 -# Admin down an interface -- nxos_interface: interface=Ethernet2/1 host=68.170.147.165 admin_state=down -# Remove all loopback interfaces -- nxos_interface: interface=loopback state=absent host=68.170.147.165 -# Remove all logical interfaces -- nxos_interface: interface_type={{ item }} state=absent host={{ inventory_hostname }} +- name Ensure an interface is a Layer 3 port and that it has the proper description + nxos_interface: + interface: Ethernet1/1 + description: 'Configured by Ansible' + mode: layer3 + host: 68.170.147.165 + +- name Admin down an interface + nxos_interface: + interface: Ethernet2/1 + host: 68.170.147.165 + admin_state: down + +- name Remove all loopback interfaces + nxos_interface: + interface: loopback + state: absent + host: 68.170.147.165 + +- name Remove all logical interfaces + nxos_interface: + interface_type: "{{ item }} " + state: absent + host: "{{ inventory_hostname }}" + with_items: - loopback - portchannel - svi - nve -# Admin up all ethernet interfaces -- nxos_interface: interface=ethernet host=68.170.147.165 admin_state=up -# Admin down ALL interfaces (physical and logical) -- nxos_interface: interface=all host=68.170.147.165 admin_state=down +- name Admin up all ethernet interfaces + nxos_interface: + interface: ethernet + host: 68.170.147.165 + admin_state: up + +- name Admin down ALL interfaces (physical and logical) + nxos_interface: + interface: all + host: 68.170.147.165 + admin_state: down ''' RETURN = ''' proposed: diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py index 5cf050e85cc..2e5a291d594 100644 --- a/network/nxos/nxos_interface_ospf.py +++ b/network/nxos/nxos_interface_ospf.py @@ -118,7 +118,7 @@ interface: ethernet1/32 ospf: 1 area: 1 - cost=default + cost: default username: "{{ un }}" password: "{{ pwd }}" host: "{{ inventory_hostname }}" diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index 25d50cbc357..416ebadd482 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -57,10 +57,23 @@ ''' EXAMPLES = ''' -# ensure ipv4 address is configured on Ethernet1/32 -- nxos_ip_interface: interface=Ethernet1/32 transport=nxapi version=v4 state=present addr=20.20.20.20 mask=24 -# ensure ipv6 address is configured on Ethernet1/31 -- nxos_ip_interface: interface=Ethernet1/31 transport=cli version=v6 state=present addr=2001::db8:800:200c:cccb mask=64 +- name: Ensure ipv4 address is configured on Ethernet1/32 + nxos_ip_interface: + interface: Ethernet1/32 + transport: nxapi + version: v4 + state: present + addr: 20.20.20.20 + mask: 24 + +- name: Ensure ipv6 address is configured on Ethernet1/31 + nxos_ip_interface: + interface: Ethernet1/31 + transport: cli + version: v6 + state: present + addr: '2001::db8:800:200c:cccb' + mask: 64 ''' RETURN = ''' diff --git a/network/nxos/nxos_ping.py b/network/nxos/nxos_ping.py index ea4ecb6826c..5764de8f127 100644 --- a/network/nxos/nxos_ping.py +++ b/network/nxos/nxos_ping.py @@ -50,10 +50,17 @@ ''' EXAMPLES = ''' -# test reachability to 8.8.8.8 using mgmt vrf -- nxos_ping: dest=8.8.8.8 vrf=management host=68.170.147.165 -# Test reachability to a few different public IPs using mgmt vrf -- nxos_ping: dest=nxos_ping vrf=management host=68.170.147.165 +- name: Test reachability to 8.8.8.8 using mgmt vrf + nxos_ping: + dest: 8.8.8.8 + vrf: management + host: 68.170.147.165 + +- name: Test reachability to a few different public IPs using mgmt vrf + nxos_ping: + dest: nxos_ping + vrf: management + host: 68.170.147.165 with_items: - 8.8.8.8 - 4.4.4.4 diff --git a/network/nxos/nxos_switchport.py b/network/nxos/nxos_switchport.py index 387c7c7cda6..006a2dcd279 100644 --- a/network/nxos/nxos_switchport.py +++ b/network/nxos/nxos_switchport.py @@ -79,16 +79,42 @@ default: null ''' EXAMPLES = ''' -# ENSURE Eth1/5 is in its default switchport state -- nxos_switchport: interface=eth1/5 state=unconfigured host={{ inventory_hostname }} -# ENSURE Eth1/5 is configured for access vlan 20 -- nxos_switchport: interface=eth1/5 mode=access access_vlan=20 host={{ inventory_hostname }} -# ENSURE Eth1/5 only has vlans 5-10 as trunk vlans -- nxos_switchport: interface=eth1/5 mode=trunk native_vlan=10 trunk_vlans=5-10 host={{ inventory_hostname }} -# Ensure eth1/5 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) -- nxos_switchport: interface=eth1/5 mode=trunk native_vlan=10 trunk_vlans=2-50 host={{ inventory_hostname }} -# Ensure these VLANs are not being tagged on the trunk -- nxos_switchport: interface=eth1/5 mode=trunk trunk_vlans=51-4094 host={{ inventory_hostname }} state=absent +- name: Ensure Eth1/5 is in its default switchport state + nxos_switchport: + interface: eth1/5 + state: unconfigured + host: "{{ inventory_hostname }}" + +- name: Ensure Eth1/5 is configured for access vlan 20 + nxos_switchport: + interface: eth1/5 + mode: access + access_vlan: 20 + host: "{{ inventory_hostname }}" + +- name: Ensure Eth1/5 only has vlans 5-10 as trunk vlans + nxos_switchport: + interface: eth1/5 + mode: trunk + native_vlan: 10 + trunk_vlans: 5-10 + host: "{{ inventory_hostname }}" + +- name: Ensure eth1/5 is a trunk port and ensure 2-50 are being tagged (doesn't mean others aren't also being tagged) + nxos_switchport: + interface: eth1/5 + mode: trunk + native_vlan: 10 + trunk_vlans: 2-50 + host: "{{ inventory_hostname }}" + +- name: Ensure these VLANs are not being tagged on the trunk + nxos_switchport: + interface: eth1/5 + mode: trunk + trunk_vlans: 51-4094 + host: "{{ inventory_hostname }} " + state: absent ''' RETURN = ''' diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index cb06bd84fbc..184d8b494e1 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -71,16 +71,33 @@ ''' EXAMPLES = ''' -# Ensure a range of VLANs are not present on the switch -- nxos_vlan: vlan_range="2-10,20,50,55-60,100-150" host=68.170.147.165 username=cisco password=cisco state=absent transport=nxapi - -# Ensure VLAN 50 exists with the name WEB and is in the shutdown state -- nxos_vlan: vlan_id=50 host=68.170.147.165 admin_state=down name=WEB transport=nxapi username=cisco password=cisco - -# Ensure VLAN is NOT on the device -- nxos_vlan: vlan_id=50 host=68.170.147.165 state=absent transport=nxapi username=cisco password=cisco - - +- name: Ensure a range of VLANs are not present on the switch + nxos_vlan: + vlan_range: "2-10,20,50,55-60,100-150" + host: 68.170.147.165 + username: cisco + password: cisco + state: absent + transport: nxapi + +- name: Ensure VLAN 50 exists with the name WEB and is in the shutdown state + nxos_vlan: + vlan_id: 50 + host: 68.170.147.165 + admin_state: down + name: WEB + transport: nxapi + username: cisco + password: cisco + +- name: Ensure VLAN is NOT on the device + nxos_vlan: + vlan_id: 50 + host: 68.170.147.165 + state: absent + transport: nxapi + username: cisco + password: cisco ''' RETURN = ''' diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index 8c8c2c17925..8dde538f993 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -76,8 +76,12 @@ ''' EXAMPLES = ''' -# ensure ntc VRF exists on switch -- nxos_vrf: vrf=ntc username="{{ un }}" password="{{ pwd }}" host="{{ inventory_hostname }}" +- name: Ensure ntc VRF exists on switch + nxos_vrf: + vrf: ntc + username: "{{ un }}" + password: "{{ pwd }}" + host: "{{ inventory_hostname }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index fe80c0f89ea..cd3c57046af 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -52,10 +52,19 @@ ''' EXAMPLES = ''' -# ensure vrf ntc exists on Eth1/1 -- nxos_vrf_interface: vrf=ntc interface=Ethernet1/1 host=68.170.147.165 state=present -# ensure ntc VRF does not exist on Eth1/1 -- nxos_vrf_interface: vrf=ntc interface=Ethernet1/1 host=68.170.147.165 state=absent +- name: Ensure vrf ntc exists on Eth1/1 + nxos_vrf_interface: + vrf: ntc + interface: Ethernet1/1 + host: 68.170.147.165 + state: present + +- name: Ensure ntc VRF does not exist on Eth1/1 + nxos_vrf_interface: + vrf: ntc + interface: Ethernet1/1 + host: 68.170.147.165 + state: absent ''' RETURN = ''' diff --git a/network/nxos/nxos_vrrp.py b/network/nxos/nxos_vrrp.py index c389a90dec4..a034b0e256c 100644 --- a/network/nxos/nxos_vrrp.py +++ b/network/nxos/nxos_vrrp.py @@ -74,12 +74,31 @@ ''' EXAMPLES = ''' -# ensure vrrp group 100 and vip 10.1.100.1 is on vlan10 -- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 host=68.170.147.165 -# ensure removal of the vrrp group config # vip is required to ensure the user knows what they are removing -- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 state=absent host=68.170.147.165 -# re-config with more params -- nxos_vrrp: interface=vlan10 group=100 vip=10.1.100.1 preempt=false priority=130 authentication=AUTHKEY host=68.170.147.165 +- name: Ensure vrrp group 100 and vip 10.1.100.1 is on vlan10 + nxos_vrrp: + interface: vlan10 + group: 100 + vip: 10.1.100.1 + host: 68.170.147.165 + +- name: Ensure removal of the vrrp group config + # vip is required to ensure the user knows what they are removing + nxos_vrrp: + interface: vlan10 + group: 100 + vip: 10.1.100.1 + state: absent + host: 68.170.147.165 + +- name: Re-config with more params + nxos_vrrp: + interface: vlan10 + group: 100 + vip: 10.1.100.1 + preempt: false + priority: 130 + authentication: AUTHKEY + host: 68.170.147.165 ''' RETURN = ''' diff --git a/utilities/helper/meta.py b/utilities/helper/meta.py index 7e2863ffddf..76bcf73996e 100644 --- a/utilities/helper/meta.py +++ b/utilities/helper/meta.py @@ -47,19 +47,28 @@ EXAMPLES = ''' # force all notified handlers to run at this point, not waiting for normal sync points -- template: src=new.j2 dest=/etc/config.txt +- template: + src: new.j2 + dest: /etc/config.txt notify: myhandler - meta: flush_handlers # reload inventory, useful with dynamic inventories when play makes changes to the existing hosts -- cloud_guest: name=newhost state=present # this is fake module -- meta: refresh_inventory +- cloud_guest: # this is fake module + name: newhost + state: present -# clear gathered facts from all currently targeted hosts -- meta: clear_facts +- name: Refresh inventory to ensure new instaces exist in inventory + meta: refresh_inventory + +- name: Clear gathered facts from all currently targeted hosts + meta: clear_facts # bring host back to play after failure -- copy: src=file dest=/etc/file +- copy: + src: file + dest: /etc/file remote_user: imightnothavepermission + - meta: clear_host_errors ''' diff --git a/utilities/logic/set_fact.py b/utilities/logic/set_fact.py index edd41119ada..881f69feffe 100644 --- a/utilities/logic/set_fact.py +++ b/utilities/logic/set_fact.py @@ -42,7 +42,9 @@ EXAMPLES = ''' # Example setting host facts using key=value pairs, note that this always creates strings or booleans -- set_fact: one_fact="something" other_fact="{{ local_var }}" +- set_fact: + one_fact: "something" + other_fact: "{{ local_var }}" # Example setting host facts using complex arguments - set_fact: diff --git a/windows/win_copy.py b/windows/win_copy.py index 4f65d6827c7..89bab5e913b 100755 --- a/windows/win_copy.py +++ b/windows/win_copy.py @@ -47,25 +47,22 @@ ''' EXAMPLES = ''' -# Copy a single file -- win_copy: src=/srv/myfiles/foo.conf dest=c:\\TEMP\\foo.conf - -# Copy the contents of files/temp_files dir into c:\temp\. Includes any sub dirs under files/temp_files -# Note the use of unix style path in the dest. -# This is necessary because \ is yaml escape sequence -- win_copy: src=files/temp_files/ dest=c:/temp/ - -# Copy the files/temp_files dir and any files or sub dirs into c:\temp -# Copies the folder because there is no trailing / on 'files/temp_files' -- win_copy: src=files/temp_files dest=c:/temp/ +- name: Copy a single file + win_copy: + src: /srv/myfiles/foo.conf + dest: c:\TEMP\foo.conf +- name: Copy files/temp_files to c:\temp + win_copy: + src: files/temp_files/ + dest: c:\temp ''' RETURN = ''' dest: description: destination file/path returned: changed type: string - sample: "c:/temp/" + sample: 'c:\temp' src: description: source file used for the copy on the target machine returned: changed diff --git a/windows/win_file.py b/windows/win_file.py index 895da567d86..989c128e3b3 100644 --- a/windows/win_file.py +++ b/windows/win_file.py @@ -46,27 +46,37 @@ If C(file), the file will NOT be created if it does not exist, see the M(copy) or M(template) module if you want that behavior. If C(absent), directories will be recursively deleted, and files will be removed. - If C(touch), an empty file will be created if the c(path) does not + If C(touch), an empty file will be created if the C(path) does not exist, while an existing file or directory will receive updated file access and - modification times (similar to the way `touch` works from the command line). + modification times (similar to the way C(touch) works from the command line). required: false default: file choices: [ file, directory, touch, absent ] ''' EXAMPLES = ''' -# create a file -- win_file: path=C:\\temp\\foo.conf +- name: Create a file + win_file: + path: C:\temp\foo.conf + state: file -# touch a file (creates if not present, updates modification time if present) -- win_file: path=C:\\temp\\foo.conf state=touch +- name: Touch a file (creates if not present, updates modification time if present) + win_file: + path: C:\temp\foo.conf + state: touch -# remove a file, if present -- win_file: path=C:\\temp\\foo.conf state=absent +- name: Remove a file, if present + win_file: + path: C:\temp\foo.conf + state: absent -# create directory structure -- win_file: path=C:\\temp\\folder\\subfolder state=directory +- name: Create directory structure + win_file: + path: C:\temp\folder\subfolder + state: directory -# remove directory structure -- win_file: path=C:\\temp state=absent +- name: Remove directory structure + win_file: + path: C:\temp + state: absent ''' diff --git a/windows/win_get_url.py b/windows/win_get_url.py index 041eb563d00..e4fe725cad1 100644 --- a/windows/win_get_url.py +++ b/windows/win_get_url.py @@ -45,9 +45,9 @@ default: null force: description: - - If C(yes), will always download the file. If C(no), will only + - If C(yes), will always download the file. If C(no), will only download the file if it does not exist or the remote file has been - modified more recently than the local file. This works by sending + modified more recently than the local file. This works by sending an http HEAD request to retrieve last modified time of the requested resource, so for this to work, the remote web server must support HEAD requests. @@ -95,22 +95,22 @@ # Playbook example - name: Download earthrise.jpg to 'C:\\Users\\RandomUser\\earthrise.jpg' win_get_url: - url: 'http://www.example.com/earthrise.jpg' - dest: 'C:\\Users\\RandomUser\\earthrise.jpg' + url: http://www.example.com/earthrise.jpg + dest: C:\Users\RandomUser\earthrise.jpg -- name: Download earthrise.jpg to 'C:\\Users\\RandomUser\\earthrise.jpg' only if modified +- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg' only if modified win_get_url: - url: 'http://www.example.com/earthrise.jpg' - dest: 'C:\\Users\\RandomUser\\earthrise.jpg' + url: http://www.example.com/earthrise.jpg + dest: C:\Users\RandomUser\earthrise.jpg force: no -- name: Download earthrise.jpg to 'C:\\Users\\RandomUser\\earthrise.jpg' through a proxy server. +- name: Download earthrise.jpg to 'C:\Users\RandomUser\earthrise.jpg' through a proxy server. win_get_url: - url: 'http://www.example.com/earthrise.jpg' - dest: 'C:\\Users\\RandomUser\\earthrise.jpg' - proxy_url: 'http://10.0.0.1:8080' - proxy_username: 'username' - proxy_password: 'password' + url: http://www.example.com/earthrise.jpg + dest: C:\Users\RandomUser\earthrise.jpg + proxy_url: http://10.0.0.1:8080 + proxy_username: username + proxy_password: password ''' RETURN = ''' url: diff --git a/windows/win_group.py b/windows/win_group.py index 5e8b0adaaf2..409065cb96a 100644 --- a/windows/win_group.py +++ b/windows/win_group.py @@ -54,13 +54,13 @@ ''' EXAMPLES = ''' - # Create a new group +- name: Create a new group win_group: name: deploy description: Deploy Group state: present - # Remove a group +- name: Remove a group win_group: name: deploy state: absent diff --git a/windows/win_lineinfile.py b/windows/win_lineinfile.py index bc378f4910c..35c478d21bc 100644 --- a/windows/win_lineinfile.py +++ b/windows/win_lineinfile.py @@ -10,11 +10,11 @@ # # 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 +# 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 . +# along with Ansible. If not, see . DOCUMENTATION = """ --- @@ -31,11 +31,11 @@ aliases: [ name, destfile ] description: - The path of the file to modify. - - Note that the Windows path delimiter '\' must be escaped as '\\' (see examples below) + - Note that the Windows path delimiter C(\) must be escaped as C(\\) when the line is double quoted. regexp: required: false description: - - "The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found will be replaced. For C(state=absent), the pattern of the line to remove. Uses .NET compatible regular expressions; see U(https://msdn.microsoft.com/en-us/library/hs600312%28v=vs.110%29.aspx)." + - "The regular expression to look for in every line of the file. For C(state=present), the pattern to replace if found; only the last line found will be replaced. For C(state=absent), the pattern of the line to remove. Uses .NET compatible regular expressions; see U(https://msdn.microsoft.com/en-us/library/hs600312%28v=vs.110%29.aspx)." state: required: false choices: [ present, absent ] @@ -58,13 +58,13 @@ default: EOF description: - Used with C(state=present). If specified, the line will be inserted after the last match of specified regular expression. A special value is available; C(EOF) for inserting the line at the end of the file. - - If specified regular expression has no matches, EOF will be used instead. May not be used with C(backrefs). + - If specified regular expression has no matches, EOF will be used instead. May not be used with C(backrefs). choices: [ 'EOF', '*regex*' ] insertbefore: required: false description: - Used with C(state=present). If specified, the line will be inserted before the last match of specified regular expression. A value is available; C(BOF) for inserting the line at the beginning of the file. - - If specified regular expression has no matches, the line will be inserted at the end of the file. May not be used with C(backrefs). + - If specified regular expression has no matches, the line will be inserted at the end of the file. May not be used with C(backrefs). choices: [ 'BOF', '*regex*' ] create: required: false @@ -81,7 +81,7 @@ validate: required: false description: - - Validation to run before copying into place. Use %s in the command to indicate the current file to validate. + - Validation to run before copying into place. Use %s in the command to indicate the current file to validate. - The command is passed securely so shell features like expansion and pipes won't work. default: None encoding: @@ -94,26 +94,49 @@ newline: required: false description: - - "Specifies the line separator style to use for the modified file. This defaults to the windows line separator (\r\n). Note that the indicated line separator will be used for file output regardless of the original line separator that appears in the input file." + - "Specifies the line separator style to use for the modified file. This defaults to the windows line separator (C(\r\n)). Note that the indicated line separator will be used for file output regardless of the original line separator that appears in the input file." choices: [ "windows", "unix" ] default: "windows" """ -EXAMPLES = """ -- win_lineinfile: dest=C:\\temp\\example.conf regexp=^name= line="name=JohnDoe" +EXAMPLES = r""" +- win_lineinfile: + dest: C:\temp\example.conf + regexp: '^name=' + line: 'name=JohnDoe' -- win_lineinfile: dest=C:\\temp\\example.conf state=absent regexp="^name=" +- win_lineinfile: + dest: C:\temp\example.conf + regexp: '^name=' + state: absent -- win_lineinfile: dest=C:\\temp\\example.conf regexp='^127\.0\.0\.1' line='127.0.0.1 localhost' +- win_lineinfile: + dest: C:\temp\example.conf + regexp: '^127\.0\.0\.1' + line: '127.0.0.1 localhost' -- win_lineinfile: dest=C:\\temp\\httpd.conf regexp="^Listen " insertafter="^#Listen " line="Listen 8080" +- win_lineinfile: + dest: C:\temp\httpd.conf + regexp: '^Listen ' + insertafter: '^#Listen ' + line: Listen 8080 -- win_lineinfile: dest=C:\\temp\\services regexp="^# port for http" insertbefore="^www.*80/tcp" line="# port for http by default" +- win_lineinfile: + dest: C:\temp\services + regexp: '^# port for http' + insertbefore: '^www.*80/tcp' + line: '# port for http by default' # Create file if it doesn't exist with a specific encoding -- win_lineinfile: dest=C:\\temp\\utf16.txt create="yes" encoding="utf-16" line="This is a utf-16 encoded file" +- win_lineinfile: + dest: C:\temp\utf16.txt + create: yes + encoding: utf-16 + line: This is a utf-16 encoded file # Add a line to a file and ensure the resulting file uses unix line separators -- win_lineinfile: dest=C:\\temp\\testfile.txt line="Line added to file" newline="unix" - +- win_lineinfile: + dest: C:\temp\testfile.txt + line: Line added to file + newline: unix """ diff --git a/windows/win_msi.py b/windows/win_msi.py index 78555ea90af..b167aadc870 100644 --- a/windows/win_msi.py +++ b/windows/win_msi.py @@ -61,13 +61,18 @@ ''' EXAMPLES = ''' -# Install an MSI file -- win_msi: path=C:\\\\7z920-x64.msi +- name: Install an MSI file + win_msi: + path: C:\7z920-x64.msi -# Install an MSI, and wait for it to complete before continuing -- win_msi: path=C:\\\\7z920-x64.msi wait=true +- name: Install an MSI, and wait for it to complete before continuing + win_msi: + path: C:\7z920-x64.msi + wait: true -# Uninstall an MSI file -- win_msi: path=C:\\\\7z920-x64.msi state=absent +- name: Uninstall an MSI file + win_msi: + path: C:\7z920-x64.msi + state: absent ''' diff --git a/windows/win_service.py b/windows/win_service.py index 1f0f6326e65..0fb9ec48652 100644 --- a/windows/win_service.py +++ b/windows/win_service.py @@ -59,12 +59,12 @@ ''' EXAMPLES = ''' - # Restart a service +- name: Restart a service win_service: name: spooler state: restarted - # Set service startup mode to auto and ensure it is started +- name: Set service startup mode to auto and ensure it is started win_service: name: spooler start_mode: auto diff --git a/windows/win_stat.py b/windows/win_stat.py index e26655985e2..f2624b45862 100644 --- a/windows/win_stat.py +++ b/windows/win_stat.py @@ -50,11 +50,12 @@ ''' EXAMPLES = ''' -# Obtain information about a file - -- win_stat: path=C:\\foo.ini +- name: Obtain information about a file + win_stat: + path: C:\foo.ini register: file_info -- debug: var=file_info +- debug: + var: file_info ''' From ff9bf54891945b90d1a1008274774b6eeeb06bf2 Mon Sep 17 00:00:00 2001 From: Andrea Tartaglia Date: Tue, 22 Nov 2016 19:26:17 +0000 Subject: [PATCH 723/770] ported iterkeys to py3 syntax (#5657) * ported iterkeys to py3 syntax Addresses ansible/ansible#18507 * Use ansible.module_utils.six.moves iterkeys instead of dict.keys() * Removed 'iterkeys' --- cloud/vmware/vsphere_guest.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index 8d60cec2523..d077803fc7d 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -19,6 +19,7 @@ # TODO: # Ability to set CPU/Memory reservations + try: import json except ImportError: @@ -986,7 +987,7 @@ def reconfigure_vm(vsphere_client, vm, module, esxi, resource_pool, cluster_name disk_num = 0 dev_changes = [] disks_changed = {} - for disk in sorted(vm_disk.iterkeys()): + for disk in sorted(vm_disk): try: disksize = int(vm_disk[disk]['size_gb']) # Convert the disk size to kilobytes @@ -1342,7 +1343,7 @@ def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest, if vm_disk: disk_num = 0 disk_key = 0 - for disk in sorted(vm_disk.iterkeys()): + for disk in sorted(vm_disk): try: datastore = vm_disk[disk]['datastore'] except KeyError: @@ -1398,7 +1399,7 @@ def create_vm(vsphere_client, module, esxi, resource_pool, cluster_name, guest, add_floppy(module, vsphere_client, config_target, config, devices, default_devs, floppy_type, floppy_image_path) if vm_nic: - for nic in sorted(vm_nic.iterkeys()): + for nic in sorted(vm_nic): try: nictype = vm_nic[nic]['type'] except KeyError: From b598611afb2f7615177fc8bf4353e077e57e7b51 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 22 Nov 2016 16:47:46 -0800 Subject: [PATCH 724/770] Support script interpreters in async_wrapper. (#5703) --- utilities/logic/async_wrapper.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index 0d8c41ac32e..c58b8ee535e 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -115,6 +115,18 @@ def _filter_non_json_lines(data): return ('\n'.join(lines), warnings) + +def _get_interpreter(module_path): + module_fd = open(module_path, 'rb') + try: + head = module_fd.read(1024) + if head[0:2] != '#!': + return None + return head[2:head.index('\n')].strip().split(' ') + finally: + module_fd.close() + + def _run_module(wrapped_cmd, jid, job_path): tmp_job_path = job_path + ".tmp" @@ -130,6 +142,11 @@ def _run_module(wrapped_cmd, jid, job_path): stderr = '' try: cmd = shlex.split(wrapped_cmd) + # call the module interpreter directly (for non-binary modules) + # this permits use of a script for an interpreter on non-Linux platforms + interpreter = _get_interpreter(cmd[0]) + if interpreter: + cmd = interpreter + cmd script = subprocess.Popen(cmd, shell=False, stdin=None, stdout=subprocess.PIPE, stderr=subprocess.PIPE) (outdata, stderr) = script.communicate() if PY3: From 351dd9344e4f9c41d47ba574f554eb16c15c2ce2 Mon Sep 17 00:00:00 2001 From: Jiri Tyr Date: Mon, 21 Nov 2016 13:55:51 +0000 Subject: [PATCH 725/770] Fall back if mountinfo reading failed (fixing #5603) --- system/mount.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/system/mount.py b/system/mount.py index 9e67e1f60a3..b94752ff520 100644 --- a/system/mount.py +++ b/system/mount.py @@ -398,7 +398,7 @@ def is_bind_mounted(module, linux_mounts, dest, src=None, fstype=None): is_mounted = False - if get_platform() == 'Linux': + if get_platform() == 'Linux' and linux_mounts is not None: if src is None: # That's for unmounted/absent if dest in linux_mounts: @@ -439,7 +439,7 @@ def get_linux_mounts(module): try: f = open(mntinfo_file) except IOError: - module.fail_json(msg="Cannot open file %s" % mntinfo_file) + return lines = map(str.strip, f.readlines()) @@ -604,6 +604,11 @@ def main(): if get_platform() == 'Linux': linux_mounts = get_linux_mounts(module) + if linux_mounts is None: + args['warnings'] = ( + 'Cannot open file /proc/self/mountinfo. ' + 'Bind mounts might be misinterpreted.') + # Override defaults with user specified params for key in ('src', 'fstype', 'passno', 'opts', 'dump', 'fstab'): if module.params[key] is not None: From aa0cad528adcdc5cd3f69cb257ae54644090eb91 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Wed, 23 Nov 2016 14:00:01 -0500 Subject: [PATCH 726/770] systemctl show rc changes across versions to avoid different errors across versions, ignore rc in favor of found/notfound fixes #5710 --- system/systemd.py | 63 ++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/system/systemd.py b/system/systemd.py index f8ee07d9916..d835b85ff13 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -272,44 +272,41 @@ def main(): if rc != 0: module.fail_json(msg='failure %d during daemon-reload: %s' % (rc, err)) - # check service data - (rc, out, err) = module.run_command("%s show '%s'" % (systemctl, unit)) - if rc != 0: - module.fail_json(msg='failure %d running systemctl show for %r: %s' % (rc, unit, err)) - found = False is_initd = sysv_exists(unit) is_systemd = False - # load return of systemctl show into dictionary for easy access and return - multival = [] - if out: - k = None - for line in to_native(out).split('\n'): # systemd can have multiline values delimited with {} - if line.strip(): - if k is None: - if '=' in line: - k,v = line.split('=', 1) - if v.lstrip().startswith('{'): - if not v.rstrip().endswith('}'): - multival.append(line) - continue - result['status'][k] = v.strip() - k = None - else: - if line.rstrip().endswith('}'): - result['status'][k] = '\n'.join(multival).strip() - multival = [] - k = None + # check service data, cannot error out on rc as it changes across versions, assume not found + (rc, out, err) = module.run_command("%s show '%s'" % (systemctl, unit)) + if rc == 0: + # load return of systemctl show into dictionary for easy access and return + multival = [] + if out: + k = None + for line in to_native(out).split('\n'): # systemd can have multiline values delimited with {} + if line.strip(): + if k is None: + if '=' in line: + k,v = line.split('=', 1) + if v.lstrip().startswith('{'): + if not v.rstrip().endswith('}'): + multival.append(line) + continue + result['status'][k] = v.strip() + k = None else: - multival.append(line) - - is_systemd = 'LoadState' in result['status'] and result['status']['LoadState'] != 'not-found' - - # Check for loading error - if is_systemd and 'LoadError' in result['status']: - module.fail_json(msg="Error loading unit file '%s': %s" % (unit, result['status']['LoadError'])) - + if line.rstrip().endswith('}'): + result['status'][k] = '\n'.join(multival).strip() + multival = [] + k = None + else: + multival.append(line) + + is_systemd = 'LoadState' in result['status'] and result['status']['LoadState'] != 'not-found' + + # Check for loading error + if is_systemd and 'LoadError' in result['status']: + module.fail_json(msg="Error loading unit file '%s': %s" % (unit, result['status']['LoadError'])) # Does service exist? found = is_systemd or is_initd From 90a92f17fae8fab2a45da3ec1762a4bd489d8b0a Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Wed, 23 Nov 2016 13:21:26 -0800 Subject: [PATCH 727/770] Force BSDs to use umount/mount instead of trying to use remount. (#5715) * Force BSDs to use umount/mount instead of trying to use remount. Fixes #5591 * Initialize out and err --- system/mount.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/system/mount.py b/system/mount.py index b94752ff520..1d7fa8ac6c8 100644 --- a/system/mount.py +++ b/system/mount.py @@ -366,16 +366,26 @@ def remount(module, mount_bin, args): cmd += _set_fstab_args(args) cmd += [ args['name'], ] + out = err = '' try: - rc, out, err = module.run_command(cmd) + if get_platform().lower().endswith('bsd'): + # Note: Forcing BSDs to do umount/mount due to BSD remount not + # working as expected (suspect bug in the BSD mount command) + # Interested contributor could rework this to use mount options on + # the CLI instead of relying on fstab + # https://github.com/ansible/ansible-modules-core/issues/5591 + rc = 1 + else: + rc, out, err = module.run_command(cmd) except: rc = 1 + if rc != 0: - msg = out+err + msg = out + err if ismount(args['name']): rc, msg = umount(module, args['name']) if rc == 0: - rc,msg = mount(module, args) + rc, msg = mount(module, args) return rc, msg # Note if we wanted to put this into module_utils we'd have to get permission From dedfe2becfaa45096fff1e35fef522a2cdf5e09a Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 24 Nov 2016 16:33:28 -0500 Subject: [PATCH 728/770] added docs for use option (cherry picked from commit ad5cb0f0d7231ff74d7918faa26091ba710c4709) --- system/service.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/system/service.py b/system/service.py index 1da88f0ef34..a71e6382804 100644 --- a/system/service.py +++ b/system/service.py @@ -74,6 +74,12 @@ description: - Additional arguments provided on the command line aliases: [ 'args' ] + use: + description: + - The service module actually uses system specific modules, normally through auto detection, this setting can force a specific module. + - Normally it uses the value of the 'ansible_service_mgr' fact and falls back to the old 'service' module when none matching is found. + default: 'auto' + version_added: 2.2 ''' EXAMPLES = ''' From 84926c1c9d7a9cdb5b4a7d84b36b4d96470d6867 Mon Sep 17 00:00:00 2001 From: jctanner Date: Mon, 28 Nov 2016 14:15:12 -0500 Subject: [PATCH 729/770] Use the the new features of fail_if_missing for checkmode (#5750) --- system/service.py | 3 ++- system/systemd.py | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/system/service.py b/system/service.py index a71e6382804..af97e7df5a4 100644 --- a/system/service.py +++ b/system/service.py @@ -131,6 +131,7 @@ import time import string import glob +from ansible.module_utils.service import fail_if_missing # The distutils module is not shipped with SUNWPython on Solaris. # It's in the SUNWPython-devel package which also contains development files @@ -493,7 +494,7 @@ def check_systemd(): self.enable_cmd = location['chkconfig'] if self.enable_cmd is None: - self.module.fail_json(msg="no service or tool found for: %s" % self.name) + fail_if_missing(self.module, False, self.name, msg='host') # If no service control tool selected yet, try to see if 'service' is available if self.svc_cmd is None and location.get('service', False): diff --git a/system/systemd.py b/system/systemd.py index d835b85ff13..19fedce4a78 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -329,8 +329,7 @@ def main(): (rc, out, err) = module.run_command("%s %s '%s'" % (systemctl, action, unit)) if rc != 0: # some versions of system CAN mask/unmask non existing services, we only fail on missing if they don't - fail_if_missing(module, found, unit, "cannot %s" % (action)) - module.fail_json(msg="Unable to %s service %s: %s" % (action, unit, err)) + fail_if_missing(module, found, unit, msg='host') # Enable/disable service startup at boot if requested @@ -341,7 +340,7 @@ def main(): else: action = 'disable' - fail_if_missing(module, found, unit, "cannot %s" % (action)) + fail_if_missing(module, found, unit, msg='host') # do we need to enable the service? enabled = False @@ -370,7 +369,7 @@ def main(): # set service state if requested if module.params['state'] is not None: - fail_if_missing(module, found, unit, "cannot check nor set state") + fail_if_missing(module, found, unit, msg="host") # default to desired state result['state'] = module.params['state'] From f98be8948bbfe6fe67cb0df1c32a7aebb711c9dc Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Mon, 28 Nov 2016 11:48:07 -0800 Subject: [PATCH 730/770] Update async_wrapper.py to remove own temp dir. (#5719) --- utilities/logic/async_wrapper.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/utilities/logic/async_wrapper.py b/utilities/logic/async_wrapper.py index c58b8ee535e..ae23d19a4a4 100644 --- a/utilities/logic/async_wrapper.py +++ b/utilities/logic/async_wrapper.py @@ -24,6 +24,7 @@ except ImportError: import simplejson as json import shlex +import shutil import os import subprocess import sys @@ -204,7 +205,8 @@ def _run_module(wrapped_cmd, jid, job_path): if len(sys.argv) < 5: print(json.dumps({ "failed" : True, - "msg" : "usage: async_wrapper . Humans, do not call directly!" + "msg" : "usage: async_wrapper [-preserve_tmp] " + "Humans, do not call directly!" })) sys.exit(1) @@ -212,6 +214,12 @@ def _run_module(wrapped_cmd, jid, job_path): time_limit = sys.argv[2] wrapped_module = sys.argv[3] argsfile = sys.argv[4] + if '-tmp-' not in os.path.dirname(wrapped_module): + preserve_tmp = True + elif len(sys.argv) > 5: + preserve_tmp = sys.argv[5] == '-preserve_tmp' + else: + preserve_tmp = False # consider underscore as no argsfile so we can support passing of additional positional parameters if argsfile != '_': cmd = "%s %s" % (wrapped_module, argsfile) @@ -244,7 +252,8 @@ def _run_module(wrapped_cmd, jid, job_path): # this probably could be done with some IPC later. Modules should always read # the argsfile at the very first start of their execution anyway notice("Return async_wrapper task started.") - print(json.dumps({ "started" : 1, "finished" : 0, "ansible_job_id" : jid, "results_file" : job_path })) + print(json.dumps({ "started" : 1, "finished" : 0, "ansible_job_id" : jid, "results_file" : job_path, + "_suppress_tmpdir_delete": not preserve_tmp})) sys.stdout.flush() time.sleep(1) sys.exit(0) @@ -276,8 +285,12 @@ def _run_module(wrapped_cmd, jid, job_path): os.killpg(sub_pid, signal.SIGKILL) notice("Sent kill to group %s"%sub_pid) time.sleep(1) + if not preserve_tmp: + shutil.rmtree(os.path.dirname(wrapped_module), True) sys.exit(0) notice("Done in kid B.") + if not preserve_tmp: + shutil.rmtree(os.path.dirname(wrapped_module), True) sys.exit(0) else: # the child process runs the actual module From c67315fc4e0c3bc5cb519ef2651cccf4bc659780 Mon Sep 17 00:00:00 2001 From: David Wittman Date: Mon, 28 Nov 2016 14:55:01 -0600 Subject: [PATCH 731/770] [git] Set IdentitiesOnly=yes when using key_file (#5682) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sets the SSH option `IdentitiesOnly=yes` in the SSH wrapper when a `key_file` is provided to the git module. This option ensures that the provided key is used. Otherwise, the system's ssh-agent could provide undesired identities when connecting. From ssh_config(5): > Specifies that ssh(1) should only use the authentication identity and > certificate files explicitly configured in the ssh_config files or > passed on the ssh(1) command-line, even if ssh-agent(1) or a > PKCS11Provider offers more identities. The argument to this keyword > must be “yes” or “no”. This option is intended for situations where > ssh-agent offers many different identities. The default is “no”. --- source_control/git.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source_control/git.py b/source_control/git.py index 7b076576896..5a4f66a6abf 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -320,7 +320,7 @@ def write_ssh_wrapper(): if [ -z "$GIT_KEY" ]; then ssh $BASEOPTS "$@" else - ssh -i "$GIT_KEY" $BASEOPTS "$@" + ssh -i "$GIT_KEY" -o IdentitiesOnly=yes $BASEOPTS "$@" fi """ fh.write(template) From 0cb32a7b304e97b5ee04d8d3fa32d2f09302d837 Mon Sep 17 00:00:00 2001 From: ekultails Date: Tue, 29 Nov 2016 08:54:05 -0500 Subject: [PATCH 732/770] add correct SELinux file context for crontabs (#4511) (#4595) --- system/cron.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/system/cron.py b/system/cron.py index 9dd5b5c9b3c..b747a8cf32a 100644 --- a/system/cron.py +++ b/system/cron.py @@ -233,6 +233,12 @@ import platform import pipes +try: + import selinux + HAS_SELINUX = True +except ImportError: + HAS_SELINUX = False + CRONCMD = "/usr/bin/crontab" class CronTabError(Exception): @@ -334,6 +340,10 @@ def write(self, backup_file=None): if rc != 0: self.module.fail_json(msg=err) + # set SELinux permissions + if HAS_SELINUX: + selinux.selinux_lsetfilecon_default(self.cron_file) + def do_comment(self, name): return "%s%s" % (self.ansible, name) From 30fb384e7fb9a94ac3929e4a650877e45d8834c9 Mon Sep 17 00:00:00 2001 From: Wouter Oosterveld Date: Tue, 29 Nov 2016 20:15:17 +0100 Subject: [PATCH 733/770] mysql_user: fix user_mod on MySQL(-like) 5.7+ (Fixes #3003) (#5388) --- database/mysql/mysql_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index 87e8318adc7..f3231c2f93e 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -339,7 +339,7 @@ def user_mod(cursor, user, host, host_all, password, encrypted, new_priv, append if old_user_mgmt: cursor.execute("SET PASSWORD FOR %s@%s = PASSWORD(%s)", (user, host, password)) else: - cursor.execute("ALTER USER %s@%s IDENTIFIED BY %s", (user, host, password)) + cursor.execute("ALTER USER %s@%s IDENTIFIED WITH mysql_native_password BY %s", (user, host, password)) changed = True # Handle privileges From 6890003c4f8dcd8f63a5b280211068cfaf1dcc35 Mon Sep 17 00:00:00 2001 From: Matt Davis Date: Tue, 29 Nov 2016 14:19:51 -0800 Subject: [PATCH 734/770] Fix test failures in win_command/win_shell on Powershell 3 Switched to use Exit-Json to avoid JSON escaping bugs in ConvertTo-Json formatter without -Compress option. --- windows/win_command.ps1 | 2 +- windows/win_shell.ps1 | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/windows/win_command.ps1 b/windows/win_command.ps1 index 2a03b6a03d5..316654b81d7 100644 --- a/windows/win_command.ps1 +++ b/windows/win_command.ps1 @@ -158,4 +158,4 @@ $result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") $result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") $result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff") -ConvertTo-Json -Depth 99 $result +Exit-Json $result diff --git a/windows/win_shell.ps1 b/windows/win_shell.ps1 index 850f2b9561a..664858e52ca 100644 --- a/windows/win_shell.ps1 +++ b/windows/win_shell.ps1 @@ -139,4 +139,4 @@ $result.start = $start_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") $result.end = $end_datetime.ToString("yyyy-MM-dd hh:mm:ss.ffffff") $result.delta = $($end_datetime - $start_datetime).ToString("h\:mm\:ss\.ffffff") -ConvertTo-Json -Depth 99 $result +Exit-Json $result From 286021056f9700ab58fae92d03d9555300aef580 Mon Sep 17 00:00:00 2001 From: "Dustin C. Hatch" Date: Wed, 30 Nov 2016 10:02:08 -0600 Subject: [PATCH 735/770] systemd: Start inactive units for reload/restart The `service` module starts services that are not running when `action=restarted` or `action=reloaded`, which is especially convenient for initial deployments because it eliminates an extraneous operation for when the service starts for the first time. This commit adjusts the behavior of the `systemd` module to match. --- system/systemd.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/system/systemd.py b/system/systemd.py index 19fedce4a78..dcc493b812b 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -384,7 +384,10 @@ def main(): if result['status']['ActiveState'] == 'active': action = 'stop' else: - action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded + if result['status']['ActiveState'] != 'active': + action = 'start' + else: + action = module.params['state'][:-2] # remove 'ed' from restarted/reloaded result['state'] = 'started' if action: From f64e80118383e0c3885b3bd84c4c1379ce7e4e5d Mon Sep 17 00:00:00 2001 From: Rob White Date: Wed, 30 Nov 2016 11:58:58 +1100 Subject: [PATCH 736/770] Fail if specifying an ENI as device but in_vpc is not true --- cloud/amazon/ec2_eip.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cloud/amazon/ec2_eip.py b/cloud/amazon/ec2_eip.py index 4fb7c2ae56f..834730448ef 100644 --- a/cloud/amazon/ec2_eip.py +++ b/cloud/amazon/ec2_eip.py @@ -376,6 +376,8 @@ def main(): if device_id and device_id.startswith('i-'): is_instance = True elif device_id: + if device_id.startswith('eni-') and not in_vpc: + module.fail_json(msg="If you are specifying an ENI, in_vpc must be true") is_instance = False try: From 7616725ac821eea33e1d7e3908ea338e0f42e880 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Moser?= Date: Thu, 1 Dec 2016 11:48:40 +0100 Subject: [PATCH 737/770] doc: network: fix syntax errors in examples (#5780) --- network/eos/eos_eapi.py | 6 +++--- network/nxos/nxos_aaa_server_host.py | 10 +++++----- network/nxos/nxos_igmp_snooping.py | 4 ++-- network/nxos/nxos_mtu.py | 24 ++++++++++++------------ network/nxos/nxos_ntp.py | 6 +++--- network/nxos/nxos_ntp_auth.py | 6 +++--- network/nxos/nxos_ntp_options.py | 6 +++--- network/nxos/nxos_nxapi.py | 6 +++--- network/nxos/nxos_pim_interface.py | 24 ++++++++++++------------ network/nxos/nxos_snmp_community.py | 6 +++--- network/nxos/nxos_snmp_contact.py | 6 +++--- network/nxos/nxos_snmp_host.py | 6 +++--- network/nxos/nxos_snmp_location.py | 12 ++++++------ network/nxos/nxos_snmp_traps.py | 12 ++++++------ network/nxos/nxos_snmp_user.py | 6 +++--- network/nxos/nxos_udld.py | 12 ++++++------ network/nxos/nxos_udld_interface.py | 18 +++++++++--------- network/nxos/nxos_vtp_domain.py | 6 +++--- network/nxos/nxos_vtp_password.py | 12 ++++++------ network/nxos/nxos_vtp_version.py | 6 +++--- network/sros/sros_config.py | 2 +- 21 files changed, 98 insertions(+), 98 deletions(-) diff --git a/network/eos/eos_eapi.py b/network/eos/eos_eapi.py index 8087a8d0da6..96ad4ee576b 100644 --- a/network/eos/eos_eapi.py +++ b/network/eos/eos_eapi.py @@ -148,7 +148,7 @@ - name: Enable eAPI access with default configuration eos_eapi: state: started - provider: {{ cli }} + provider: "{{ cli }}" - name: Enable eAPI with no HTTP, HTTPS at port 9443, local HTTP at port 80, and socket enabled eos_eapi: @@ -158,12 +158,12 @@ local_http: yes local_http_port: 80 socket: yes - provider: {{ cli }} + provider: "{{ cli }}" - name: Shutdown eAPI access eos_eapi: state: stopped - provider: {{ cli }} + provider: "{{ cli }}" """ RETURN = """ diff --git a/network/nxos/nxos_aaa_server_host.py b/network/nxos/nxos_aaa_server_host.py index 28f24f86adf..9cfcd004bbc 100644 --- a/network/nxos/nxos_aaa_server_host.py +++ b/network/nxos/nxos_aaa_server_host.py @@ -88,9 +88,9 @@ address: 1.2.3.4 acct_port: 2084 host_timeout: 10 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # Radius Server Host Key Configuration - name: "Radius Server Host Key Configuration" @@ -101,8 +101,8 @@ key: hello encrypt_type: 7 host: inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + username: "{{ un }}" + password: "{{ pwd }}" # TACACS Server Host Configuration - name: "Tacacs Server Host Configuration" diff --git a/network/nxos/nxos_igmp_snooping.py b/network/nxos/nxos_igmp_snooping.py index 8b8ad6620af..c1ee7c9b13d 100644 --- a/network/nxos/nxos_igmp_snooping.py +++ b/network/nxos/nxos_igmp_snooping.py @@ -86,8 +86,8 @@ report_supp: true v3_report_supp: true host: "{{ inventory_hostname }}" - username: {{ un }} - password: {{ pwd }} + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_mtu.py b/network/nxos/nxos_mtu.py index 26fccca6153..58942a277b2 100644 --- a/network/nxos/nxos_mtu.py +++ b/network/nxos/nxos_mtu.py @@ -57,33 +57,33 @@ # Ensure system mtu is 9126 - nxos_mtu: sysmtu: 9216 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # Config mtu on Eth1/1 (routed interface) - nxos_mtu: interface: Ethernet1/1 mtu: 1600 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # Config mtu on Eth1/3 (switched interface) - nxos_mtu: interface: Ethernet1/3 mtu: 9216 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # Unconfigure mtu on a given interface - nxos_mtu: interface: Ethernet1/3 mtu: 9216 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" state: absent ''' diff --git a/network/nxos/nxos_ntp.py b/network/nxos/nxos_ntp.py index 18cbe2b1a7c..f99f1876a6e 100644 --- a/network/nxos/nxos_ntp.py +++ b/network/nxos/nxos_ntp.py @@ -81,9 +81,9 @@ server: 1.2.3.4 key_id: 32 prefer: enabled - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py index 9790389b9c9..e6652e1bfb7 100644 --- a/network/nxos/nxos_ntp_auth.py +++ b/network/nxos/nxos_ntp_auth.py @@ -77,9 +77,9 @@ key_id: 32 md5string: hello auth_type: text - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py index fbc1cd5745b..26a28523d61 100644 --- a/network/nxos/nxos_ntp_options.py +++ b/network/nxos/nxos_ntp_options.py @@ -66,9 +66,9 @@ master: true stratum: 12 logging: false - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_nxapi.py b/network/nxos/nxos_nxapi.py index 3324130b178..f8144aba451 100644 --- a/network/nxos/nxos_nxapi.py +++ b/network/nxos/nxos_nxapi.py @@ -112,7 +112,7 @@ - name: Enable NXAPI access with default configuration nxos_nxapi: - provider: {{ cli }} + provider: "{{ cli }}" - name: Enable NXAPI with no HTTP, HTTPS at port 9443 and sandbox disabled nxos_nxapi: @@ -120,12 +120,12 @@ https_port: 9443 https: yes enable_sandbox: no - provider: {{ cli }} + provider: "{{ cli }}" - name: remove NXAPI configuration nxos_nxapi: state: absent - provider: {{ cli }} + provider: "{{ cli }}" """ RETURN = """ diff --git a/network/nxos/nxos_pim_interface.py b/network/nxos/nxos_pim_interface.py index 9556b03ab65..6a627565526 100644 --- a/network/nxos/nxos_pim_interface.py +++ b/network/nxos/nxos_pim_interface.py @@ -110,9 +110,9 @@ - nxos_pim_interface: interface: eth1/33 state: absent - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # ensure the interface has pim-sm enabled with the appropriate priority and hello interval - nxos_pim_interface: @@ -120,9 +120,9 @@ dr_prio: 10 hello_interval: 40 state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # ensure join-prune policies exist - nxos_pim_interface: @@ -131,17 +131,17 @@ jp_policy_out: JPOUT jp_type_in: routemap jp_type_out: routemap - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + 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 }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_community.py b/network/nxos/nxos_snmp_community.py index 5abc428fb8e..7ffe7b46e58 100644 --- a/network/nxos/nxos_snmp_community.py +++ b/network/nxos/nxos_snmp_community.py @@ -62,9 +62,9 @@ community: TESTING7 group: network-operator state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index aaaeab4fb76..e2d98733687 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -47,9 +47,9 @@ - nxos_snmp_contact: contact: Test state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_host.py b/network/nxos/nxos_snmp_host.py index 92f050fe5a7..8717bb7e3d8 100644 --- a/network/nxos/nxos_snmp_host.py +++ b/network/nxos/nxos_snmp_host.py @@ -87,9 +87,9 @@ snmp_host: 3.3.3.3 community: TESTING state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py index 29d5cf576ed..25acf5141e3 100644 --- a/network/nxos/nxos_snmp_location.py +++ b/network/nxos/nxos_snmp_location.py @@ -46,17 +46,17 @@ - nxos_snmp_location: location: Test state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # ensure snmp location is not configured - nxos_snmp_location: location: Test state: absent - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py index 63f1a3e3e78..a615c492c0f 100644 --- a/network/nxos/nxos_snmp_traps.py +++ b/network/nxos/nxos_snmp_traps.py @@ -54,17 +54,17 @@ - nxos_snmp_traps: group: lldp state: enabled - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # ensure lldp trap is not configured - nxos_snmp_traps: group: lldp state: disabled - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_snmp_user.py b/network/nxos/nxos_snmp_user.py index d3add9b7614..56d515a8ffd 100644 --- a/network/nxos/nxos_snmp_user.py +++ b/network/nxos/nxos_snmp_user.py @@ -73,9 +73,9 @@ group: network-operator auth: md5 pwd: test_password - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_udld.py b/network/nxos/nxos_udld.py index db53461f921..7f8cedeca91 100644 --- a/network/nxos/nxos_udld.py +++ b/network/nxos/nxos_udld.py @@ -62,17 +62,17 @@ - nxos_udld: aggressive: disabled msg_time: 20 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # Ensure agg mode is globally enabled and msg time is 15 - nxos_udld: aggressive: enabled msg_time: 15 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_udld_interface.py b/network/nxos/nxos_udld_interface.py index e061dde4651..66473495401 100644 --- a/network/nxos/nxos_udld_interface.py +++ b/network/nxos/nxos_udld_interface.py @@ -51,26 +51,26 @@ interface: Ethernet1/1 mode: aggressive state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # Remove the aggressive config only if it's currently in aggressive mode and then disable udld (switch default) - nxos_udld_interface: interface: Ethernet1/1 mode: aggressive state: absent - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # ensure Ethernet1/1 has aggressive mode enabled - nxos_udld_interface: interface: Ethernet1/1 mode: enabled - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 54704913cd1..93f62318e22 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -45,9 +45,9 @@ # ENSURE VTP DOMAIN IS CONFIGURED - nxos_vtp_domain: domain: ntc - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index fee217a8ff0..aa3cb13d6f6 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -56,17 +56,17 @@ - nxos_vtp_password: password: ntc state: present - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" # ENSURE VTP PASSWORD IS REMOVED - nxos_vtp_password: password: ntc state: absent - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/nxos/nxos_vtp_version.py b/network/nxos/nxos_vtp_version.py index 90fad190160..5ce455d81c9 100644 --- a/network/nxos/nxos_vtp_version.py +++ b/network/nxos/nxos_vtp_version.py @@ -43,9 +43,9 @@ # ENSURE VTP VERSION IS 2 - nxos_vtp_version: version: 2 - host: {{ inventory_hostname }} - username: {{ un }} - password: {{ pwd }} + host: "{{ inventory_hostname }}" + username: "{{ un }}" + password: "{{ pwd }}" ''' RETURN = ''' diff --git a/network/sros/sros_config.py b/network/sros/sros_config.py index f1fd84a8bb2..27c62fd5362 100644 --- a/network/sros/sros_config.py +++ b/network/sros/sros_config.py @@ -186,7 +186,7 @@ - name: load config from file sros_config: - src: {{ inventory_hostname }}.cfg + src: "{{ inventory_hostname }}.cfg" provider: "{{ cli }}" save: yes """ From c72bc670131ddf3b5723b549ff045fde4ec0790d Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:13:00 +0000 Subject: [PATCH 738/770] Use native YAML (#5580) From 2e6e2e688f820baa5c9861551281be952aa483ea Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:13:41 +0000 Subject: [PATCH 739/770] Use native YAML (#5579) From eb845781cb6e45d11cadef87fc51fa9b5f9235cb Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:13:59 +0000 Subject: [PATCH 740/770] Use native YAML (#5578) From 519e21075715f63271bde4c8f094c857d48c7b66 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Thu, 1 Dec 2016 11:14:32 +0000 Subject: [PATCH 741/770] Use native YAML (#5575) From 30b5b3accb093ea9ce1da3c6a0ca15eaa24c9304 Mon Sep 17 00:00:00 2001 From: John R Barker Date: Thu, 1 Dec 2016 17:05:26 +0000 Subject: [PATCH 742/770] Missing space between key:value (#5785) --- network/vyos/vyos_facts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/network/vyos/vyos_facts.py b/network/vyos/vyos_facts.py index 3b0e5558538..8e068484ff7 100644 --- a/network/vyos/vyos_facts.py +++ b/network/vyos/vyos_facts.py @@ -57,7 +57,7 @@ - name: collect only the config and default facts vyos_facts: - gather_subset:config + gather_subset: config - name: collect everything exception the config vyos_facts: From edcbc2410ab6b90b79f809fc91d4d9ea7f57bb2b Mon Sep 17 00:00:00 2001 From: Ted Timmons Date: Thu, 1 Dec 2016 10:30:26 -0800 Subject: [PATCH 743/770] use 'six' for urlparse compatability (#5777) * use 'six' for urlparse compatability --- cloud/amazon/s3.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/amazon/s3.py b/cloud/amazon/s3.py index bfb4627ffa3..fad1d6d6aa4 100755 --- a/cloud/amazon/s3.py +++ b/cloud/amazon/s3.py @@ -248,7 +248,7 @@ ''' import os -import urlparse +from ansible.module_utils.six.moves.urllib.parse import urlparse from ssl import SSLError try: From 421e1197e10b9382611845947774b22192beb009 Mon Sep 17 00:00:00 2001 From: Brian Coca Date: Thu, 1 Dec 2016 18:48:47 -0500 Subject: [PATCH 744/770] made fact_path actual path (#5789) --- system/setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/setup.py b/system/setup.py index b0ad9c0aa45..ac06b478b4b 100644 --- a/system/setup.py +++ b/system/setup.py @@ -119,7 +119,7 @@ def main(): gather_subset=dict(default=["all"], required=False, type='list'), gather_timeout=dict(default=10, required=False, type='int'), filter=dict(default="*", required=False), - fact_path=dict(default='/etc/ansible/facts.d', required=False), + fact_path=dict(default='/etc/ansible/facts.d', required=False, type='path'), ), supports_check_mode = True, ) From b58312f08f08628d0aeb80bb1696c249013fa440 Mon Sep 17 00:00:00 2001 From: Asara Date: Fri, 2 Dec 2016 03:44:10 -0500 Subject: [PATCH 745/770] Fixed typo in docker_image.py docs (#5764) --- cloud/docker/docker_image.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cloud/docker/docker_image.py b/cloud/docker/docker_image.py index a7450b0b23a..df9df1dd2c5 100644 --- a/cloud/docker/docker_image.py +++ b/cloud/docker/docker_image.py @@ -206,7 +206,7 @@ name: registry.ansible.com/chouseknecht/sinatra tag: v1 -- name: Build an image ad push it to a private repo +- name: Build an image and push it to a private repo docker_image: path: ./sinatra name: registry.ansible.com/chouseknecht/sinatra From cdb6a61f0da4a67e58704ab1ccdb6e1d0877148a Mon Sep 17 00:00:00 2001 From: Evan Kaufman Date: Fri, 2 Dec 2016 00:49:48 -0800 Subject: [PATCH 746/770] Fix for python 2.4 compatibility (#5693) Fixes #5692 --- system/cron.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/system/cron.py b/system/cron.py index b747a8cf32a..34307c6b872 100644 --- a/system/cron.py +++ b/system/cron.py @@ -727,8 +727,9 @@ def main(): changed = True # no changes to env/job, but existing crontab needs a terminating newline - if not changed and not crontab.existing.endswith(('\r', '\n')): - changed = True + if not changed: + if not (crontab.existing.endswith('\r') or crontab.existing.endswith('\n')): + changed = True res_args = dict( jobs = crontab.get_jobnames(), From ed7ec6aa86425918f9bbfec50aa7b8212701a689 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:27:26 +0000 Subject: [PATCH 747/770] Unquote urls in YAML - network (#5792) --- network/basics/uri.py | 2 +- network/cumulus/cl_img_install.py | 6 +++--- network/cumulus/cl_license.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/network/basics/uri.py b/network/basics/uri.py index c66ce399c9c..0c1e9b673f2 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -155,7 +155,7 @@ EXAMPLES = ''' - name: Check that you can connect (GET) to a page and it returns a status 200 uri: - url: 'http://www.example.com' + url: http://www.example.com # Check that a page returns a status 200 and fail if the word AWESOME is not # in the page contents. diff --git a/network/cumulus/cl_img_install.py b/network/cumulus/cl_img_install.py index cfd5f4548a0..c5d7c45ffef 100644 --- a/network/cumulus/cl_img_install.py +++ b/network/cumulus/cl_img_install.py @@ -62,7 +62,7 @@ - name: Install image using using http url. Switch slots so the subsequent will load the new version cl_img_install: version: 2.0.1 - src: 'http://10.1.1.1/CumulusLinux-2.0.1.bin' + src: http://10.1.1.1/CumulusLinux-2.0.1.bin switch_slot: yes ## Copy the software from the ansible server to the switch. @@ -72,7 +72,7 @@ - name: Download cumulus linux to local system get_url: - src: 'ftp://cumuluslinux.bin' + src: ftp://cumuluslinux.bin dest: /root/CumulusLinux-2.0.1.bin - name: Install image from local filesystem. Get version from the filename. @@ -85,7 +85,7 @@ - name: Download cumulus linux to local system get_url: - src: 'ftp://CumulusLinux-2.0.1.bin' + src: ftp://CumulusLinux-2.0.1.bin dest: /root/image.bin - name: install image and switch slots. only reboot needed diff --git a/network/cumulus/cl_license.py b/network/cumulus/cl_license.py index 8097cda5f56..37f20b7cb94 100644 --- a/network/cumulus/cl_license.py +++ b/network/cumulus/cl_license.py @@ -61,7 +61,7 @@ tasks: - name: install license using http url cl_license: - src: 'http://10.1.1.1/license.txt' + src: http://10.1.1.1/license.txt notify: restart switchd - name: Triggers switchd to be restarted right away, before play, or role From fbe10919ac53e093ea8f9e096b6d905cf21012c7 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:27:54 +0000 Subject: [PATCH 748/770] Unquote urls in YAML - packaging (#5793) --- packaging/language/pip.py | 8 ++++---- packaging/os/apt_repository.py | 8 ++++---- packaging/os/rhn_channel.py | 2 +- packaging/os/rhn_register.py | 2 +- packaging/os/rpm_key.py | 2 +- packaging/os/yum.py | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/packaging/language/pip.py b/packaging/language/pip.py index e49e275036a..aa78ead6e59 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -152,17 +152,17 @@ # Install (MyApp) using one of the remote protocols (bzr+,hg+,git+,svn+). You do not have to supply '-e' option in extra_args. - pip: - name: 'svn+http://myrepo/svn/MyApp#' + name: svn+http://myrepo/svn/MyApp# egg: MyApp' # Install MyApp using one of the remote protocols (bzr+,hg+,git+) in a non editable way. - pip: - name: 'git+http://myrepo/app/MyApp' + name: git+http://myrepo/app/MyApp editable: false # Install (MyApp) from local tarball - pip: - name: 'file:///path/to/MyApp.tar.gz' + name: file:///path/to/MyApp.tar.gz # Install (Bottle) into the specified (virtualenv), inheriting none of the globally installed modules - pip: @@ -193,7 +193,7 @@ # Install specified python requirements and custom Index URL. - pip: requirements: /my_app/requirements.txt - extra_args: '-i https://example.com/pypi/simple' + extra_args: -i https://example.com/pypi/simple # Install (Bottle) for Python 3.3 specifically,using the 'pip-3.3' executable. - pip: diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index aedf1cb2fba..fc33021e519 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -85,23 +85,23 @@ EXAMPLES = ''' # Add specified repository into sources list. - apt_repository: - repo: 'deb http://archive.canonical.com/ubuntu hardy partner' + repo: deb http://archive.canonical.com/ubuntu hardy partner state: present # Add specified repository into sources list using specified filename. - apt_repository: - repo: 'deb http://dl.google.com/linux/chrome/deb/ stable main' + repo: deb http://dl.google.com/linux/chrome/deb/ stable main state: present filename: 'google-chrome' # Add source repository into sources list. - apt_repository: - repo: 'deb-src http://archive.canonical.com/ubuntu hardy partner' + repo: deb-src http://archive.canonical.com/ubuntu hardy partner state: present # Remove specified repository from sources list. - apt_repository: - repo: 'deb http://archive.canonical.com/ubuntu hardy partner' + repo: deb http://archive.canonical.com/ubuntu hardy partner state: absent # Add nginx stable repository from PPA and install its signing key. diff --git a/packaging/os/rhn_channel.py b/packaging/os/rhn_channel.py index 9ec24483ef7..7a7735b84b5 100644 --- a/packaging/os/rhn_channel.py +++ b/packaging/os/rhn_channel.py @@ -63,7 +63,7 @@ - rhn_channel: name: rhel-x86_64-server-v2vwin-6 sysname: server01 - url: 'https://rhn.redhat.com/rpc/api' + url: https://rhn.redhat.com/rpc/api user: rhnuser password: guessme ''' diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index 1a99fed8273..cac93420fe5 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -112,7 +112,7 @@ state: present username: joe_user password: somepass' - server_url: 'https://xmlrpc.my.satellite/XMLRPC' + server_url: https://xmlrpc.my.satellite/XMLRPC # Register as user (joe_user) with password (somepass) and enable # channels (rhel-x86_64-server-6-foo-1) and (rhel-x86_64-server-6-bar-1). diff --git a/packaging/os/rpm_key.py b/packaging/os/rpm_key.py index 19f364192f7..42d75670a53 100644 --- a/packaging/os/rpm_key.py +++ b/packaging/os/rpm_key.py @@ -54,7 +54,7 @@ # Example action to import a key from a url - rpm_key: state: present - key: 'http://apt.sw.be/RPM-GPG-KEY.dag.txt' + key: http://apt.sw.be/RPM-GPG-KEY.dag.txt # Example action to import a key from a file - rpm_key: diff --git a/packaging/os/yum.py b/packaging/os/yum.py index cc9b004033a..0714303acbf 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -183,7 +183,7 @@ - name: install the nginx rpm from a remote repo yum: - name: 'http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm' + name: http://nginx.org/packages/centos/6/noarch/RPMS/nginx-release-centos-6-0.el6.ngx.noarch.rpm state: present - name: install nginx rpm from a local file From 9d1aff43bbaf99d9bab1b03e21b7b5745e40830e Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:29:54 +0000 Subject: [PATCH 749/770] Unquote urls in YAML - web_infrastructure (#5794) --- web_infrastructure/supervisorctl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web_infrastructure/supervisorctl.py b/web_infrastructure/supervisorctl.py index 97cd3a0c637..35d40928ca4 100644 --- a/web_infrastructure/supervisorctl.py +++ b/web_infrastructure/supervisorctl.py @@ -104,7 +104,7 @@ state: restarted username: test password: testpass - server_url: 'http://localhost:9001' + server_url: http://localhost:9001 ''' From 345dd32d1058af98414b9bb1a645f8a6a5e6b92e Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:34:33 +0000 Subject: [PATCH 750/770] Unquote urls in YAML - source_control (#5795) --- source_control/git.py | 10 +++++----- source_control/hg.py | 4 ++-- source_control/subversion.py | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/source_control/git.py b/source_control/git.py index 5a4f66a6abf..d8307023d23 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -195,32 +195,32 @@ EXAMPLES = ''' # Example git checkout from Ansible Playbooks - git: - repo: 'git://foosball.example.org/path/to/repo.git' + repo: git://foosball.example.org/path/to/repo.git dest: /srv/checkout version: release-0.22 # Example read-write git checkout from github - git: - repo: 'ssh://git@github.com/mylogin/hello.git' + repo: ssh://git@github.com/mylogin/hello.git dest: /home/mylogin/hello # Example just ensuring the repo checkout exists - git: - repo: 'git://foosball.example.org/path/to/repo.git' + repo: git://foosball.example.org/path/to/repo.git dest: /srv/checkout update: no # Example just get information about the repository whether or not it has # already been cloned locally. - git: - repo: 'git://foosball.example.org/path/to/repo.git' + repo: git://foosball.example.org/path/to/repo.git dest: /srv/checkout clone: no update: no # Example checkout a github repo and use refspec to fetch all pull requests - git: - repo: 'https://github.com/ansible/ansible-examples.git' + repo: https://github.com/ansible/ansible-examples.git dest: /src/ansible-examples refspec: '+refs/pull/*:refs/heads/*' ''' diff --git a/source_control/hg.py b/source_control/hg.py index effc8a7c8c9..5874857ad19 100644 --- a/source_control/hg.py +++ b/source_control/hg.py @@ -96,7 +96,7 @@ EXAMPLES = ''' # Ensure the current working copy is inside the stable branch and deletes untracked files if any. - hg: - repo: 'https://bitbucket.org/user/repo1' + repo: https://bitbucket.org/user/repo1 dest: /home/user/repo1 revision: stable purge: yes @@ -104,7 +104,7 @@ # Example just get information about the repository whether or not it has # already been cloned locally. - hg: - repo: 'git://bitbucket.org/user/repo' + repo: git://bitbucket.org/user/repo dest: /srv/checkout clone: no update: no diff --git a/source_control/subversion.py b/source_control/subversion.py index 49a0e22fd77..1049a3e6a56 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -105,18 +105,18 @@ EXAMPLES = ''' # Checkout subversion repository to specified folder. - subversion: - repo: 'svn+ssh://an.example.org/path/to/repo' + repo: svn+ssh://an.example.org/path/to/repo dest: /src/checkout # Export subversion directory to folder - subversion: - repo: 'svn+ssh://an.example.org/path/to/repo' + repo: svn+ssh://an.example.org/path/to/repo dest: /src/export # Example just get information about the repository whether or not it has # already been cloned locally. - subversion: - repo: 'svn+ssh://an.example.org/path/to/repo' + repo: svn+ssh://an.example.org/path/to/repo dest: /srv/checkout checkout: no update: no From cd7a9f0498d0631463d89059eff0b841eb9f96c1 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:35:20 +0000 Subject: [PATCH 751/770] Unquote urls in YAML - system (#5796) --- system/authorized_key.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/authorized_key.py b/system/authorized_key.py index 6cfd856e9a3..7ac85eae0de 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -99,7 +99,7 @@ # Using github url as key source - authorized_key: user: charlie - key: 'https://github.com/charlie.keys' + key: https://github.com/charlie.keys # Using alternate directory locations: - authorized_key: @@ -126,7 +126,7 @@ # Using validate_certs: - authorized_key: user: charlie - key: 'https://github.com/user.keys' + key: https://github.com/user.keys validate_certs: no # Set up authorized_keys exclusively with one key From f8b989755cc7c6c912a6807db4131389d31c9af9 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:39:15 +0000 Subject: [PATCH 752/770] Unquote urls in YAML - cloud (#5797) --- cloud/amazon/cloudformation.py | 4 ++-- cloud/openstack/_glance_image.py | 4 ++-- cloud/openstack/_keystone_user.py | 2 +- cloud/openstack/_nova_compute.py | 2 +- cloud/openstack/_nova_keypair.py | 2 +- cloud/openstack/_quantum_floating_ip.py | 2 +- cloud/openstack/_quantum_floating_ip_associate.py | 2 +- cloud/openstack/_quantum_network.py | 2 +- cloud/openstack/_quantum_router.py | 2 +- cloud/openstack/_quantum_router_gateway.py | 2 +- cloud/openstack/_quantum_subnet.py | 2 +- cloud/openstack/os_image_facts.py | 2 +- cloud/openstack/os_networks_facts.py | 6 +++--- cloud/openstack/os_server.py | 10 +++++----- cloud/openstack/os_subnets_facts.py | 6 +++--- 15 files changed, 25 insertions(+), 25 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index 2cf26785aa0..bdc15613bb4 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -147,7 +147,7 @@ state: present region: us-east-1 disable_rollback: true - template_url: 'https://s3.amazonaws.com/my-bucket/cloudformation.template' + template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template args: template_parameters: KeyName: jmartin @@ -164,7 +164,7 @@ state: present region: us-east-1 disable_rollback: true - template_url: 'https://s3.amazonaws.com/my-bucket/cloudformation.template' + template_url: https://s3.amazonaws.com/my-bucket/cloudformation.template role_arn: 'arn:aws:iam::123456789012:role/cloudformation-iam-role' args: template_parameters: diff --git a/cloud/openstack/_glance_image.py b/cloud/openstack/_glance_image.py index 2d3c9583ffc..bc113b09030 100644 --- a/cloud/openstack/_glance_image.py +++ b/cloud/openstack/_glance_image.py @@ -44,7 +44,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region @@ -129,7 +129,7 @@ container_format: bare disk_format: qcow2 state: present - copy_from: 'http://launchpad.net/cirros/trunk/0.3.0/+download/cirros-0.3.0-x86_64-disk.img' + copy_from: http://launchpad.net/cirros/trunk/0.3.0/+download/cirros-0.3.0-x86_64-disk.img ''' import time diff --git a/cloud/openstack/_keystone_user.py b/cloud/openstack/_keystone_user.py index 430e8bf40c4..72bf4dcf5b8 100644 --- a/cloud/openstack/_keystone_user.py +++ b/cloud/openstack/_keystone_user.py @@ -51,7 +51,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ user: description: - The name of the user that has to added/removed from OpenStack diff --git a/cloud/openstack/_nova_compute.py b/cloud/openstack/_nova_compute.py index 29d3ab04591..634e847485e 100644 --- a/cloud/openstack/_nova_compute.py +++ b/cloud/openstack/_nova_compute.py @@ -58,7 +58,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/_nova_keypair.py b/cloud/openstack/_nova_keypair.py index 587bb5dc042..cabe35ea87d 100644 --- a/cloud/openstack/_nova_keypair.py +++ b/cloud/openstack/_nova_keypair.py @@ -56,7 +56,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/_quantum_floating_ip.py b/cloud/openstack/_quantum_floating_ip.py index d3e2c898db5..f89f632e3f4 100644 --- a/cloud/openstack/_quantum_floating_ip.py +++ b/cloud/openstack/_quantum_floating_ip.py @@ -60,7 +60,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/_quantum_floating_ip_associate.py b/cloud/openstack/_quantum_floating_ip_associate.py index 7c7d7045cb2..3a87af9c4f9 100644 --- a/cloud/openstack/_quantum_floating_ip_associate.py +++ b/cloud/openstack/_quantum_floating_ip_associate.py @@ -57,7 +57,7 @@ description: - the keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - name of the region diff --git a/cloud/openstack/_quantum_network.py b/cloud/openstack/_quantum_network.py index a6cf688ebc1..1e62e561333 100644 --- a/cloud/openstack/_quantum_network.py +++ b/cloud/openstack/_quantum_network.py @@ -59,7 +59,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/_quantum_router.py b/cloud/openstack/_quantum_router.py index 3a12028d272..619a4cf86c7 100644 --- a/cloud/openstack/_quantum_router.py +++ b/cloud/openstack/_quantum_router.py @@ -55,7 +55,7 @@ description: - The keystone url for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/_quantum_router_gateway.py b/cloud/openstack/_quantum_router_gateway.py index e71a171707b..6cdc4424b3f 100644 --- a/cloud/openstack/_quantum_router_gateway.py +++ b/cloud/openstack/_quantum_router_gateway.py @@ -55,7 +55,7 @@ description: - The keystone URL for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/_quantum_subnet.py b/cloud/openstack/_quantum_subnet.py index 8e6dee2c08d..57349558115 100644 --- a/cloud/openstack/_quantum_subnet.py +++ b/cloud/openstack/_quantum_subnet.py @@ -54,7 +54,7 @@ description: - The keystone URL for authentication required: false - default: 'http://127.0.0.1:35357/v2.0/' + default: http://127.0.0.1:35357/v2.0/ region_name: description: - Name of the region diff --git a/cloud/openstack/os_image_facts.py b/cloud/openstack/os_image_facts.py index 9f693c73b77..9fe906da7db 100644 --- a/cloud/openstack/os_image_facts.py +++ b/cloud/openstack/os_image_facts.py @@ -45,7 +45,7 @@ - name: Gather facts about a previously created image named image1 os_image_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject diff --git a/cloud/openstack/os_networks_facts.py b/cloud/openstack/os_networks_facts.py index c261fc32b76..6eb73f9220c 100644 --- a/cloud/openstack/os_networks_facts.py +++ b/cloud/openstack/os_networks_facts.py @@ -49,7 +49,7 @@ - name: Gather facts about previously created networks os_networks_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject @@ -61,7 +61,7 @@ - name: Gather facts about a previously created network by name os_networks_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject @@ -75,7 +75,7 @@ # Note: name and filters parameters are Not mutually exclusive os_networks_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index f4d546d211d..d6d74e1fef0 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -207,7 +207,7 @@ os_server: state: present auth: - auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ username: admin password: admin project_name: admin @@ -232,7 +232,7 @@ os_server: state: present auth: - auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ username: username password: Equality7-2521 project_name: username-project1 @@ -299,7 +299,7 @@ - name: launch an instance with a string os_server: auth: - auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ username: admin password: admin project_name: admin @@ -314,7 +314,7 @@ os_server: state: present auth: - auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ username: admin password: admin project_name: admin @@ -332,7 +332,7 @@ os_server: state: present auth: - auth_url: 'https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/' + auth_url: https://region-b.geo-1.identity.hpcloudsvc.com:35357/v2.0/ username: admin password: admin project_name: admin diff --git a/cloud/openstack/os_subnets_facts.py b/cloud/openstack/os_subnets_facts.py index dce24362f78..3fe0b80ed1d 100644 --- a/cloud/openstack/os_subnets_facts.py +++ b/cloud/openstack/os_subnets_facts.py @@ -49,7 +49,7 @@ - name: Gather facts about previously created subnets os_subnets_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject @@ -61,7 +61,7 @@ - name: Gather facts about a previously created subnet by name os_subnets_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject @@ -75,7 +75,7 @@ # Note: name and filters parameters are not mutually exclusive os_subnets_facts: auth: - auth_url: 'https://your_api_url.com:9000/v2.0' + auth_url: https://your_api_url.com:9000/v2.0 username: user password: password project_name: someproject From db66833a80bcd7f0d33ccbfd6555d69c5ae88003 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:39:29 +0000 Subject: [PATCH 753/770] Unquote urls in YAML - windows (#5798) --- windows/win_get_url.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/windows/win_get_url.py b/windows/win_get_url.py index e4fe725cad1..4091a596920 100644 --- a/windows/win_get_url.py +++ b/windows/win_get_url.py @@ -117,7 +117,7 @@ description: requested url returned: always type: string - sample: 'http://www.example.com/earthrise.jpg' + sample: http://www.example.com/earthrise.jpg dest: description: destination file/path returned: always From 985948b508b69980dd4cb8924d0b2902cb7f7653 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Fri, 2 Dec 2016 15:52:28 +0000 Subject: [PATCH 754/770] Native YAML - cloud/google/gce.py (#5800) --- cloud/google/gce.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cloud/google/gce.py b/cloud/google/gce.py index e1682e9c424..aa06d9c8e81 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -207,8 +207,10 @@ - http-server - my-other-tag disks: - - { 'name' : 'disk-2', 'mode': 'READ_WRITE' } - - { 'name' : 'disk-3', 'mode': 'READ_ONLY' } + - name: disk-2 + mode: READ_WRITE + - name: disk-3 + mode: READ_ONLY disk_auto_delete: false network: foobar-network subnetwork: foobar-subnetwork-1 From 4f6189b7732bce107f99de2988ad6ad2109c5626 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Sun, 4 Dec 2016 10:48:20 +0000 Subject: [PATCH 755/770] Native YAML - files/file.py (#5799) * Native YAML - files/file.py * Rollaback part of the change as for request --- files/file.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/files/file.py b/files/file.py index b7044ca2a18..f0947cfbd70 100644 --- a/files/file.py +++ b/files/file.py @@ -100,8 +100,8 @@ group: foo state: link - file: - src: /tmp/{{ item.src }} - dest: "{{ item.dest }}" + src: '/tmp/{{ item.src }}' + dest: '{{ item.dest }}' state: link with_items: - { src: 'x', dest: 'y' } @@ -124,7 +124,6 @@ path: /etc/some_directory state: directory mode: 0755 - ''' import errno From 60b82f757dade5eefea905f97a4ebdc1d9ef1937 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Mon, 5 Dec 2016 03:57:27 -0800 Subject: [PATCH 756/770] Do not use the fstab parameter on openbsd for mounting (#5805) * Do not use the fstab parameter on openbsd for mounting OpenBSD's mount command doesn't allow selecting which fstab file to use. So if we're operating on the live filesystem (mount or remount) return an error if the user specified an fstab file. Fixes #5591 * Fix the logic inversion (thanks to @landryb) --- system/mount.py | 51 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 17 deletions(-) diff --git a/system/mount.py b/system/mount.py index 1d7fa8ac6c8..9fe7fe32cfe 100644 --- a/system/mount.py +++ b/system/mount.py @@ -90,9 +90,11 @@ choices: ["present", "absent", "mounted", "unmounted"] fstab: description: - - File to use instead of C(/etc/fstab). You shouldn't use that option + - File to use instead of C(/etc/fstab). You shouldn't use this option unless you really know what you are doing. This might be useful if - you need to configure mountpoints in a chroot environment. + you need to configure mountpoints in a chroot environment. OpenBSD + does not allow specifying alternate fstab files with mount so do not + use this on OpenBSD with any state that operates on the live filesystem. required: false default: /etc/fstab (/etc/vfstab on Solaris) boot: @@ -308,14 +310,14 @@ def unset_mount(module, args): return (args['name'], changed) -def _set_fstab_args(args): +def _set_fstab_args(fstab_file): result = [] - if 'fstab' in args and args['fstab'] != '/etc/fstab': + if fstab_file and fstab_file != '/etc/fstab': if get_platform().lower().endswith('bsd'): result.append('-F') else: result.append('-T') - result.append(args['fstab']) + result.append(fstab_file) return result def mount(module, args): @@ -328,7 +330,13 @@ def mount(module, args): if ismount(name): return remount(module, mount_bin, args) - cmd += _set_fstab_args(args) + if get_platform().lower() == 'openbsd': + # Use module.params['fstab'] here as args['fstab'] has been set to the + # default value. + if module.params['fstab'] is not None: + module.fail_json(msg='OpenBSD does not support alternate fstab files. Do not specify the fstab parameter for OpenBSD hosts') + else: + cmd += _set_fstab_args(args['fstab']) cmd += [name] @@ -364,7 +372,13 @@ def remount(module, mount_bin, args): else: cmd += ['-o', 'remount' ] - cmd += _set_fstab_args(args) + if get_platform().lower() == 'openbsd': + # Use module.params['fstab'] here as args['fstab'] has been set to the + # default value. + if module.params['fstab'] is not None: + module.fail_json(msg='OpenBSD does not support alternate fstab files. Do not specify the fstab parameter for OpenBSD hosts') + else: + cmd += _set_fstab_args(args['fstab']) cmd += [ args['name'], ] out = err = '' try: @@ -564,7 +578,7 @@ def main(): argument_spec=dict( boot=dict(default='yes', choices=['yes', 'no']), dump=dict(), - fstab=dict(default='/etc/fstab'), + fstab=dict(default=None), fstype=dict(), name=dict(required=True, type='path'), opts=dict(), @@ -586,26 +600,32 @@ def main(): # name, src, fstype, opts, boot, passno, state, fstab=/etc/vfstab # linux args: # name, src, fstype, opts, dump, passno, state, fstab=/etc/fstab - if get_platform() == 'SunOS': + # Note: Do not modify module.params['fstab'] as we need to know if the user + # explicitly specified it in mount() and remount() + if get_platform().lower() == 'sunos': args = dict( name=module.params['name'], opts='-', passno='-', - fstab='/etc/vfstab', + fstab=module.params['fstab'], boot='yes' ) + if args['fstab'] is None: + args['fstab'] = '/etc/vfstab' else: args = dict( name=module.params['name'], opts='defaults', dump='0', passno='0', - fstab='/etc/fstab' + fstab=module.params['fstab'] ) + if args['fstab'] is None: + args['fstab'] = '/etc/fstab' - # FreeBSD doesn't have any 'default' so set 'rw' instead - if get_platform() == 'FreeBSD': - args['opts'] = 'rw' + # FreeBSD doesn't have any 'default' so set 'rw' instead + if get_platform() == 'FreeBSD': + args['opts'] = 'rw' linux_mounts = [] @@ -624,9 +644,6 @@ def main(): if module.params[key] is not None: args[key] = module.params[key] - if get_platform() == 'SunOS' and args['fstab'] == '/etc/fstab': - args['fstab'] = '/etc/vfstab' - # If fstab file does not exist, we first need to create it. This mainly # happens when fstab option is passed to the module. if not os.path.exists(args['fstab']): From 636106861ef817da24b0a26dbda8ea6308d60b15 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:03:05 +0000 Subject: [PATCH 757/770] Call main in conditional way - web_infrastructure --- web_infrastructure/django_manage.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/web_infrastructure/django_manage.py b/web_infrastructure/django_manage.py index f334f3989b2..b21c2834a81 100644 --- a/web_infrastructure/django_manage.py +++ b/web_infrastructure/django_manage.py @@ -294,4 +294,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From a598c9bc92944e3a7c86d1d5df55616faa72869f Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:04:51 +0000 Subject: [PATCH 758/770] Call main in conditional way - system --- system/cron.py | 4 ++-- system/group.py | 4 +++- system/seboolean.py | 4 +++- system/service.py | 3 ++- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/system/cron.py b/system/cron.py index 34307c6b872..3157b48b69b 100644 --- a/system/cron.py +++ b/system/cron.py @@ -771,5 +771,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() - +if __name__ == '__main__': + main() diff --git a/system/group.py b/system/group.py index ceffd75369b..a642a3ee3e2 100644 --- a/system/group.py +++ b/system/group.py @@ -466,4 +466,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/system/seboolean.py b/system/seboolean.py index 973b5b2d20b..6d3b312d588 100644 --- a/system/seboolean.py +++ b/system/seboolean.py @@ -219,4 +219,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils._text import to_bytes -main() + +if __name__ == '__main__': + main() diff --git a/system/service.py b/system/service.py index af97e7df5a4..4b22f2654f7 100644 --- a/system/service.py +++ b/system/service.py @@ -1568,4 +1568,5 @@ def main(): from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 1b004ff694b89653082674b3c85bcbd8c2b32505 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:06:04 +0000 Subject: [PATCH 759/770] Call main in conditional way - source_control --- source_control/subversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/source_control/subversion.py b/source_control/subversion.py index 1049a3e6a56..dc9cedbbedf 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -304,4 +304,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From b0d98da534dd9a6af516072fdc9cb703a020be02 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:08:15 +0000 Subject: [PATCH 760/770] Call main in conditional way - cloud/amazon --- cloud/amazon/ec2.py | 3 ++- cloud/amazon/ec2_facts.py | 3 ++- cloud/amazon/ec2_group.py | 3 ++- cloud/amazon/ec2_key.py | 3 ++- cloud/amazon/ec2_lc.py | 3 ++- cloud/amazon/ec2_vpc.py | 3 ++- cloud/amazon/elasticache.py | 3 ++- cloud/amazon/elasticache_subnet_group.py | 3 ++- cloud/amazon/iam.py | 3 ++- cloud/amazon/iam_policy.py | 3 ++- cloud/amazon/rds.py | 3 ++- cloud/amazon/rds_subnet_group.py | 3 ++- cloud/amazon/route53.py | 3 ++- cloud/amazon/s3.py | 3 ++- 14 files changed, 28 insertions(+), 14 deletions(-) diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 8e17b92009f..3ca9bf3751a 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -1624,4 +1624,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_facts.py b/cloud/amazon/ec2_facts.py index f7fb86cbc90..7522658c348 100644 --- a/cloud/amazon/ec2_facts.py +++ b/cloud/amazon/ec2_facts.py @@ -183,4 +183,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_group.py b/cloud/amazon/ec2_group.py index 919edfae9b0..4e320d121cd 100644 --- a/cloud/amazon/ec2_group.py +++ b/cloud/amazon/ec2_group.py @@ -475,4 +475,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_key.py b/cloud/amazon/ec2_key.py index d0f361e3944..5adb2e95bb5 100644 --- a/cloud/amazon/ec2_key.py +++ b/cloud/amazon/ec2_key.py @@ -241,4 +241,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_lc.py b/cloud/amazon/ec2_lc.py index d36bb15ecd6..97c959267c9 100644 --- a/cloud/amazon/ec2_lc.py +++ b/cloud/amazon/ec2_lc.py @@ -308,4 +308,5 @@ def main(): elif state == 'absent': delete_launch_config(connection, module) -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/ec2_vpc.py b/cloud/amazon/ec2_vpc.py index e17674e2d05..4ae2065b074 100644 --- a/cloud/amazon/ec2_vpc.py +++ b/cloud/amazon/ec2_vpc.py @@ -742,4 +742,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/elasticache.py b/cloud/amazon/elasticache.py index 5a3a60c005f..b0457ace4e9 100644 --- a/cloud/amazon/elasticache.py +++ b/cloud/amazon/elasticache.py @@ -554,4 +554,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/elasticache_subnet_group.py b/cloud/amazon/elasticache_subnet_group.py index f2aa2e25b9c..7fbdfec9ff4 100644 --- a/cloud/amazon/elasticache_subnet_group.py +++ b/cloud/amazon/elasticache_subnet_group.py @@ -151,4 +151,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/iam.py b/cloud/amazon/iam.py index 15f6741b074..8c10bd09189 100644 --- a/cloud/amazon/iam.py +++ b/cloud/amazon/iam.py @@ -795,4 +795,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/iam_policy.py b/cloud/amazon/iam_policy.py index a95f88f42d3..5391294beab 100644 --- a/cloud/amazon/iam_policy.py +++ b/cloud/amazon/iam_policy.py @@ -349,4 +349,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py index 5b2e43834f8..1e33f9c47b4 100644 --- a/cloud/amazon/rds.py +++ b/cloud/amazon/rds.py @@ -1104,4 +1104,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/rds_subnet_group.py b/cloud/amazon/rds_subnet_group.py index 7ec4ac01b7c..e99b3d8d479 100644 --- a/cloud/amazon/rds_subnet_group.py +++ b/cloud/amazon/rds_subnet_group.py @@ -151,4 +151,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/route53.py b/cloud/amazon/route53.py index 4f08a4f6543..9c0f19bffa3 100644 --- a/cloud/amazon/route53.py +++ b/cloud/amazon/route53.py @@ -586,4 +586,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() diff --git a/cloud/amazon/s3.py b/cloud/amazon/s3.py index fad1d6d6aa4..d2b4912faa9 100755 --- a/cloud/amazon/s3.py +++ b/cloud/amazon/s3.py @@ -736,4 +736,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.ec2 import * -main() +if __name__ == '__main__': + main() From 2696a702465208f7b0f9f04ef65a8da44f8ff3fe Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:14:23 +0000 Subject: [PATCH 761/770] Call main in conditional way - cloud/rackspace --- cloud/rackspace/rax.py | 4 +++- cloud/rackspace/rax_cbs.py | 4 +++- cloud/rackspace/rax_cbs_attachments.py | 4 +++- cloud/rackspace/rax_cdb.py | 4 +++- cloud/rackspace/rax_cdb_database.py | 4 +++- cloud/rackspace/rax_cdb_user.py | 4 +++- cloud/rackspace/rax_clb.py | 4 +++- cloud/rackspace/rax_clb_nodes.py | 4 +++- cloud/rackspace/rax_dns.py | 4 +++- cloud/rackspace/rax_dns_record.py | 4 +++- cloud/rackspace/rax_facts.py | 4 +++- cloud/rackspace/rax_files.py | 4 +++- cloud/rackspace/rax_files_objects.py | 4 +++- cloud/rackspace/rax_identity.py | 4 +++- cloud/rackspace/rax_keypair.py | 4 +++- cloud/rackspace/rax_meta.py | 4 +++- cloud/rackspace/rax_network.py | 4 +++- cloud/rackspace/rax_queue.py | 4 +++- cloud/rackspace/rax_scaling_group.py | 4 +++- cloud/rackspace/rax_scaling_policy.py | 4 +++- 20 files changed, 60 insertions(+), 20 deletions(-) diff --git a/cloud/rackspace/rax.py b/cloud/rackspace/rax.py index 9ac0318c105..31b34f8c45d 100644 --- a/cloud/rackspace/rax.py +++ b/cloud/rackspace/rax.py @@ -890,4 +890,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_cbs.py b/cloud/rackspace/rax_cbs.py index c54ee580886..597ef1bfe72 100644 --- a/cloud/rackspace/rax_cbs.py +++ b/cloud/rackspace/rax_cbs.py @@ -233,4 +233,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_cbs_attachments.py b/cloud/rackspace/rax_cbs_attachments.py index 3b014d465fc..54e261be511 100644 --- a/cloud/rackspace/rax_cbs_attachments.py +++ b/cloud/rackspace/rax_cbs_attachments.py @@ -218,4 +218,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_cdb.py b/cloud/rackspace/rax_cdb.py index ec659222e88..47af20ffc90 100644 --- a/cloud/rackspace/rax_cdb.py +++ b/cloud/rackspace/rax_cdb.py @@ -258,4 +258,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_cdb_database.py b/cloud/rackspace/rax_cdb_database.py index 45d0f24722d..6a5e2e86e44 100644 --- a/cloud/rackspace/rax_cdb_database.py +++ b/cloud/rackspace/rax_cdb_database.py @@ -172,4 +172,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_cdb_user.py b/cloud/rackspace/rax_cdb_user.py index ff54365ea16..6d7ae27ec70 100644 --- a/cloud/rackspace/rax_cdb_user.py +++ b/cloud/rackspace/rax_cdb_user.py @@ -217,4 +217,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_clb.py b/cloud/rackspace/rax_clb.py index f02d6488db0..8eae3a5bba9 100644 --- a/cloud/rackspace/rax_clb.py +++ b/cloud/rackspace/rax_clb.py @@ -305,4 +305,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_clb_nodes.py b/cloud/rackspace/rax_clb_nodes.py index 31bba7bf525..e638bb0ee84 100644 --- a/cloud/rackspace/rax_clb_nodes.py +++ b/cloud/rackspace/rax_clb_nodes.py @@ -278,4 +278,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_dns.py b/cloud/rackspace/rax_dns.py index 6024fea1271..fa509802b36 100644 --- a/cloud/rackspace/rax_dns.py +++ b/cloud/rackspace/rax_dns.py @@ -170,4 +170,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_dns_record.py b/cloud/rackspace/rax_dns_record.py index c5227174ac3..1d8b8a01b71 100644 --- a/cloud/rackspace/rax_dns_record.py +++ b/cloud/rackspace/rax_dns_record.py @@ -345,4 +345,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_facts.py b/cloud/rackspace/rax_facts.py index 8923718e5e7..14e17ef32d8 100644 --- a/cloud/rackspace/rax_facts.py +++ b/cloud/rackspace/rax_facts.py @@ -143,4 +143,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_files.py b/cloud/rackspace/rax_files.py index 77ab70d8e8c..3eb9dad0390 100644 --- a/cloud/rackspace/rax_files.py +++ b/cloud/rackspace/rax_files.py @@ -389,4 +389,6 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.rax import * -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_files_objects.py b/cloud/rackspace/rax_files_objects.py index d0175996b14..fb1638bce0d 100644 --- a/cloud/rackspace/rax_files_objects.py +++ b/cloud/rackspace/rax_files_objects.py @@ -617,4 +617,6 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.rax import * -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_identity.py b/cloud/rackspace/rax_identity.py index b4d1cc3f28a..9473585c169 100644 --- a/cloud/rackspace/rax_identity.py +++ b/cloud/rackspace/rax_identity.py @@ -106,4 +106,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_keypair.py b/cloud/rackspace/rax_keypair.py index 0e9585b24c9..bbc077a4504 100644 --- a/cloud/rackspace/rax_keypair.py +++ b/cloud/rackspace/rax_keypair.py @@ -171,4 +171,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_meta.py b/cloud/rackspace/rax_meta.py index 724513a3120..5a177905187 100644 --- a/cloud/rackspace/rax_meta.py +++ b/cloud/rackspace/rax_meta.py @@ -175,4 +175,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_network.py b/cloud/rackspace/rax_network.py index 59b1cd6554a..257e0cac265 100644 --- a/cloud/rackspace/rax_network.py +++ b/cloud/rackspace/rax_network.py @@ -143,4 +143,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_queue.py b/cloud/rackspace/rax_queue.py index fc2c1ae8d5c..bfa7626ac75 100644 --- a/cloud/rackspace/rax_queue.py +++ b/cloud/rackspace/rax_queue.py @@ -144,4 +144,6 @@ def main(): from ansible.module_utils.rax import * ### invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_scaling_group.py b/cloud/rackspace/rax_scaling_group.py index 952e215c72a..74ee298cbba 100644 --- a/cloud/rackspace/rax_scaling_group.py +++ b/cloud/rackspace/rax_scaling_group.py @@ -426,4 +426,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() diff --git a/cloud/rackspace/rax_scaling_policy.py b/cloud/rackspace/rax_scaling_policy.py index 33ebc60779d..8533261c2a8 100644 --- a/cloud/rackspace/rax_scaling_policy.py +++ b/cloud/rackspace/rax_scaling_policy.py @@ -280,4 +280,6 @@ def main(): from ansible.module_utils.rax import * # invoke the module -main() + +if __name__ == '__main__': + main() From b2d01ccba4d6b62632bb149c5de2e8eed153c68c Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:15:33 +0000 Subject: [PATCH 762/770] Call main in conditional way - databases --- database/postgresql/postgresql_user.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index 532e8645327..1cdc92ef943 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -682,4 +682,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * from ansible.module_utils.database import * -main() + +if __name__ == '__main__': + main() From b568340d18df7767ec05b93d84c300750a1af6f3 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 17:59:46 +0000 Subject: [PATCH 763/770] Call main in conditional way - files (#5828) --- files/acl.py | 3 ++- files/find.py | 3 ++- files/synchronize.py | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/files/acl.py b/files/acl.py index 4974b6bbfcc..fda245d4080 100644 --- a/files/acl.py +++ b/files/acl.py @@ -368,4 +368,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/files/find.py b/files/find.py index 3fdf2646269..f6bfbebe64c 100644 --- a/files/find.py +++ b/files/find.py @@ -374,5 +374,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/files/synchronize.py b/files/synchronize.py index 89541840098..eb813b72f8c 100644 --- a/files/synchronize.py +++ b/files/synchronize.py @@ -497,4 +497,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() From 4659c084de73db337f11e08b8b78153380e13a76 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 18:00:03 +0000 Subject: [PATCH 764/770] Call main in conditional way - utilities (#5827) --- utilities/helper/_accelerate.py | 3 ++- utilities/logic/async_status.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/utilities/helper/_accelerate.py b/utilities/helper/_accelerate.py index 3bb79122191..c00646fcd4f 100644 --- a/utilities/helper/_accelerate.py +++ b/utilities/helper/_accelerate.py @@ -738,4 +738,5 @@ def main(): # try to start up the daemon daemonize(module, password, port, timeout, minutes, ipv6, pid_file) -main() +if __name__ == '__main__': + main() diff --git a/utilities/logic/async_status.py b/utilities/logic/async_status.py index 507780b46a1..c0e2526d549 100644 --- a/utilities/logic/async_status.py +++ b/utilities/logic/async_status.py @@ -102,4 +102,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() From 454844931508c20bea270b694f67663586715728 Mon Sep 17 00:00:00 2001 From: Fabio Alessandro Locati Date: Mon, 5 Dec 2016 18:00:18 +0000 Subject: [PATCH 765/770] Call main in conditional way - packaging (#5826) --- packaging/language/easy_install.py | 3 ++- packaging/language/gem.py | 4 +++- packaging/os/apt_repository.py | 3 ++- packaging/os/apt_rpm.py | 3 ++- packaging/os/redhat_subscription.py | 4 +++- packaging/os/rhn_channel.py | 3 ++- packaging/os/rhn_register.py | 3 ++- 7 files changed, 16 insertions(+), 7 deletions(-) diff --git a/packaging/language/easy_install.py b/packaging/language/easy_install.py index 96d21ea8f35..31d1df9abc2 100644 --- a/packaging/language/easy_install.py +++ b/packaging/language/easy_install.py @@ -204,4 +204,5 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/language/gem.py b/packaging/language/gem.py index 188841593e7..9407e9c5d4d 100644 --- a/packaging/language/gem.py +++ b/packaging/language/gem.py @@ -273,4 +273,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index fc33021e519..0b86b6bddba 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -556,4 +556,5 @@ def main(): from ansible.module_utils.basic import * from ansible.module_utils.urls import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/apt_rpm.py b/packaging/os/apt_rpm.py index e8a702e9a09..452d2ff8ca6 100755 --- a/packaging/os/apt_rpm.py +++ b/packaging/os/apt_rpm.py @@ -179,4 +179,5 @@ def main(): # this is magic, see lib/ansible/module_common.py from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/redhat_subscription.py b/packaging/os/redhat_subscription.py index ea56ac55100..b3b0aabb356 100644 --- a/packaging/os/redhat_subscription.py +++ b/packaging/os/redhat_subscription.py @@ -552,4 +552,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() + +if __name__ == '__main__': + main() diff --git a/packaging/os/rhn_channel.py b/packaging/os/rhn_channel.py index 7a7735b84b5..e77972b30ce 100644 --- a/packaging/os/rhn_channel.py +++ b/packaging/os/rhn_channel.py @@ -171,5 +171,6 @@ def main(): # import module snippets from ansible.module_utils.basic import * -main() +if __name__ == '__main__': + main() diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index cac93420fe5..fda8416e153 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -422,4 +422,5 @@ def main(): module.exit_json(changed=True, msg="System successfully unregistered from %s." % rhn.hostname) -main() +if __name__ == '__main__': + main() From fc465fa1e776a1e9d3a7448db0081d3e205ec06e Mon Sep 17 00:00:00 2001 From: Adam Chainz Date: Mon, 5 Dec 2016 18:23:53 +0000 Subject: [PATCH 766/770] Cloudformation - remove dead invoke_with_throttling_retries function (#5690) Unused since f040d63403f6c459a278918fa48fa8cb87754506. --- cloud/amazon/cloudformation.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index bdc15613bb4..be20b64074b 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -339,20 +339,6 @@ def get_stack_facts(cfn, stack_name): return stack_info -IGNORE_CODE = 'Throttling' -MAX_RETRIES=3 -def invoke_with_throttling_retries(function_ref, *argv, **kwargs): - retries=0 - while True: - try: - retval=function_ref(*argv, **kwargs) - return retval - except Exception as e: - # boto way of looking for retries - #if e.code != IGNORE_CODE or retries==MAX_RETRIES: - raise e - time.sleep(5 * (2**retries)) - retries += 1 def main(): argument_spec = ansible.module_utils.ec2.ec2_argument_spec() From d749ec91e513c40a50b539a7eaccf29c31dda4d4 Mon Sep 17 00:00:00 2001 From: Toshio Kuratomi Date: Tue, 6 Dec 2016 02:35:05 -0800 Subject: [PATCH 767/770] Refreshed metadata for core modules --- cloud/amazon/_ec2_ami_search.py | 4 ++++ cloud/amazon/cloudformation.py | 4 ++++ cloud/amazon/ec2.py | 4 ++++ cloud/amazon/ec2_ami.py | 4 ++++ cloud/amazon/ec2_ami_find.py | 4 ++++ cloud/amazon/ec2_asg.py | 4 ++++ cloud/amazon/ec2_eip.py | 4 ++++ cloud/amazon/ec2_elb.py | 4 ++++ cloud/amazon/ec2_elb_lb.py | 4 ++++ cloud/amazon/ec2_facts.py | 4 ++++ cloud/amazon/ec2_group.py | 4 ++++ cloud/amazon/ec2_key.py | 4 ++++ cloud/amazon/ec2_lc.py | 4 ++++ cloud/amazon/ec2_metric_alarm.py | 4 ++++ cloud/amazon/ec2_scaling_policy.py | 4 ++++ cloud/amazon/ec2_snapshot.py | 4 ++++ cloud/amazon/ec2_tag.py | 4 ++++ cloud/amazon/ec2_vol.py | 4 ++++ cloud/amazon/ec2_vpc.py | 4 ++++ cloud/amazon/ec2_vpc_net.py | 4 ++++ cloud/amazon/elasticache.py | 4 ++++ cloud/amazon/elasticache_subnet_group.py | 4 ++++ cloud/amazon/iam.py | 4 ++++ cloud/amazon/iam_cert.py | 4 ++++ cloud/amazon/iam_policy.py | 4 ++++ cloud/amazon/rds.py | 4 ++++ cloud/amazon/rds_param_group.py | 4 ++++ cloud/amazon/rds_subnet_group.py | 4 ++++ cloud/amazon/route53.py | 4 ++++ cloud/amazon/s3.py | 4 ++++ cloud/azure/azure.py | 4 ++++ cloud/azure/azure_rm_networkinterface.py | 4 ++++ cloud/azure/azure_rm_networkinterface_facts.py | 4 ++++ cloud/azure/azure_rm_publicipaddress.py | 4 ++++ cloud/azure/azure_rm_publicipaddress_facts.py | 4 ++++ cloud/azure/azure_rm_resourcegroup.py | 4 ++++ cloud/azure/azure_rm_resourcegroup_facts.py | 4 ++++ cloud/azure/azure_rm_securitygroup.py | 4 ++++ cloud/azure/azure_rm_securitygroup_facts.py | 4 ++++ cloud/azure/azure_rm_storageaccount.py | 4 ++++ cloud/azure/azure_rm_storageaccount_facts.py | 4 ++++ cloud/azure/azure_rm_storageblob.py | 4 ++++ cloud/azure/azure_rm_subnet.py | 4 ++++ cloud/azure/azure_rm_virtualmachine.py | 4 ++++ cloud/azure/azure_rm_virtualmachineimage_facts.py | 4 ++++ cloud/azure/azure_rm_virtualnetwork.py | 4 ++++ cloud/azure/azure_rm_virtualnetwork_facts.py | 4 ++++ cloud/digital_ocean/digital_ocean.py | 4 ++++ cloud/digital_ocean/digital_ocean_block_storage.py | 4 ++++ cloud/digital_ocean/digital_ocean_domain.py | 4 ++++ cloud/digital_ocean/digital_ocean_sshkey.py | 4 ++++ cloud/digital_ocean/digital_ocean_tag.py | 4 ++++ cloud/docker/_docker.py | 4 ++++ cloud/docker/docker_container.py | 4 ++++ cloud/docker/docker_image.py | 4 ++++ cloud/docker/docker_image_facts.py | 4 ++++ cloud/docker/docker_login.py | 4 ++++ cloud/docker/docker_network.py | 4 ++++ cloud/docker/docker_service.py | 4 ++++ cloud/google/gc_storage.py | 4 ++++ cloud/google/gce.py | 4 ++++ cloud/google/gce_lb.py | 4 ++++ cloud/google/gce_mig.py | 4 ++++ cloud/google/gce_net.py | 4 ++++ cloud/google/gce_pd.py | 4 ++++ cloud/linode/linode.py | 4 ++++ cloud/openstack/_glance_image.py | 4 ++++ cloud/openstack/_keystone_user.py | 4 ++++ cloud/openstack/_nova_compute.py | 4 ++++ cloud/openstack/_nova_keypair.py | 4 ++++ cloud/openstack/_quantum_floating_ip.py | 4 ++++ cloud/openstack/_quantum_floating_ip_associate.py | 4 ++++ cloud/openstack/_quantum_network.py | 4 ++++ cloud/openstack/_quantum_router.py | 4 ++++ cloud/openstack/_quantum_router_gateway.py | 4 ++++ cloud/openstack/_quantum_router_interface.py | 4 ++++ cloud/openstack/_quantum_subnet.py | 4 ++++ cloud/openstack/os_auth.py | 4 ++++ cloud/openstack/os_client_config.py | 4 ++++ cloud/openstack/os_floating_ip.py | 4 ++++ cloud/openstack/os_image.py | 4 ++++ cloud/openstack/os_image_facts.py | 4 ++++ cloud/openstack/os_ironic.py | 4 ++++ cloud/openstack/os_ironic_node.py | 4 ++++ cloud/openstack/os_keypair.py | 4 ++++ cloud/openstack/os_network.py | 4 ++++ cloud/openstack/os_networks_facts.py | 4 ++++ cloud/openstack/os_nova_flavor.py | 4 ++++ cloud/openstack/os_object.py | 4 ++++ cloud/openstack/os_port.py | 4 ++++ cloud/openstack/os_router.py | 4 ++++ cloud/openstack/os_security_group.py | 4 ++++ cloud/openstack/os_security_group_rule.py | 4 ++++ cloud/openstack/os_server.py | 4 ++++ cloud/openstack/os_server_actions.py | 4 ++++ cloud/openstack/os_server_facts.py | 4 ++++ cloud/openstack/os_server_volume.py | 4 ++++ cloud/openstack/os_subnet.py | 4 ++++ cloud/openstack/os_subnets_facts.py | 4 ++++ cloud/openstack/os_user.py | 4 ++++ cloud/openstack/os_user_group.py | 4 ++++ cloud/openstack/os_volume.py | 4 ++++ cloud/rackspace/rax.py | 4 ++++ cloud/rackspace/rax_cbs.py | 4 ++++ cloud/rackspace/rax_cbs_attachments.py | 4 ++++ cloud/rackspace/rax_cdb.py | 4 ++++ cloud/rackspace/rax_cdb_database.py | 4 ++++ cloud/rackspace/rax_cdb_user.py | 4 ++++ cloud/rackspace/rax_clb.py | 4 ++++ cloud/rackspace/rax_clb_nodes.py | 4 ++++ cloud/rackspace/rax_dns.py | 4 ++++ cloud/rackspace/rax_dns_record.py | 4 ++++ cloud/rackspace/rax_facts.py | 4 ++++ cloud/rackspace/rax_files.py | 4 ++++ cloud/rackspace/rax_files_objects.py | 4 ++++ cloud/rackspace/rax_identity.py | 4 ++++ cloud/rackspace/rax_keypair.py | 4 ++++ cloud/rackspace/rax_meta.py | 4 ++++ cloud/rackspace/rax_network.py | 4 ++++ cloud/rackspace/rax_queue.py | 4 ++++ cloud/rackspace/rax_scaling_group.py | 4 ++++ cloud/rackspace/rax_scaling_policy.py | 4 ++++ cloud/vmware/vsphere_guest.py | 4 ++++ commands/command.py | 4 ++++ commands/raw.py | 4 ++++ commands/script.py | 4 ++++ commands/shell.py | 4 ++++ database/mysql/mysql_db.py | 4 ++++ database/mysql/mysql_user.py | 4 ++++ database/mysql/mysql_variables.py | 4 ++++ database/postgresql/postgresql_db.py | 4 ++++ database/postgresql/postgresql_privs.py | 4 ++++ database/postgresql/postgresql_user.py | 4 ++++ files/acl.py | 4 ++++ files/assemble.py | 4 ++++ files/copy.py | 4 ++++ files/fetch.py | 4 ++++ files/file.py | 4 ++++ files/find.py | 4 ++++ files/ini_file.py | 4 ++++ files/lineinfile.py | 4 ++++ files/replace.py | 4 ++++ files/stat.py | 4 ++++ files/synchronize.py | 4 ++++ files/template.py | 4 ++++ files/unarchive.py | 4 ++++ files/xattr.py | 4 ++++ inventory/add_host.py | 4 ++++ inventory/group_by.py | 4 ++++ network/basics/get_url.py | 4 ++++ network/basics/slurp.py | 4 ++++ network/basics/uri.py | 4 ++++ network/cumulus/cl_bond.py | 4 ++++ network/cumulus/cl_bridge.py | 4 ++++ network/cumulus/cl_img_install.py | 4 ++++ network/cumulus/cl_interface.py | 4 ++++ network/cumulus/cl_interface_policy.py | 4 ++++ network/cumulus/cl_license.py | 4 ++++ network/cumulus/cl_ports.py | 4 ++++ network/dellos10/dellos10_command.py | 4 ++++ network/dellos10/dellos10_config.py | 4 ++++ network/dellos10/dellos10_facts.py | 4 ++++ network/dellos6/dellos6_command.py | 4 ++++ network/dellos6/dellos6_config.py | 4 ++++ network/dellos6/dellos6_facts.py | 4 ++++ network/dellos9/dellos9_command.py | 4 ++++ network/dellos9/dellos9_config.py | 4 ++++ network/dellos9/dellos9_facts.py | 4 ++++ network/eos/_eos_template.py | 4 ++++ network/eos/eos_command.py | 4 ++++ network/eos/eos_config.py | 4 ++++ network/eos/eos_eapi.py | 4 ++++ network/eos/eos_facts.py | 4 ++++ network/ios/_ios_template.py | 5 +++++ network/ios/ios_command.py | 4 ++++ network/ios/ios_config.py | 4 ++++ network/ios/ios_facts.py | 4 ++++ network/iosxr/_iosxr_template.py | 5 +++++ network/iosxr/iosxr_command.py | 4 ++++ network/iosxr/iosxr_config.py | 4 ++++ network/iosxr/iosxr_facts.py | 4 ++++ network/junos/_junos_template.py | 5 +++++ network/junos/junos_command.py | 4 ++++ network/junos/junos_config.py | 4 ++++ network/junos/junos_facts.py | 4 ++++ network/junos/junos_netconf.py | 4 ++++ network/junos/junos_package.py | 4 ++++ network/netvisor/pn_cluster.py | 4 ++++ network/netvisor/pn_ospf.py | 4 ++++ network/netvisor/pn_ospfarea.py | 4 ++++ network/netvisor/pn_show.py | 4 ++++ network/netvisor/pn_trunk.py | 4 ++++ network/netvisor/pn_vlag.py | 4 ++++ network/netvisor/pn_vlan.py | 4 ++++ network/netvisor/pn_vrouter.py | 4 ++++ network/netvisor/pn_vrouterbgp.py | 4 ++++ network/netvisor/pn_vrouterif.py | 4 ++++ network/netvisor/pn_vrouterlbif.py | 4 ++++ network/nxos/_nxos_template.py | 5 +++++ network/nxos/nxos_aaa_server.py | 4 ++++ network/nxos/nxos_aaa_server_host.py | 4 ++++ network/nxos/nxos_acl.py | 4 ++++ network/nxos/nxos_acl_interface.py | 4 ++++ network/nxos/nxos_bgp.py | 4 ++++ network/nxos/nxos_bgp_af.py | 4 ++++ network/nxos/nxos_bgp_neighbor.py | 4 ++++ network/nxos/nxos_bgp_neighbor_af.py | 4 ++++ network/nxos/nxos_command.py | 4 ++++ network/nxos/nxos_config.py | 4 ++++ network/nxos/nxos_evpn_global.py | 4 ++++ network/nxos/nxos_evpn_vni.py | 4 ++++ network/nxos/nxos_facts.py | 4 ++++ network/nxos/nxos_feature.py | 4 ++++ network/nxos/nxos_file_copy.py | 4 ++++ network/nxos/nxos_gir.py | 4 ++++ network/nxos/nxos_gir_profile_management.py | 4 ++++ network/nxos/nxos_hsrp.py | 4 ++++ network/nxos/nxos_igmp.py | 4 ++++ network/nxos/nxos_igmp_interface.py | 4 ++++ network/nxos/nxos_igmp_snooping.py | 4 ++++ network/nxos/nxos_install_os.py | 4 ++++ network/nxos/nxos_interface.py | 4 ++++ network/nxos/nxos_interface_ospf.py | 4 ++++ network/nxos/nxos_ip_interface.py | 4 ++++ network/nxos/nxos_mtu.py | 4 ++++ network/nxos/nxos_ntp.py | 4 ++++ network/nxos/nxos_ntp_auth.py | 4 ++++ network/nxos/nxos_ntp_options.py | 4 ++++ network/nxos/nxos_nxapi.py | 4 ++++ network/nxos/nxos_ospf.py | 4 ++++ network/nxos/nxos_ospf_vrf.py | 4 ++++ network/nxos/nxos_overlay_global.py | 4 ++++ network/nxos/nxos_pim.py | 4 ++++ network/nxos/nxos_pim_interface.py | 4 ++++ network/nxos/nxos_pim_rp_address.py | 4 ++++ network/nxos/nxos_ping.py | 4 ++++ network/nxos/nxos_portchannel.py | 4 ++++ network/nxos/nxos_reboot.py | 4 ++++ network/nxos/nxos_rollback.py | 4 ++++ network/nxos/nxos_smu.py | 4 ++++ network/nxos/nxos_snapshot.py | 4 ++++ network/nxos/nxos_snmp_community.py | 4 ++++ network/nxos/nxos_snmp_contact.py | 4 ++++ network/nxos/nxos_snmp_host.py | 4 ++++ network/nxos/nxos_snmp_location.py | 4 ++++ network/nxos/nxos_snmp_traps.py | 4 ++++ network/nxos/nxos_snmp_user.py | 4 ++++ network/nxos/nxos_static_route.py | 4 ++++ network/nxos/nxos_switchport.py | 4 ++++ network/nxos/nxos_udld.py | 4 ++++ network/nxos/nxos_udld_interface.py | 4 ++++ network/nxos/nxos_vlan.py | 4 ++++ network/nxos/nxos_vpc.py | 4 ++++ network/nxos/nxos_vpc_interface.py | 4 ++++ network/nxos/nxos_vrf.py | 4 ++++ network/nxos/nxos_vrf_af.py | 4 ++++ network/nxos/nxos_vrf_interface.py | 4 ++++ network/nxos/nxos_vrrp.py | 4 ++++ network/nxos/nxos_vtp_domain.py | 4 ++++ network/nxos/nxos_vtp_password.py | 4 ++++ network/nxos/nxos_vtp_version.py | 4 ++++ network/nxos/nxos_vxlan_vtep.py | 4 ++++ network/nxos/nxos_vxlan_vtep_vni.py | 4 ++++ network/openswitch/_ops_template.py | 5 +++++ network/openswitch/ops_command.py | 4 ++++ network/openswitch/ops_config.py | 4 ++++ network/openswitch/ops_facts.py | 4 ++++ network/sros/sros_command.py | 4 ++++ network/sros/sros_config.py | 4 ++++ network/sros/sros_rollback.py | 4 ++++ network/vyos/vyos_command.py | 4 ++++ network/vyos/vyos_config.py | 4 ++++ network/vyos/vyos_facts.py | 4 ++++ packaging/language/easy_install.py | 4 ++++ packaging/language/gem.py | 4 ++++ packaging/language/pip.py | 4 ++++ packaging/os/apt.py | 4 ++++ packaging/os/apt_key.py | 4 ++++ packaging/os/apt_repository.py | 4 ++++ packaging/os/apt_rpm.py | 4 ++++ packaging/os/package.py | 4 ++++ packaging/os/redhat_subscription.py | 4 ++++ packaging/os/rhn_channel.py | 4 ++++ packaging/os/rhn_register.py | 4 ++++ packaging/os/rpm_key.py | 4 ++++ packaging/os/yum.py | 4 ++++ source_control/git.py | 4 ++++ source_control/hg.py | 4 ++++ source_control/subversion.py | 4 ++++ system/authorized_key.py | 4 ++++ system/cron.py | 4 ++++ system/group.py | 4 ++++ system/hostname.py | 4 ++++ system/mount.py | 4 ++++ system/ping.py | 4 ++++ system/seboolean.py | 4 ++++ system/selinux.py | 4 ++++ system/service.py | 4 ++++ system/setup.py | 4 ++++ system/sysctl.py | 4 ++++ system/systemd.py | 4 ++++ system/user.py | 4 ++++ utilities/helper/_accelerate.py | 4 ++++ utilities/helper/_fireball.py | 4 ++++ utilities/helper/meta.py | 4 ++++ utilities/logic/assert.py | 4 ++++ utilities/logic/async_status.py | 4 ++++ utilities/logic/debug.py | 4 ++++ utilities/logic/fail.py | 4 ++++ utilities/logic/include.py | 4 ++++ utilities/logic/include_role.py | 4 ++++ utilities/logic/include_vars.py | 4 ++++ utilities/logic/pause.py | 4 ++++ utilities/logic/set_fact.py | 4 ++++ utilities/logic/wait_for.py | 4 ++++ web_infrastructure/apache2_module.py | 4 ++++ web_infrastructure/django_manage.py | 4 ++++ web_infrastructure/htpasswd.py | 4 ++++ web_infrastructure/supervisorctl.py | 4 ++++ windows/win_command.py | 4 ++++ windows/win_copy.py | 4 ++++ windows/win_feature.py | 4 ++++ windows/win_file.py | 4 ++++ windows/win_get_url.py | 4 ++++ windows/win_group.py | 4 ++++ windows/win_lineinfile.py | 4 ++++ windows/win_msi.py | 4 ++++ windows/win_ping.py | 4 ++++ windows/win_reboot.py | 4 ++++ windows/win_service.py | 4 ++++ windows/win_shell.py | 4 ++++ windows/win_stat.py | 4 ++++ windows/win_template.py | 4 ++++ windows/win_user.py | 4 ++++ 334 files changed, 1341 insertions(+) diff --git a/cloud/amazon/_ec2_ami_search.py b/cloud/amazon/_ec2_ami_search.py index eae013cc882..a497ab851d9 100644 --- a/cloud/amazon/_ec2_ami_search.py +++ b/cloud/amazon/_ec2_ami_search.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_ami_search diff --git a/cloud/amazon/cloudformation.py b/cloud/amazon/cloudformation.py index be20b64074b..c05ae2df280 100644 --- a/cloud/amazon/cloudformation.py +++ b/cloud/amazon/cloudformation.py @@ -21,6 +21,10 @@ # - move create/update code out of main # - unit tests +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: cloudformation diff --git a/cloud/amazon/ec2.py b/cloud/amazon/ec2.py index 3ca9bf3751a..d19f06f3695 100644 --- a/cloud/amazon/ec2.py +++ b/cloud/amazon/ec2.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2 diff --git a/cloud/amazon/ec2_ami.py b/cloud/amazon/ec2_ami.py index 506a370cbce..de3a31c4a76 100644 --- a/cloud/amazon/ec2_ami.py +++ b/cloud/amazon/ec2_ami.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_ami diff --git a/cloud/amazon/ec2_ami_find.py b/cloud/amazon/ec2_ami_find.py index 5545766287a..c6b986bdd80 100644 --- a/cloud/amazon/ec2_ami_find.py +++ b/cloud/amazon/ec2_ami_find.py @@ -15,6 +15,10 @@ # 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: ec2_ami_find diff --git a/cloud/amazon/ec2_asg.py b/cloud/amazon/ec2_asg.py index 378aaddde38..be7c634d742 100644 --- a/cloud/amazon/ec2_asg.py +++ b/cloud/amazon/ec2_asg.py @@ -13,6 +13,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ec2_asg diff --git a/cloud/amazon/ec2_eip.py b/cloud/amazon/ec2_eip.py index 834730448ef..22d950f9fb6 100644 --- a/cloud/amazon/ec2_eip.py +++ b/cloud/amazon/ec2_eip.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_eip diff --git a/cloud/amazon/ec2_elb.py b/cloud/amazon/ec2_elb.py index 0aab5cc0828..cd2cf5fbae6 100644 --- a/cloud/amazon/ec2_elb.py +++ b/cloud/amazon/ec2_elb.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ec2_elb diff --git a/cloud/amazon/ec2_elb_lb.py b/cloud/amazon/ec2_elb_lb.py index 066719b84a2..ca87a1cb3ad 100644 --- a/cloud/amazon/ec2_elb_lb.py +++ b/cloud/amazon/ec2_elb_lb.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ec2_elb_lb diff --git a/cloud/amazon/ec2_facts.py b/cloud/amazon/ec2_facts.py index 7522658c348..498cf9c2dfe 100644 --- a/cloud/amazon/ec2_facts.py +++ b/cloud/amazon/ec2_facts.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_facts diff --git a/cloud/amazon/ec2_group.py b/cloud/amazon/ec2_group.py index 4e320d121cd..b381218f496 100644 --- a/cloud/amazon/ec2_group.py +++ b/cloud/amazon/ec2_group.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_group diff --git a/cloud/amazon/ec2_key.py b/cloud/amazon/ec2_key.py index 5adb2e95bb5..69d96fed977 100644 --- a/cloud/amazon/ec2_key.py +++ b/cloud/amazon/ec2_key.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_key diff --git a/cloud/amazon/ec2_lc.py b/cloud/amazon/ec2_lc.py index 97c959267c9..7a8754b8994 100644 --- a/cloud/amazon/ec2_lc.py +++ b/cloud/amazon/ec2_lc.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ec2_lc diff --git a/cloud/amazon/ec2_metric_alarm.py b/cloud/amazon/ec2_metric_alarm.py index d581b54f978..984211bc277 100644 --- a/cloud/amazon/ec2_metric_alarm.py +++ b/cloud/amazon/ec2_metric_alarm.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ module: ec2_metric_alarm short_description: "Create/update or delete AWS Cloudwatch 'metric alarms'" diff --git a/cloud/amazon/ec2_scaling_policy.py b/cloud/amazon/ec2_scaling_policy.py index 5ca8eafcc75..bea3bfbca8a 100644 --- a/cloud/amazon/ec2_scaling_policy.py +++ b/cloud/amazon/ec2_scaling_policy.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ module: ec2_scaling_policy short_description: Create or delete AWS scaling policies for Autoscaling groups diff --git a/cloud/amazon/ec2_snapshot.py b/cloud/amazon/ec2_snapshot.py index 0fa98a10d7a..b962e187608 100644 --- a/cloud/amazon/ec2_snapshot.py +++ b/cloud/amazon/ec2_snapshot.py @@ -14,6 +14,10 @@ # 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: ec2_snapshot diff --git a/cloud/amazon/ec2_tag.py b/cloud/amazon/ec2_tag.py index 76bba14b20b..0fe20e1786d 100644 --- a/cloud/amazon/ec2_tag.py +++ b/cloud/amazon/ec2_tag.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_tag diff --git a/cloud/amazon/ec2_vol.py b/cloud/amazon/ec2_vol.py index fb78365ff9f..cd76703f432 100644 --- a/cloud/amazon/ec2_vol.py +++ b/cloud/amazon/ec2_vol.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vol diff --git a/cloud/amazon/ec2_vpc.py b/cloud/amazon/ec2_vpc.py index 4ae2065b074..5b0cfc51b0e 100644 --- a/cloud/amazon/ec2_vpc.py +++ b/cloud/amazon/ec2_vpc.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc diff --git a/cloud/amazon/ec2_vpc_net.py b/cloud/amazon/ec2_vpc_net.py index 54be37028a5..7b7e0e46477 100644 --- a/cloud/amazon/ec2_vpc_net.py +++ b/cloud/amazon/ec2_vpc_net.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ec2_vpc_net diff --git a/cloud/amazon/elasticache.py b/cloud/amazon/elasticache.py index b0457ace4e9..00098b171ef 100644 --- a/cloud/amazon/elasticache.py +++ b/cloud/amazon/elasticache.py @@ -14,6 +14,10 @@ # 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: elasticache diff --git a/cloud/amazon/elasticache_subnet_group.py b/cloud/amazon/elasticache_subnet_group.py index 7fbdfec9ff4..1e5708c03e3 100644 --- a/cloud/amazon/elasticache_subnet_group.py +++ b/cloud/amazon/elasticache_subnet_group.py @@ -14,6 +14,10 @@ # 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: elasticache_subnet_group diff --git a/cloud/amazon/iam.py b/cloud/amazon/iam.py index 8c10bd09189..79a3e346221 100644 --- a/cloud/amazon/iam.py +++ b/cloud/amazon/iam.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: iam diff --git a/cloud/amazon/iam_cert.py b/cloud/amazon/iam_cert.py index dbc4fcb476e..6e71b5552f5 100644 --- a/cloud/amazon/iam_cert.py +++ b/cloud/amazon/iam_cert.py @@ -13,6 +13,10 @@ # # 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: iam_cert diff --git a/cloud/amazon/iam_policy.py b/cloud/amazon/iam_policy.py index 5391294beab..97be3f40512 100644 --- a/cloud/amazon/iam_policy.py +++ b/cloud/amazon/iam_policy.py @@ -13,6 +13,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: iam_policy diff --git a/cloud/amazon/rds.py b/cloud/amazon/rds.py index 1e33f9c47b4..41438da1843 100644 --- a/cloud/amazon/rds.py +++ b/cloud/amazon/rds.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rds diff --git a/cloud/amazon/rds_param_group.py b/cloud/amazon/rds_param_group.py index 1d863b1a379..154fed391aa 100644 --- a/cloud/amazon/rds_param_group.py +++ b/cloud/amazon/rds_param_group.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rds_param_group diff --git a/cloud/amazon/rds_subnet_group.py b/cloud/amazon/rds_subnet_group.py index e99b3d8d479..bec08cf61d6 100644 --- a/cloud/amazon/rds_subnet_group.py +++ b/cloud/amazon/rds_subnet_group.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rds_subnet_group diff --git a/cloud/amazon/route53.py b/cloud/amazon/route53.py index 9c0f19bffa3..6452fefb359 100644 --- a/cloud/amazon/route53.py +++ b/cloud/amazon/route53.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: route53 diff --git a/cloud/amazon/s3.py b/cloud/amazon/s3.py index d2b4912faa9..9974a4f467e 100755 --- a/cloud/amazon/s3.py +++ b/cloud/amazon/s3.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: s3 diff --git a/cloud/azure/azure.py b/cloud/azure/azure.py index 226a8c07253..60cdbbe0479 100644 --- a/cloud/azure/azure.py +++ b/cloud/azure/azure.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure diff --git a/cloud/azure/azure_rm_networkinterface.py b/cloud/azure/azure_rm_networkinterface.py index 742306ddb9f..651da3caadd 100644 --- a/cloud/azure/azure_rm_networkinterface.py +++ b/cloud/azure/azure_rm_networkinterface.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_networkinterface diff --git a/cloud/azure/azure_rm_networkinterface_facts.py b/cloud/azure/azure_rm_networkinterface_facts.py index 07e0e2bd0a1..b82a9bf690b 100644 --- a/cloud/azure/azure_rm_networkinterface_facts.py +++ b/cloud/azure/azure_rm_networkinterface_facts.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_networkinterface_facts diff --git a/cloud/azure/azure_rm_publicipaddress.py b/cloud/azure/azure_rm_publicipaddress.py index fd5f07b9f30..7aa05d4ed5e 100644 --- a/cloud/azure/azure_rm_publicipaddress.py +++ b/cloud/azure/azure_rm_publicipaddress.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_publicipaddress diff --git a/cloud/azure/azure_rm_publicipaddress_facts.py b/cloud/azure/azure_rm_publicipaddress_facts.py index a9fc27bab5c..d6b443756a3 100644 --- a/cloud/azure/azure_rm_publicipaddress_facts.py +++ b/cloud/azure/azure_rm_publicipaddress_facts.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_publicip_facts diff --git a/cloud/azure/azure_rm_resourcegroup.py b/cloud/azure/azure_rm_resourcegroup.py index 66c5eb52b09..34a01afef05 100644 --- a/cloud/azure/azure_rm_resourcegroup.py +++ b/cloud/azure/azure_rm_resourcegroup.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_resourcegroup diff --git a/cloud/azure/azure_rm_resourcegroup_facts.py b/cloud/azure/azure_rm_resourcegroup_facts.py index 01278794731..a6f7d1c7376 100644 --- a/cloud/azure/azure_rm_resourcegroup_facts.py +++ b/cloud/azure/azure_rm_resourcegroup_facts.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_resouregroup_facts diff --git a/cloud/azure/azure_rm_securitygroup.py b/cloud/azure/azure_rm_securitygroup.py index db24bacd409..de2569d0941 100644 --- a/cloud/azure/azure_rm_securitygroup.py +++ b/cloud/azure/azure_rm_securitygroup.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_securitygroup diff --git a/cloud/azure/azure_rm_securitygroup_facts.py b/cloud/azure/azure_rm_securitygroup_facts.py index a4ba06a3810..66c3cd20da6 100644 --- a/cloud/azure/azure_rm_securitygroup_facts.py +++ b/cloud/azure/azure_rm_securitygroup_facts.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_securitygroup_facts diff --git a/cloud/azure/azure_rm_storageaccount.py b/cloud/azure/azure_rm_storageaccount.py index 99f92ece745..b71db78e96e 100644 --- a/cloud/azure/azure_rm_storageaccount.py +++ b/cloud/azure/azure_rm_storageaccount.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_storageaccount diff --git a/cloud/azure/azure_rm_storageaccount_facts.py b/cloud/azure/azure_rm_storageaccount_facts.py index 386647ddf66..bbc18eb0ad5 100644 --- a/cloud/azure/azure_rm_storageaccount_facts.py +++ b/cloud/azure/azure_rm_storageaccount_facts.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_storageaccount_facts diff --git a/cloud/azure/azure_rm_storageblob.py b/cloud/azure/azure_rm_storageblob.py index 4b5a2e078c7..9ff235fd961 100644 --- a/cloud/azure/azure_rm_storageblob.py +++ b/cloud/azure/azure_rm_storageblob.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_storageblob diff --git a/cloud/azure/azure_rm_subnet.py b/cloud/azure/azure_rm_subnet.py index d11c35f2e5b..5155715ac28 100644 --- a/cloud/azure/azure_rm_subnet.py +++ b/cloud/azure/azure_rm_subnet.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_subnet diff --git a/cloud/azure/azure_rm_virtualmachine.py b/cloud/azure/azure_rm_virtualmachine.py index a237a8f88ce..40010df8eba 100644 --- a/cloud/azure/azure_rm_virtualmachine.py +++ b/cloud/azure/azure_rm_virtualmachine.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_virtualmachine diff --git a/cloud/azure/azure_rm_virtualmachineimage_facts.py b/cloud/azure/azure_rm_virtualmachineimage_facts.py index 45aa8bbacdf..3efdc57db39 100644 --- a/cloud/azure/azure_rm_virtualmachineimage_facts.py +++ b/cloud/azure/azure_rm_virtualmachineimage_facts.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_virtualmachineimage_facts diff --git a/cloud/azure/azure_rm_virtualnetwork.py b/cloud/azure/azure_rm_virtualnetwork.py index 417e999b635..97494444eea 100644 --- a/cloud/azure/azure_rm_virtualnetwork.py +++ b/cloud/azure/azure_rm_virtualnetwork.py @@ -20,6 +20,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_virtualnetwork diff --git a/cloud/azure/azure_rm_virtualnetwork_facts.py b/cloud/azure/azure_rm_virtualnetwork_facts.py index 5f9f94c8097..229df95b74c 100644 --- a/cloud/azure/azure_rm_virtualnetwork_facts.py +++ b/cloud/azure/azure_rm_virtualnetwork_facts.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: azure_rm_virtualnetwork_facts diff --git a/cloud/digital_ocean/digital_ocean.py b/cloud/digital_ocean/digital_ocean.py index d2c894cc340..2cdcbd6420c 100644 --- a/cloud/digital_ocean/digital_ocean.py +++ b/cloud/digital_ocean/digital_ocean.py @@ -15,6 +15,10 @@ # # 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: digital_ocean diff --git a/cloud/digital_ocean/digital_ocean_block_storage.py b/cloud/digital_ocean/digital_ocean_block_storage.py index 42c9df73d13..73a76cd737b 100644 --- a/cloud/digital_ocean/digital_ocean_block_storage.py +++ b/cloud/digital_ocean/digital_ocean_block_storage.py @@ -16,6 +16,10 @@ # 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: digital_ocean_block_storage diff --git a/cloud/digital_ocean/digital_ocean_domain.py b/cloud/digital_ocean/digital_ocean_domain.py index b91e47ffb67..61fc6c8eeac 100644 --- a/cloud/digital_ocean/digital_ocean_domain.py +++ b/cloud/digital_ocean/digital_ocean_domain.py @@ -15,6 +15,10 @@ # # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: digital_ocean_domain diff --git a/cloud/digital_ocean/digital_ocean_sshkey.py b/cloud/digital_ocean/digital_ocean_sshkey.py index e15822dc94b..00c31c788e9 100644 --- a/cloud/digital_ocean/digital_ocean_sshkey.py +++ b/cloud/digital_ocean/digital_ocean_sshkey.py @@ -15,6 +15,10 @@ # # 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: digital_ocean_sshkey diff --git a/cloud/digital_ocean/digital_ocean_tag.py b/cloud/digital_ocean/digital_ocean_tag.py index 825b57db21d..3ad387a90c3 100644 --- a/cloud/digital_ocean/digital_ocean_tag.py +++ b/cloud/digital_ocean/digital_ocean_tag.py @@ -15,6 +15,10 @@ # # 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: digital_ocean_tag diff --git a/cloud/docker/_docker.py b/cloud/docker/_docker.py index 071e7853987..08adf3b9071 100644 --- a/cloud/docker/_docker.py +++ b/cloud/docker/_docker.py @@ -21,6 +21,10 @@ ###################################################################### +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: docker diff --git a/cloud/docker/docker_container.py b/cloud/docker/docker_container.py index d6811e00eab..f921ce47177 100644 --- a/cloud/docker/docker_container.py +++ b/cloud/docker/docker_container.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: docker_container diff --git a/cloud/docker/docker_image.py b/cloud/docker/docker_image.py index df9df1dd2c5..0de16632e03 100644 --- a/cloud/docker/docker_image.py +++ b/cloud/docker/docker_image.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: docker_image diff --git a/cloud/docker/docker_image_facts.py b/cloud/docker/docker_image_facts.py index 600491ec52e..81e16fe5e2a 100644 --- a/cloud/docker/docker_image_facts.py +++ b/cloud/docker/docker_image_facts.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: docker_image_facts diff --git a/cloud/docker/docker_login.py b/cloud/docker/docker_login.py index bee5be46e67..f0b3c87ec39 100644 --- a/cloud/docker/docker_login.py +++ b/cloud/docker/docker_login.py @@ -21,6 +21,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: docker_login diff --git a/cloud/docker/docker_network.py b/cloud/docker/docker_network.py index 6b8056aafce..24ce4dc6a40 100644 --- a/cloud/docker/docker_network.py +++ b/cloud/docker/docker_network.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' module: docker_network version_added: "2.2" diff --git a/cloud/docker/docker_service.py b/cloud/docker/docker_service.py index 33a31cf7679..ee7c35329f4 100644 --- a/cloud/docker/docker_service.py +++ b/cloud/docker/docker_service.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' module: docker_service diff --git a/cloud/google/gc_storage.py b/cloud/google/gc_storage.py index a032c63c7f8..6a5e9023d02 100644 --- a/cloud/google/gc_storage.py +++ b/cloud/google/gc_storage.py @@ -14,6 +14,10 @@ # 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: gc_storage diff --git a/cloud/google/gce.py b/cloud/google/gce.py index aa06d9c8e81..802a7a1393c 100644 --- a/cloud/google/gce.py +++ b/cloud/google/gce.py @@ -16,6 +16,10 @@ # 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: gce diff --git a/cloud/google/gce_lb.py b/cloud/google/gce_lb.py index 32233d4b36b..dc31bb3b421 100644 --- a/cloud/google/gce_lb.py +++ b/cloud/google/gce_lb.py @@ -16,6 +16,10 @@ # 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: gce_lb diff --git a/cloud/google/gce_mig.py b/cloud/google/gce_mig.py index 37b17de0ba8..024055dff30 100644 --- a/cloud/google/gce_mig.py +++ b/cloud/google/gce_mig.py @@ -16,6 +16,10 @@ # 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: gce_mig diff --git a/cloud/google/gce_net.py b/cloud/google/gce_net.py index 128fdb41291..aec0a294271 100644 --- a/cloud/google/gce_net.py +++ b/cloud/google/gce_net.py @@ -16,6 +16,10 @@ # 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: gce_net diff --git a/cloud/google/gce_pd.py b/cloud/google/gce_pd.py index f71442b16a1..a645806712b 100644 --- a/cloud/google/gce_pd.py +++ b/cloud/google/gce_pd.py @@ -16,6 +16,10 @@ # 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: gce_pd diff --git a/cloud/linode/linode.py b/cloud/linode/linode.py index 079feea39f1..7de16bbb2b9 100644 --- a/cloud/linode/linode.py +++ b/cloud/linode/linode.py @@ -14,6 +14,10 @@ # 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: linode diff --git a/cloud/openstack/_glance_image.py b/cloud/openstack/_glance_image.py index bc113b09030..a97255241ae 100644 --- a/cloud/openstack/_glance_image.py +++ b/cloud/openstack/_glance_image.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: glance_image diff --git a/cloud/openstack/_keystone_user.py b/cloud/openstack/_keystone_user.py index 72bf4dcf5b8..250c8936bed 100644 --- a/cloud/openstack/_keystone_user.py +++ b/cloud/openstack/_keystone_user.py @@ -17,6 +17,10 @@ # Based on Jimmy Tang's implementation +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: keystone_user diff --git a/cloud/openstack/_nova_compute.py b/cloud/openstack/_nova_compute.py index 634e847485e..0bea21048f5 100644 --- a/cloud/openstack/_nova_compute.py +++ b/cloud/openstack/_nova_compute.py @@ -30,6 +30,10 @@ except ImportError: HAS_NOVACLIENT = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nova_compute diff --git a/cloud/openstack/_nova_keypair.py b/cloud/openstack/_nova_keypair.py index cabe35ea87d..914db91bf2f 100644 --- a/cloud/openstack/_nova_keypair.py +++ b/cloud/openstack/_nova_keypair.py @@ -25,6 +25,10 @@ except ImportError: HAS_NOVACLIENT = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nova_keypair diff --git a/cloud/openstack/_quantum_floating_ip.py b/cloud/openstack/_quantum_floating_ip.py index f89f632e3f4..9c72c431d0b 100644 --- a/cloud/openstack/_quantum_floating_ip.py +++ b/cloud/openstack/_quantum_floating_ip.py @@ -29,6 +29,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_floating_ip diff --git a/cloud/openstack/_quantum_floating_ip_associate.py b/cloud/openstack/_quantum_floating_ip_associate.py index 3a87af9c4f9..f7eed5fe861 100644 --- a/cloud/openstack/_quantum_floating_ip_associate.py +++ b/cloud/openstack/_quantum_floating_ip_associate.py @@ -28,6 +28,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_floating_ip_associate diff --git a/cloud/openstack/_quantum_network.py b/cloud/openstack/_quantum_network.py index 1e62e561333..db82e90d335 100644 --- a/cloud/openstack/_quantum_network.py +++ b/cloud/openstack/_quantum_network.py @@ -26,6 +26,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_network diff --git a/cloud/openstack/_quantum_router.py b/cloud/openstack/_quantum_router.py index 619a4cf86c7..c65f916d6b1 100644 --- a/cloud/openstack/_quantum_router.py +++ b/cloud/openstack/_quantum_router.py @@ -26,6 +26,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_router diff --git a/cloud/openstack/_quantum_router_gateway.py b/cloud/openstack/_quantum_router_gateway.py index 6cdc4424b3f..af6179bc62d 100644 --- a/cloud/openstack/_quantum_router_gateway.py +++ b/cloud/openstack/_quantum_router_gateway.py @@ -26,6 +26,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_router_gateway diff --git a/cloud/openstack/_quantum_router_interface.py b/cloud/openstack/_quantum_router_interface.py index aba6d0cbac4..b2a1784d99a 100644 --- a/cloud/openstack/_quantum_router_interface.py +++ b/cloud/openstack/_quantum_router_interface.py @@ -26,6 +26,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_router_interface diff --git a/cloud/openstack/_quantum_subnet.py b/cloud/openstack/_quantum_subnet.py index 57349558115..e03f5962295 100644 --- a/cloud/openstack/_quantum_subnet.py +++ b/cloud/openstack/_quantum_subnet.py @@ -26,6 +26,10 @@ except ImportError: HAVE_DEPS = False +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: quantum_subnet diff --git a/cloud/openstack/os_auth.py b/cloud/openstack/os_auth.py index f4cdea432e1..bcc95aaaa1f 100644 --- a/cloud/openstack/os_auth.py +++ b/cloud/openstack/os_auth.py @@ -22,6 +22,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_auth diff --git a/cloud/openstack/os_client_config.py b/cloud/openstack/os_client_config.py index ba524a5b0b5..cc840085f67 100644 --- a/cloud/openstack/os_client_config.py +++ b/cloud/openstack/os_client_config.py @@ -18,6 +18,10 @@ import os_client_config from os_client_config import exceptions +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_client_config diff --git a/cloud/openstack/os_floating_ip.py b/cloud/openstack/os_floating_ip.py index 1198b6a4699..16c217c2d86 100644 --- a/cloud/openstack/os_floating_ip.py +++ b/cloud/openstack/os_floating_ip.py @@ -26,6 +26,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_floating_ip diff --git a/cloud/openstack/os_image.py b/cloud/openstack/os_image.py index 3aa49583f11..7632672555c 100644 --- a/cloud/openstack/os_image.py +++ b/cloud/openstack/os_image.py @@ -24,6 +24,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_image diff --git a/cloud/openstack/os_image_facts.py b/cloud/openstack/os_image_facts.py index 9fe906da7db..a810ad467f3 100644 --- a/cloud/openstack/os_image_facts.py +++ b/cloud/openstack/os_image_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: os_image_facts short_description: Retrieve facts about an image within OpenStack. diff --git a/cloud/openstack/os_ironic.py b/cloud/openstack/os_ironic.py index 8537a378c66..2296082f321 100644 --- a/cloud/openstack/os_ironic.py +++ b/cloud/openstack/os_ironic.py @@ -23,6 +23,10 @@ HAS_SHADE = False import jsonpatch +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_ironic diff --git a/cloud/openstack/os_ironic_node.py b/cloud/openstack/os_ironic_node.py index 17e3b1578eb..fa41d6fcbc0 100644 --- a/cloud/openstack/os_ironic_node.py +++ b/cloud/openstack/os_ironic_node.py @@ -24,6 +24,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_ironic_node diff --git a/cloud/openstack/os_keypair.py b/cloud/openstack/os_keypair.py index cf2b0a808ae..8651901a2af 100644 --- a/cloud/openstack/os_keypair.py +++ b/cloud/openstack/os_keypair.py @@ -25,6 +25,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_keypair diff --git a/cloud/openstack/os_network.py b/cloud/openstack/os_network.py index 5220dba5eba..39f0afa219b 100644 --- a/cloud/openstack/os_network.py +++ b/cloud/openstack/os_network.py @@ -25,6 +25,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_network diff --git a/cloud/openstack/os_networks_facts.py b/cloud/openstack/os_networks_facts.py index 6eb73f9220c..f39580321ee 100644 --- a/cloud/openstack/os_networks_facts.py +++ b/cloud/openstack/os_networks_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_networks_facts diff --git a/cloud/openstack/os_nova_flavor.py b/cloud/openstack/os_nova_flavor.py index 8dd939bc8c2..0f9f5afa91b 100644 --- a/cloud/openstack/os_nova_flavor.py +++ b/cloud/openstack/os_nova_flavor.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_nova_flavor diff --git a/cloud/openstack/os_object.py b/cloud/openstack/os_object.py index d386d85330e..9e67ab39dff 100644 --- a/cloud/openstack/os_object.py +++ b/cloud/openstack/os_object.py @@ -23,6 +23,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_object diff --git a/cloud/openstack/os_port.py b/cloud/openstack/os_port.py index e31f4fa0e1d..56625a63f56 100644 --- a/cloud/openstack/os_port.py +++ b/cloud/openstack/os_port.py @@ -22,6 +22,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_port diff --git a/cloud/openstack/os_router.py b/cloud/openstack/os_router.py index cb492feed1f..d4a6b28bc82 100644 --- a/cloud/openstack/os_router.py +++ b/cloud/openstack/os_router.py @@ -22,6 +22,10 @@ from distutils.version import StrictVersion +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_router diff --git a/cloud/openstack/os_security_group.py b/cloud/openstack/os_security_group.py index e5d0b8996a8..3ed5dfceb7a 100644 --- a/cloud/openstack/os_security_group.py +++ b/cloud/openstack/os_security_group.py @@ -23,6 +23,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_security_group diff --git a/cloud/openstack/os_security_group_rule.py b/cloud/openstack/os_security_group_rule.py index 5cb1418acce..3379d160408 100644 --- a/cloud/openstack/os_security_group_rule.py +++ b/cloud/openstack/os_security_group_rule.py @@ -23,6 +23,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_security_group_rule diff --git a/cloud/openstack/os_server.py b/cloud/openstack/os_server.py index d6d74e1fef0..0bb7dbcfbc6 100644 --- a/cloud/openstack/os_server.py +++ b/cloud/openstack/os_server.py @@ -27,6 +27,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_server diff --git a/cloud/openstack/os_server_actions.py b/cloud/openstack/os_server_actions.py index e298fb9ea29..ae37e358a84 100644 --- a/cloud/openstack/os_server_actions.py +++ b/cloud/openstack/os_server_actions.py @@ -25,6 +25,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_server_actions diff --git a/cloud/openstack/os_server_facts.py b/cloud/openstack/os_server_facts.py index bcda1d4f78f..efeb7780691 100644 --- a/cloud/openstack/os_server_facts.py +++ b/cloud/openstack/os_server_facts.py @@ -24,6 +24,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_server_facts diff --git a/cloud/openstack/os_server_volume.py b/cloud/openstack/os_server_volume.py index e71e0954a20..a6549649d87 100644 --- a/cloud/openstack/os_server_volume.py +++ b/cloud/openstack/os_server_volume.py @@ -25,6 +25,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_server_volume diff --git a/cloud/openstack/os_subnet.py b/cloud/openstack/os_subnet.py index 3330af2a267..54b12adf327 100644 --- a/cloud/openstack/os_subnet.py +++ b/cloud/openstack/os_subnet.py @@ -23,6 +23,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_subnet diff --git a/cloud/openstack/os_subnets_facts.py b/cloud/openstack/os_subnets_facts.py index 3fe0b80ed1d..b4beedab5a1 100644 --- a/cloud/openstack/os_subnets_facts.py +++ b/cloud/openstack/os_subnets_facts.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_subnets_facts diff --git a/cloud/openstack/os_user.py b/cloud/openstack/os_user.py index 831f2fa9dee..2ef4fe75e94 100644 --- a/cloud/openstack/os_user.py +++ b/cloud/openstack/os_user.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_user diff --git a/cloud/openstack/os_user_group.py b/cloud/openstack/os_user_group.py index 9d21a7f033f..2f39f346792 100644 --- a/cloud/openstack/os_user_group.py +++ b/cloud/openstack/os_user_group.py @@ -21,6 +21,10 @@ except ImportError: HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_user_group diff --git a/cloud/openstack/os_volume.py b/cloud/openstack/os_volume.py index 9e7436e1a5f..6d6cc08d749 100644 --- a/cloud/openstack/os_volume.py +++ b/cloud/openstack/os_volume.py @@ -23,6 +23,10 @@ HAS_SHADE = False +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: os_volume diff --git a/cloud/rackspace/rax.py b/cloud/rackspace/rax.py index 31b34f8c45d..e9a1fd48768 100644 --- a/cloud/rackspace/rax.py +++ b/cloud/rackspace/rax.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax diff --git a/cloud/rackspace/rax_cbs.py b/cloud/rackspace/rax_cbs.py index 597ef1bfe72..a09ce53def1 100644 --- a/cloud/rackspace/rax_cbs.py +++ b/cloud/rackspace/rax_cbs.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_cbs diff --git a/cloud/rackspace/rax_cbs_attachments.py b/cloud/rackspace/rax_cbs_attachments.py index 54e261be511..0c8032b35eb 100644 --- a/cloud/rackspace/rax_cbs_attachments.py +++ b/cloud/rackspace/rax_cbs_attachments.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_cbs_attachments diff --git a/cloud/rackspace/rax_cdb.py b/cloud/rackspace/rax_cdb.py index 47af20ffc90..4706457ae61 100644 --- a/cloud/rackspace/rax_cdb.py +++ b/cloud/rackspace/rax_cdb.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_cdb diff --git a/cloud/rackspace/rax_cdb_database.py b/cloud/rackspace/rax_cdb_database.py index 6a5e2e86e44..d2f061d4a93 100644 --- a/cloud/rackspace/rax_cdb_database.py +++ b/cloud/rackspace/rax_cdb_database.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' module: rax_cdb_database short_description: 'create / delete a database in the Cloud Databases' diff --git a/cloud/rackspace/rax_cdb_user.py b/cloud/rackspace/rax_cdb_user.py index 6d7ae27ec70..7fa1bc50485 100644 --- a/cloud/rackspace/rax_cdb_user.py +++ b/cloud/rackspace/rax_cdb_user.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_cdb_user diff --git a/cloud/rackspace/rax_clb.py b/cloud/rackspace/rax_clb.py index 8eae3a5bba9..9d4d75c2291 100644 --- a/cloud/rackspace/rax_clb.py +++ b/cloud/rackspace/rax_clb.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_clb diff --git a/cloud/rackspace/rax_clb_nodes.py b/cloud/rackspace/rax_clb_nodes.py index e638bb0ee84..844834d7476 100644 --- a/cloud/rackspace/rax_clb_nodes.py +++ b/cloud/rackspace/rax_clb_nodes.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_clb_nodes diff --git a/cloud/rackspace/rax_dns.py b/cloud/rackspace/rax_dns.py index fa509802b36..eb62eaac356 100644 --- a/cloud/rackspace/rax_dns.py +++ b/cloud/rackspace/rax_dns.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_dns diff --git a/cloud/rackspace/rax_dns_record.py b/cloud/rackspace/rax_dns_record.py index 1d8b8a01b71..1499b09eb68 100644 --- a/cloud/rackspace/rax_dns_record.py +++ b/cloud/rackspace/rax_dns_record.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_dns_record diff --git a/cloud/rackspace/rax_facts.py b/cloud/rackspace/rax_facts.py index 14e17ef32d8..8c49e8df988 100644 --- a/cloud/rackspace/rax_facts.py +++ b/cloud/rackspace/rax_facts.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_facts diff --git a/cloud/rackspace/rax_files.py b/cloud/rackspace/rax_files.py index 3eb9dad0390..aac6b8d5bf1 100644 --- a/cloud/rackspace/rax_files.py +++ b/cloud/rackspace/rax_files.py @@ -19,6 +19,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_files diff --git a/cloud/rackspace/rax_files_objects.py b/cloud/rackspace/rax_files_objects.py index fb1638bce0d..a1124913aef 100644 --- a/cloud/rackspace/rax_files_objects.py +++ b/cloud/rackspace/rax_files_objects.py @@ -19,6 +19,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_files_objects diff --git a/cloud/rackspace/rax_identity.py b/cloud/rackspace/rax_identity.py index 9473585c169..baa856447a3 100644 --- a/cloud/rackspace/rax_identity.py +++ b/cloud/rackspace/rax_identity.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_identity diff --git a/cloud/rackspace/rax_keypair.py b/cloud/rackspace/rax_keypair.py index bbc077a4504..5fab5ca79a5 100644 --- a/cloud/rackspace/rax_keypair.py +++ b/cloud/rackspace/rax_keypair.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_keypair diff --git a/cloud/rackspace/rax_meta.py b/cloud/rackspace/rax_meta.py index 5a177905187..18bce5a4f4e 100644 --- a/cloud/rackspace/rax_meta.py +++ b/cloud/rackspace/rax_meta.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_meta diff --git a/cloud/rackspace/rax_network.py b/cloud/rackspace/rax_network.py index 257e0cac265..7ff6edd0eca 100644 --- a/cloud/rackspace/rax_network.py +++ b/cloud/rackspace/rax_network.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_network diff --git a/cloud/rackspace/rax_queue.py b/cloud/rackspace/rax_queue.py index bfa7626ac75..a1112460db8 100644 --- a/cloud/rackspace/rax_queue.py +++ b/cloud/rackspace/rax_queue.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_queue diff --git a/cloud/rackspace/rax_scaling_group.py b/cloud/rackspace/rax_scaling_group.py index 74ee298cbba..95aef91cc57 100644 --- a/cloud/rackspace/rax_scaling_group.py +++ b/cloud/rackspace/rax_scaling_group.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_scaling_group diff --git a/cloud/rackspace/rax_scaling_policy.py b/cloud/rackspace/rax_scaling_policy.py index 8533261c2a8..c56cee50dd2 100644 --- a/cloud/rackspace/rax_scaling_policy.py +++ b/cloud/rackspace/rax_scaling_policy.py @@ -16,6 +16,10 @@ # This is a DOCUMENTATION stub specific to this module, it extends # a documentation fragment located in ansible.utils.module_docs_fragments +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rax_scaling_policy diff --git a/cloud/vmware/vsphere_guest.py b/cloud/vmware/vsphere_guest.py index d077803fc7d..5425db6f89e 100644 --- a/cloud/vmware/vsphere_guest.py +++ b/cloud/vmware/vsphere_guest.py @@ -37,6 +37,10 @@ import ssl +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: vsphere_guest diff --git a/commands/command.py b/commands/command.py index 6c75d3f0567..9b8afe3ef58 100644 --- a/commands/command.py +++ b/commands/command.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: command diff --git a/commands/raw.py b/commands/raw.py index 36c3b38f430..3d6f315624e 100644 --- a/commands/raw.py +++ b/commands/raw.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: raw diff --git a/commands/script.py b/commands/script.py index be8d9a1b437..47a6571455f 100644 --- a/commands/script.py +++ b/commands/script.py @@ -13,6 +13,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: script diff --git a/commands/shell.py b/commands/shell.py index ca17ddda775..93d187b81ec 100644 --- a/commands/shell.py +++ b/commands/shell.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: shell diff --git a/database/mysql/mysql_db.py b/database/mysql/mysql_db.py index 437288425f8..b3ae64f206c 100644 --- a/database/mysql/mysql_db.py +++ b/database/mysql/mysql_db.py @@ -19,6 +19,10 @@ # 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: mysql_db diff --git a/database/mysql/mysql_user.py b/database/mysql/mysql_user.py index f3231c2f93e..286106fe711 100644 --- a/database/mysql/mysql_user.py +++ b/database/mysql/mysql_user.py @@ -18,6 +18,10 @@ # 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: mysql_user diff --git a/database/mysql/mysql_variables.py b/database/mysql/mysql_variables.py index 014b768dabc..506ff705d56 100644 --- a/database/mysql/mysql_variables.py +++ b/database/mysql/mysql_variables.py @@ -22,6 +22,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mysql_variables diff --git a/database/postgresql/postgresql_db.py b/database/postgresql/postgresql_db.py index 70cc96dc8fe..ffd22060096 100755 --- a/database/postgresql/postgresql_db.py +++ b/database/postgresql/postgresql_db.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: postgresql_db diff --git a/database/postgresql/postgresql_privs.py b/database/postgresql/postgresql_privs.py index ea49a55fc6f..ae606464dc9 100644 --- a/database/postgresql/postgresql_privs.py +++ b/database/postgresql/postgresql_privs.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: postgresql_privs diff --git a/database/postgresql/postgresql_user.py b/database/postgresql/postgresql_user.py index 1cdc92ef943..95c19caaba9 100644 --- a/database/postgresql/postgresql_user.py +++ b/database/postgresql/postgresql_user.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: postgresql_user diff --git a/files/acl.py b/files/acl.py index fda245d4080..ebd46d53c9b 100644 --- a/files/acl.py +++ b/files/acl.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: acl diff --git a/files/assemble.py b/files/assemble.py index d1d1bb34d35..41e7530e446 100644 --- a/files/assemble.py +++ b/files/assemble.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: assemble diff --git a/files/copy.py b/files/copy.py index 000b8ff269e..f9bbd1baa4f 100644 --- a/files/copy.py +++ b/files/copy.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: copy diff --git a/files/fetch.py b/files/fetch.py index 9b3f07827be..f069a23acfb 100644 --- a/files/fetch.py +++ b/files/fetch.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: fetch diff --git a/files/file.py b/files/file.py index f0947cfbd70..ef98f036e8b 100644 --- a/files/file.py +++ b/files/file.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: file diff --git a/files/find.py b/files/find.py index f6bfbebe64c..e2988aa36e9 100644 --- a/files/find.py +++ b/files/find.py @@ -26,6 +26,10 @@ import time import re +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: find diff --git a/files/ini_file.py b/files/ini_file.py index 69f092a1630..f9f08ac5697 100644 --- a/files/ini_file.py +++ b/files/ini_file.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ini_file diff --git a/files/lineinfile.py b/files/lineinfile.py index 4b6088c223a..ba7f4a3cc16 100644 --- a/files/lineinfile.py +++ b/files/lineinfile.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: lineinfile diff --git a/files/replace.py b/files/replace.py index d4e3ab9f0c5..85d6d91e1f7 100644 --- a/files/replace.py +++ b/files/replace.py @@ -22,6 +22,10 @@ import os import tempfile +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: replace diff --git a/files/stat.py b/files/stat.py index 363e94ad3f7..8cccae1d685 100644 --- a/files/stat.py +++ b/files/stat.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: stat diff --git a/files/synchronize.py b/files/synchronize.py index eb813b72f8c..4a7933adf1d 100644 --- a/files/synchronize.py +++ b/files/synchronize.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: synchronize diff --git a/files/template.py b/files/template.py index c7ff655cdcd..c603ec8f428 100644 --- a/files/template.py +++ b/files/template.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: template diff --git a/files/unarchive.py b/files/unarchive.py index 3a69cc33b67..c919d1f0ae8 100644 --- a/files/unarchive.py +++ b/files/unarchive.py @@ -21,6 +21,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: unarchive diff --git a/files/xattr.py b/files/xattr.py index 02af9f974ff..0ae74efbaa9 100644 --- a/files/xattr.py +++ b/files/xattr.py @@ -14,6 +14,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: xattr diff --git a/inventory/add_host.py b/inventory/add_host.py index 91363121f5c..44ae5303cc3 100644 --- a/inventory/add_host.py +++ b/inventory/add_host.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: add_host diff --git a/inventory/group_by.py b/inventory/group_by.py index 28e9a41ebc8..c7cb6a034ba 100644 --- a/inventory/group_by.py +++ b/inventory/group_by.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: group_by diff --git a/network/basics/get_url.py b/network/basics/get_url.py index e7a5860b310..a15b78df4fe 100644 --- a/network/basics/get_url.py +++ b/network/basics/get_url.py @@ -25,6 +25,10 @@ import re import tempfile +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: get_url diff --git a/network/basics/slurp.py b/network/basics/slurp.py index 991e0be05e7..f2ece413da8 100644 --- a/network/basics/slurp.py +++ b/network/basics/slurp.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: slurp diff --git a/network/basics/uri.py b/network/basics/uri.py index 0c1e9b673f2..24257dc3566 100644 --- a/network/basics/uri.py +++ b/network/basics/uri.py @@ -20,6 +20,10 @@ # # see examples/playbooks/uri.yml +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: uri diff --git a/network/cumulus/cl_bond.py b/network/cumulus/cl_bond.py index 958edc7fe30..baf9b0fe844 100644 --- a/network/cumulus/cl_bond.py +++ b/network/cumulus/cl_bond.py @@ -15,6 +15,10 @@ # 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: cl_bond diff --git a/network/cumulus/cl_bridge.py b/network/cumulus/cl_bridge.py index abeb0758a78..e2805307c28 100644 --- a/network/cumulus/cl_bridge.py +++ b/network/cumulus/cl_bridge.py @@ -15,6 +15,10 @@ # 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: cl_bridge diff --git a/network/cumulus/cl_img_install.py b/network/cumulus/cl_img_install.py index c5d7c45ffef..26fe8857b6f 100644 --- a/network/cumulus/cl_img_install.py +++ b/network/cumulus/cl_img_install.py @@ -15,6 +15,10 @@ # 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: cl_img_install diff --git a/network/cumulus/cl_interface.py b/network/cumulus/cl_interface.py index 475f9ce21dc..c8262b0710e 100644 --- a/network/cumulus/cl_interface.py +++ b/network/cumulus/cl_interface.py @@ -15,6 +15,10 @@ # 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: cl_interface diff --git a/network/cumulus/cl_interface_policy.py b/network/cumulus/cl_interface_policy.py index a8392c570e0..597f11a3fc3 100644 --- a/network/cumulus/cl_interface_policy.py +++ b/network/cumulus/cl_interface_policy.py @@ -15,6 +15,10 @@ # 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: cl_interface_policy diff --git a/network/cumulus/cl_license.py b/network/cumulus/cl_license.py index 37f20b7cb94..a0656e0abce 100644 --- a/network/cumulus/cl_license.py +++ b/network/cumulus/cl_license.py @@ -15,6 +15,10 @@ # 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: cl_license diff --git a/network/cumulus/cl_ports.py b/network/cumulus/cl_ports.py index 9ed48089cc4..85b3ed94d02 100644 --- a/network/cumulus/cl_ports.py +++ b/network/cumulus/cl_ports.py @@ -15,6 +15,10 @@ # 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: cl_ports diff --git a/network/dellos10/dellos10_command.py b/network/dellos10/dellos10_command.py index 5c8e5ea35f2..77e381c9117 100644 --- a/network/dellos10/dellos10_command.py +++ b/network/dellos10/dellos10_command.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dellos10_command diff --git a/network/dellos10/dellos10_config.py b/network/dellos10/dellos10_config.py index b164acec391..469fa4ca0b3 100644 --- a/network/dellos10/dellos10_config.py +++ b/network/dellos10/dellos10_config.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dellos10_config diff --git a/network/dellos10/dellos10_facts.py b/network/dellos10/dellos10_facts.py index 2d516f55739..e73785a0e15 100644 --- a/network/dellos10/dellos10_facts.py +++ b/network/dellos10/dellos10_facts.py @@ -19,6 +19,10 @@ # 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: dellos10_facts diff --git a/network/dellos6/dellos6_command.py b/network/dellos6/dellos6_command.py index a9d442f45bc..19efe7e083b 100644 --- a/network/dellos6/dellos6_command.py +++ b/network/dellos6/dellos6_command.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dellos6_command diff --git a/network/dellos6/dellos6_config.py b/network/dellos6/dellos6_config.py index 1f4c4417356..e2c4743319e 100644 --- a/network/dellos6/dellos6_config.py +++ b/network/dellos6/dellos6_config.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dellos6_config diff --git a/network/dellos6/dellos6_facts.py b/network/dellos6/dellos6_facts.py index 224ff2e3b82..bfb82fbc6f5 100644 --- a/network/dellos6/dellos6_facts.py +++ b/network/dellos6/dellos6_facts.py @@ -19,6 +19,10 @@ # 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: dellos6_facts diff --git a/network/dellos9/dellos9_command.py b/network/dellos9/dellos9_command.py index cc610855839..fcd70f0c35e 100755 --- a/network/dellos9/dellos9_command.py +++ b/network/dellos9/dellos9_command.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dellos9_command diff --git a/network/dellos9/dellos9_config.py b/network/dellos9/dellos9_config.py index 39ae652b305..bee90ee1c3f 100755 --- a/network/dellos9/dellos9_config.py +++ b/network/dellos9/dellos9_config.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: dellos9_config diff --git a/network/dellos9/dellos9_facts.py b/network/dellos9/dellos9_facts.py index 2e033acfa2e..fe752ac373f 100644 --- a/network/dellos9/dellos9_facts.py +++ b/network/dellos9/dellos9_facts.py @@ -19,6 +19,10 @@ # 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: dellos9_facts diff --git a/network/eos/_eos_template.py b/network/eos/_eos_template.py index 99a1a5f51fe..35be114cf7f 100644 --- a/network/eos/_eos_template.py +++ b/network/eos/_eos_template.py @@ -15,6 +15,10 @@ # 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: eos_template diff --git a/network/eos/eos_command.py b/network/eos/eos_command.py index 24cb6f3d794..110cfb60cea 100644 --- a/network/eos/eos_command.py +++ b/network/eos/eos_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: eos_command diff --git a/network/eos/eos_config.py b/network/eos/eos_config.py index 04d0c9f0fba..731b11efb90 100644 --- a/network/eos/eos_config.py +++ b/network/eos/eos_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: eos_config diff --git a/network/eos/eos_eapi.py b/network/eos/eos_eapi.py index 96ad4ee576b..a5cfafabd69 100644 --- a/network/eos/eos_eapi.py +++ b/network/eos/eos_eapi.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: eos_eapi diff --git a/network/eos/eos_facts.py b/network/eos/eos_facts.py index 7c53479c8ca..5e7cb2a7bee 100644 --- a/network/eos/eos_facts.py +++ b/network/eos/eos_facts.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: eos_facts diff --git a/network/ios/_ios_template.py b/network/ios/_ios_template.py index aa69c8f2e85..62186320caa 100644 --- a/network/ios/_ios_template.py +++ b/network/ios/_ios_template.py @@ -15,6 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + + DOCUMENTATION = """ --- module: ios_template diff --git a/network/ios/ios_command.py b/network/ios/ios_command.py index 4204a73a682..01878856c00 100644 --- a/network/ios/ios_command.py +++ b/network/ios/ios_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ios_command diff --git a/network/ios/ios_config.py b/network/ios/ios_config.py index 6c07d2cfdc0..dd77449e5bc 100644 --- a/network/ios/ios_config.py +++ b/network/ios/ios_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ios_config diff --git a/network/ios/ios_facts.py b/network/ios/ios_facts.py index c030e78a961..abe68ec69ce 100644 --- a/network/ios/ios_facts.py +++ b/network/ios/ios_facts.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ios_facts diff --git a/network/iosxr/_iosxr_template.py b/network/iosxr/_iosxr_template.py index 2f1ba8d02d8..315e693c3f2 100644 --- a/network/iosxr/_iosxr_template.py +++ b/network/iosxr/_iosxr_template.py @@ -15,6 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + + DOCUMENTATION = """ --- module: iosxr_template diff --git a/network/iosxr/iosxr_command.py b/network/iosxr/iosxr_command.py index b66d1ec3f05..e266700bbf8 100644 --- a/network/iosxr/iosxr_command.py +++ b/network/iosxr/iosxr_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: iosxr_command diff --git a/network/iosxr/iosxr_config.py b/network/iosxr/iosxr_config.py index 15978ff108b..d686f83111a 100644 --- a/network/iosxr/iosxr_config.py +++ b/network/iosxr/iosxr_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: iosxr_config diff --git a/network/iosxr/iosxr_facts.py b/network/iosxr/iosxr_facts.py index d8d0e5d93b7..9f93d06125c 100644 --- a/network/iosxr/iosxr_facts.py +++ b/network/iosxr/iosxr_facts.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: iosxr_facts diff --git a/network/junos/_junos_template.py b/network/junos/_junos_template.py index 868c52001fd..bd0ecf371ea 100644 --- a/network/junos/_junos_template.py +++ b/network/junos/_junos_template.py @@ -16,6 +16,11 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + + DOCUMENTATION = """ --- module: junos_template diff --git a/network/junos/junos_command.py b/network/junos/junos_command.py index 49fa23da853..c54061834a0 100644 --- a/network/junos/junos_command.py +++ b/network/junos/junos_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: junos_command diff --git a/network/junos/junos_config.py b/network/junos/junos_config.py index af0134764f8..a1212e974c3 100644 --- a/network/junos/junos_config.py +++ b/network/junos/junos_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: junos_config diff --git a/network/junos/junos_facts.py b/network/junos/junos_facts.py index d321b9f1780..383eb90bcf2 100644 --- a/network/junos/junos_facts.py +++ b/network/junos/junos_facts.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: junos_facts diff --git a/network/junos/junos_netconf.py b/network/junos/junos_netconf.py index 99b918c0517..6f47daa8db0 100644 --- a/network/junos/junos_netconf.py +++ b/network/junos/junos_netconf.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: junos_netconf diff --git a/network/junos/junos_package.py b/network/junos/junos_package.py index e893ee9df56..c457be8228c 100644 --- a/network/junos/junos_package.py +++ b/network/junos/junos_package.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: junos_package diff --git a/network/netvisor/pn_cluster.py b/network/netvisor/pn_cluster.py index 76cea3c9d04..de02198e2d9 100644 --- a/network/netvisor/pn_cluster.py +++ b/network/netvisor/pn_cluster.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_cluster diff --git a/network/netvisor/pn_ospf.py b/network/netvisor/pn_ospf.py index 16867aeaa1d..7c4cbd1400f 100644 --- a/network/netvisor/pn_ospf.py +++ b/network/netvisor/pn_ospf.py @@ -20,6 +20,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_ospf diff --git a/network/netvisor/pn_ospfarea.py b/network/netvisor/pn_ospfarea.py index e9fd404ddd9..d34b145d28a 100644 --- a/network/netvisor/pn_ospfarea.py +++ b/network/netvisor/pn_ospfarea.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_ospfarea diff --git a/network/netvisor/pn_show.py b/network/netvisor/pn_show.py index fce0c967de7..ff22667a434 100644 --- a/network/netvisor/pn_show.py +++ b/network/netvisor/pn_show.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_show diff --git a/network/netvisor/pn_trunk.py b/network/netvisor/pn_trunk.py index 68fcb07df5d..da3c568f203 100644 --- a/network/netvisor/pn_trunk.py +++ b/network/netvisor/pn_trunk.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_trunk diff --git a/network/netvisor/pn_vlag.py b/network/netvisor/pn_vlag.py index 0387d80b6d1..66b9ce05960 100644 --- a/network/netvisor/pn_vlag.py +++ b/network/netvisor/pn_vlag.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_vlag diff --git a/network/netvisor/pn_vlan.py b/network/netvisor/pn_vlan.py index cf94c3bc753..c79e4ba5e3e 100644 --- a/network/netvisor/pn_vlan.py +++ b/network/netvisor/pn_vlan.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_vlan diff --git a/network/netvisor/pn_vrouter.py b/network/netvisor/pn_vrouter.py index 6036daf74b8..bb0cadc76d2 100644 --- a/network/netvisor/pn_vrouter.py +++ b/network/netvisor/pn_vrouter.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_vrouter diff --git a/network/netvisor/pn_vrouterbgp.py b/network/netvisor/pn_vrouterbgp.py index 1a535a221f5..e53403da070 100644 --- a/network/netvisor/pn_vrouterbgp.py +++ b/network/netvisor/pn_vrouterbgp.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_vrouterbgp diff --git a/network/netvisor/pn_vrouterif.py b/network/netvisor/pn_vrouterif.py index 8e1a5255a0a..4c5df6b7fd4 100644 --- a/network/netvisor/pn_vrouterif.py +++ b/network/netvisor/pn_vrouterif.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_vrouterif diff --git a/network/netvisor/pn_vrouterlbif.py b/network/netvisor/pn_vrouterlbif.py index 1b59311d22a..e6fc928a205 100644 --- a/network/netvisor/pn_vrouterlbif.py +++ b/network/netvisor/pn_vrouterlbif.py @@ -21,6 +21,10 @@ import shlex +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: pn_vrouterlbif diff --git a/network/nxos/_nxos_template.py b/network/nxos/_nxos_template.py index d7b50059e10..a5b976aa4ce 100644 --- a/network/nxos/_nxos_template.py +++ b/network/nxos/_nxos_template.py @@ -15,6 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + + DOCUMENTATION = """ --- module: nxos_template diff --git a/network/nxos/nxos_aaa_server.py b/network/nxos/nxos_aaa_server.py index 33988657e6f..6b4f52ae4e3 100644 --- a/network/nxos/nxos_aaa_server.py +++ b/network/nxos/nxos_aaa_server.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- diff --git a/network/nxos/nxos_aaa_server_host.py b/network/nxos/nxos_aaa_server_host.py index 9cfcd004bbc..aef2af144f2 100644 --- a/network/nxos/nxos_aaa_server_host.py +++ b/network/nxos/nxos_aaa_server_host.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_aaa_server_host diff --git a/network/nxos/nxos_acl.py b/network/nxos/nxos_acl.py index 0b3ea06dc3d..63762f97615 100644 --- a/network/nxos/nxos_acl.py +++ b/network/nxos/nxos_acl.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_acl diff --git a/network/nxos/nxos_acl_interface.py b/network/nxos/nxos_acl_interface.py index 83ab0d41568..e9dbcb3fe07 100644 --- a/network/nxos/nxos_acl_interface.py +++ b/network/nxos/nxos_acl_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_acl_interface diff --git a/network/nxos/nxos_bgp.py b/network/nxos/nxos_bgp.py index 18ca8402577..a6306fe74cc 100644 --- a/network/nxos/nxos_bgp.py +++ b/network/nxos/nxos_bgp.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_bgp diff --git a/network/nxos/nxos_bgp_af.py b/network/nxos/nxos_bgp_af.py index 1f02b113cbf..3b804d51a3c 100644 --- a/network/nxos/nxos_bgp_af.py +++ b/network/nxos/nxos_bgp_af.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_bgp_af diff --git a/network/nxos/nxos_bgp_neighbor.py b/network/nxos/nxos_bgp_neighbor.py index a9080ae2f28..f0cc6145819 100644 --- a/network/nxos/nxos_bgp_neighbor.py +++ b/network/nxos/nxos_bgp_neighbor.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_bgp_neighbor diff --git a/network/nxos/nxos_bgp_neighbor_af.py b/network/nxos/nxos_bgp_neighbor_af.py index 1062ed214ce..e7d9ea6481e 100644 --- a/network/nxos/nxos_bgp_neighbor_af.py +++ b/network/nxos/nxos_bgp_neighbor_af.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_bgp_neighbor_af diff --git a/network/nxos/nxos_command.py b/network/nxos/nxos_command.py index c6e33c6e45c..b90034a09e6 100644 --- a/network/nxos/nxos_command.py +++ b/network/nxos/nxos_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: nxos_command diff --git a/network/nxos/nxos_config.py b/network/nxos/nxos_config.py index 19b15d06ef9..2355ec37d89 100644 --- a/network/nxos/nxos_config.py +++ b/network/nxos/nxos_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: nxos_config diff --git a/network/nxos/nxos_evpn_global.py b/network/nxos/nxos_evpn_global.py index 650a19370e1..375269e0709 100644 --- a/network/nxos/nxos_evpn_global.py +++ b/network/nxos/nxos_evpn_global.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_evpn_global diff --git a/network/nxos/nxos_evpn_vni.py b/network/nxos/nxos_evpn_vni.py index 48cddf9456a..5d20addd63a 100644 --- a/network/nxos/nxos_evpn_vni.py +++ b/network/nxos/nxos_evpn_vni.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_evpn_vni diff --git a/network/nxos/nxos_facts.py b/network/nxos/nxos_facts.py index 25dd9f4095d..f5ec1238ee3 100644 --- a/network/nxos/nxos_facts.py +++ b/network/nxos/nxos_facts.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: nxos_facts diff --git a/network/nxos/nxos_feature.py b/network/nxos/nxos_feature.py index a00c71a4852..2a532dd5994 100644 --- a/network/nxos/nxos_feature.py +++ b/network/nxos/nxos_feature.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_feature diff --git a/network/nxos/nxos_file_copy.py b/network/nxos/nxos_file_copy.py index 2c01cad1383..0885c4575e2 100644 --- a/network/nxos/nxos_file_copy.py +++ b/network/nxos/nxos_file_copy.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_file_copy diff --git a/network/nxos/nxos_gir.py b/network/nxos/nxos_gir.py index 1359547f161..f72f7d79a07 100644 --- a/network/nxos/nxos_gir.py +++ b/network/nxos/nxos_gir.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_gir diff --git a/network/nxos/nxos_gir_profile_management.py b/network/nxos/nxos_gir_profile_management.py index 0b9e795b566..99c29d6253d 100644 --- a/network/nxos/nxos_gir_profile_management.py +++ b/network/nxos/nxos_gir_profile_management.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_gir_profile diff --git a/network/nxos/nxos_hsrp.py b/network/nxos/nxos_hsrp.py index ec6e5893403..9e9e7e3542a 100644 --- a/network/nxos/nxos_hsrp.py +++ b/network/nxos/nxos_hsrp.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_hsrp diff --git a/network/nxos/nxos_igmp.py b/network/nxos/nxos_igmp.py index d1ac322c947..3cd5a0dbd20 100644 --- a/network/nxos/nxos_igmp.py +++ b/network/nxos/nxos_igmp.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_igmp diff --git a/network/nxos/nxos_igmp_interface.py b/network/nxos/nxos_igmp_interface.py index 902d5808ccf..d5e3226d346 100644 --- a/network/nxos/nxos_igmp_interface.py +++ b/network/nxos/nxos_igmp_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_igmp_interface diff --git a/network/nxos/nxos_igmp_snooping.py b/network/nxos/nxos_igmp_snooping.py index c1ee7c9b13d..7044adecd16 100644 --- a/network/nxos/nxos_igmp_snooping.py +++ b/network/nxos/nxos_igmp_snooping.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_igmp_snooping diff --git a/network/nxos/nxos_install_os.py b/network/nxos/nxos_install_os.py index be11cc2e8c6..aa0c0505437 100644 --- a/network/nxos/nxos_install_os.py +++ b/network/nxos/nxos_install_os.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_install_os diff --git a/network/nxos/nxos_interface.py b/network/nxos/nxos_interface.py index ac1c773d0ed..b65fb9d3109 100644 --- a/network/nxos/nxos_interface.py +++ b/network/nxos/nxos_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_interface diff --git a/network/nxos/nxos_interface_ospf.py b/network/nxos/nxos_interface_ospf.py index 2e5a291d594..9f63bf57a04 100644 --- a/network/nxos/nxos_interface_ospf.py +++ b/network/nxos/nxos_interface_ospf.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_interface_ospf diff --git a/network/nxos/nxos_ip_interface.py b/network/nxos/nxos_ip_interface.py index 416ebadd482..b860ea20b6f 100644 --- a/network/nxos/nxos_ip_interface.py +++ b/network/nxos/nxos_ip_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_ip_interface diff --git a/network/nxos/nxos_mtu.py b/network/nxos/nxos_mtu.py index 58942a277b2..48a92c2f941 100644 --- a/network/nxos/nxos_mtu.py +++ b/network/nxos/nxos_mtu.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_mtu diff --git a/network/nxos/nxos_ntp.py b/network/nxos/nxos_ntp.py index f99f1876a6e..4c6e406e6b9 100644 --- a/network/nxos/nxos_ntp.py +++ b/network/nxos/nxos_ntp.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_ntp diff --git a/network/nxos/nxos_ntp_auth.py b/network/nxos/nxos_ntp_auth.py index e6652e1bfb7..25071d6b893 100644 --- a/network/nxos/nxos_ntp_auth.py +++ b/network/nxos/nxos_ntp_auth.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- diff --git a/network/nxos/nxos_ntp_options.py b/network/nxos/nxos_ntp_options.py index 26a28523d61..010e67c8866 100644 --- a/network/nxos/nxos_ntp_options.py +++ b/network/nxos/nxos_ntp_options.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- diff --git a/network/nxos/nxos_nxapi.py b/network/nxos/nxos_nxapi.py index f8144aba451..5317869f5aa 100644 --- a/network/nxos/nxos_nxapi.py +++ b/network/nxos/nxos_nxapi.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: nxos_nxapi diff --git a/network/nxos/nxos_ospf.py b/network/nxos/nxos_ospf.py index 5942785476a..024b5f0ba24 100644 --- a/network/nxos/nxos_ospf.py +++ b/network/nxos/nxos_ospf.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_ospf diff --git a/network/nxos/nxos_ospf_vrf.py b/network/nxos/nxos_ospf_vrf.py index 8b4d330c995..35e0d398f04 100644 --- a/network/nxos/nxos_ospf_vrf.py +++ b/network/nxos/nxos_ospf_vrf.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_ospf_vrf diff --git a/network/nxos/nxos_overlay_global.py b/network/nxos/nxos_overlay_global.py index 710e02e2b12..5b7d89c4b29 100644 --- a/network/nxos/nxos_overlay_global.py +++ b/network/nxos/nxos_overlay_global.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_overlay_global diff --git a/network/nxos/nxos_pim.py b/network/nxos/nxos_pim.py index da5b49a818f..659686ceaa2 100644 --- a/network/nxos/nxos_pim.py +++ b/network/nxos/nxos_pim.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_pim diff --git a/network/nxos/nxos_pim_interface.py b/network/nxos/nxos_pim_interface.py index 6a627565526..119785d47ed 100644 --- a/network/nxos/nxos_pim_interface.py +++ b/network/nxos/nxos_pim_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_pim_interface diff --git a/network/nxos/nxos_pim_rp_address.py b/network/nxos/nxos_pim_rp_address.py index bdb3c46fdb7..c53747e02de 100644 --- a/network/nxos/nxos_pim_rp_address.py +++ b/network/nxos/nxos_pim_rp_address.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_pim_rp_address diff --git a/network/nxos/nxos_ping.py b/network/nxos/nxos_ping.py index 5764de8f127..a698b98ba8d 100644 --- a/network/nxos/nxos_ping.py +++ b/network/nxos/nxos_ping.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_ping diff --git a/network/nxos/nxos_portchannel.py b/network/nxos/nxos_portchannel.py index 48985a13178..acb0a61b4c4 100644 --- a/network/nxos/nxos_portchannel.py +++ b/network/nxos/nxos_portchannel.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_portchannel diff --git a/network/nxos/nxos_reboot.py b/network/nxos/nxos_reboot.py index aefab439e91..c479d10a806 100644 --- a/network/nxos/nxos_reboot.py +++ b/network/nxos/nxos_reboot.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_reboot diff --git a/network/nxos/nxos_rollback.py b/network/nxos/nxos_rollback.py index a6744dc1a77..736fa25ccaa 100644 --- a/network/nxos/nxos_rollback.py +++ b/network/nxos/nxos_rollback.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_rollback diff --git a/network/nxos/nxos_smu.py b/network/nxos/nxos_smu.py index 776430b86d8..1404725ac38 100644 --- a/network/nxos/nxos_smu.py +++ b/network/nxos/nxos_smu.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_smu diff --git a/network/nxos/nxos_snapshot.py b/network/nxos/nxos_snapshot.py index f7923a10fa1..e6f367d7bfa 100644 --- a/network/nxos/nxos_snapshot.py +++ b/network/nxos/nxos_snapshot.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snapshot diff --git a/network/nxos/nxos_snmp_community.py b/network/nxos/nxos_snmp_community.py index 7ffe7b46e58..93ed3f7be4b 100644 --- a/network/nxos/nxos_snmp_community.py +++ b/network/nxos/nxos_snmp_community.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snmp_community diff --git a/network/nxos/nxos_snmp_contact.py b/network/nxos/nxos_snmp_contact.py index e2d98733687..b5b97da92c1 100644 --- a/network/nxos/nxos_snmp_contact.py +++ b/network/nxos/nxos_snmp_contact.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snmp_contact diff --git a/network/nxos/nxos_snmp_host.py b/network/nxos/nxos_snmp_host.py index 8717bb7e3d8..366da22a918 100644 --- a/network/nxos/nxos_snmp_host.py +++ b/network/nxos/nxos_snmp_host.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snmp_host diff --git a/network/nxos/nxos_snmp_location.py b/network/nxos/nxos_snmp_location.py index 25acf5141e3..e3b90973e72 100644 --- a/network/nxos/nxos_snmp_location.py +++ b/network/nxos/nxos_snmp_location.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snmp_location diff --git a/network/nxos/nxos_snmp_traps.py b/network/nxos/nxos_snmp_traps.py index a615c492c0f..632e8e8d1d7 100644 --- a/network/nxos/nxos_snmp_traps.py +++ b/network/nxos/nxos_snmp_traps.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snmp_trap diff --git a/network/nxos/nxos_snmp_user.py b/network/nxos/nxos_snmp_user.py index 56d515a8ffd..a06a0151019 100644 --- a/network/nxos/nxos_snmp_user.py +++ b/network/nxos/nxos_snmp_user.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_snmp_user diff --git a/network/nxos/nxos_static_route.py b/network/nxos/nxos_static_route.py index a0e2a78bb11..2ff042d0307 100644 --- a/network/nxos/nxos_static_route.py +++ b/network/nxos/nxos_static_route.py @@ -15,6 +15,10 @@ # 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_static_route diff --git a/network/nxos/nxos_switchport.py b/network/nxos/nxos_switchport.py index 006a2dcd279..f0e06163ce1 100644 --- a/network/nxos/nxos_switchport.py +++ b/network/nxos/nxos_switchport.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_switchport diff --git a/network/nxos/nxos_udld.py b/network/nxos/nxos_udld.py index 7f8cedeca91..8318f4025be 100644 --- a/network/nxos/nxos_udld.py +++ b/network/nxos/nxos_udld.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- diff --git a/network/nxos/nxos_udld_interface.py b/network/nxos/nxos_udld_interface.py index 66473495401..f73670c2b1a 100644 --- a/network/nxos/nxos_udld_interface.py +++ b/network/nxos/nxos_udld_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_udld_interface diff --git a/network/nxos/nxos_vlan.py b/network/nxos/nxos_vlan.py index 184d8b494e1..67719262bd0 100644 --- a/network/nxos/nxos_vlan.py +++ b/network/nxos/nxos_vlan.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vlan diff --git a/network/nxos/nxos_vpc.py b/network/nxos/nxos_vpc.py index 3fae9cee00d..42af1cb6511 100644 --- a/network/nxos/nxos_vpc.py +++ b/network/nxos/nxos_vpc.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vpc diff --git a/network/nxos/nxos_vpc_interface.py b/network/nxos/nxos_vpc_interface.py index 0f930cacf8f..6f122f6f2d5 100644 --- a/network/nxos/nxos_vpc_interface.py +++ b/network/nxos/nxos_vpc_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vpc_interface diff --git a/network/nxos/nxos_vrf.py b/network/nxos/nxos_vrf.py index 8dde538f993..eb60306e08a 100644 --- a/network/nxos/nxos_vrf.py +++ b/network/nxos/nxos_vrf.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vrf diff --git a/network/nxos/nxos_vrf_af.py b/network/nxos/nxos_vrf_af.py index b8706d28c29..70ca67109e7 100644 --- a/network/nxos/nxos_vrf_af.py +++ b/network/nxos/nxos_vrf_af.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vrf_af diff --git a/network/nxos/nxos_vrf_interface.py b/network/nxos/nxos_vrf_interface.py index cd3c57046af..a3420307a6f 100644 --- a/network/nxos/nxos_vrf_interface.py +++ b/network/nxos/nxos_vrf_interface.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vrf_interface diff --git a/network/nxos/nxos_vrrp.py b/network/nxos/nxos_vrrp.py index a034b0e256c..58c04a8367a 100644 --- a/network/nxos/nxos_vrrp.py +++ b/network/nxos/nxos_vrrp.py @@ -17,6 +17,10 @@ # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vrrp diff --git a/network/nxos/nxos_vtp_domain.py b/network/nxos/nxos_vtp_domain.py index 93f62318e22..f96db115a6b 100644 --- a/network/nxos/nxos_vtp_domain.py +++ b/network/nxos/nxos_vtp_domain.py @@ -15,6 +15,10 @@ # 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_vtp_domain diff --git a/network/nxos/nxos_vtp_password.py b/network/nxos/nxos_vtp_password.py index aa3cb13d6f6..12c142c2fc7 100644 --- a/network/nxos/nxos_vtp_password.py +++ b/network/nxos/nxos_vtp_password.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- diff --git a/network/nxos/nxos_vtp_version.py b/network/nxos/nxos_vtp_version.py index 5ce455d81c9..bd10745ba86 100644 --- a/network/nxos/nxos_vtp_version.py +++ b/network/nxos/nxos_vtp_version.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- diff --git a/network/nxos/nxos_vxlan_vtep.py b/network/nxos/nxos_vxlan_vtep.py index 620d3e240e1..6d29597cd28 100644 --- a/network/nxos/nxos_vxlan_vtep.py +++ b/network/nxos/nxos_vxlan_vtep.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vxlan_vtep diff --git a/network/nxos/nxos_vxlan_vtep_vni.py b/network/nxos/nxos_vxlan_vtep_vni.py index a50c7600a07..cf354d59c4f 100644 --- a/network/nxos/nxos_vxlan_vtep_vni.py +++ b/network/nxos/nxos_vxlan_vtep_vni.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: nxos_vxlan_vtep_vni diff --git a/network/openswitch/_ops_template.py b/network/openswitch/_ops_template.py index 73af4c0a085..d3cb0e00d10 100644 --- a/network/openswitch/_ops_template.py +++ b/network/openswitch/_ops_template.py @@ -15,6 +15,11 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + + DOCUMENTATION = """ --- module: ops_template diff --git a/network/openswitch/ops_command.py b/network/openswitch/ops_command.py index 9a3c0b2f2d8..0f1ffd04a1d 100644 --- a/network/openswitch/ops_command.py +++ b/network/openswitch/ops_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ops_command diff --git a/network/openswitch/ops_config.py b/network/openswitch/ops_config.py index 8c65144b079..79c23aafcb4 100644 --- a/network/openswitch/ops_config.py +++ b/network/openswitch/ops_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = """ --- module: ops_config diff --git a/network/openswitch/ops_facts.py b/network/openswitch/ops_facts.py index 5be10b38d40..cbf31bc87e7 100644 --- a/network/openswitch/ops_facts.py +++ b/network/openswitch/ops_facts.py @@ -15,6 +15,10 @@ # 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: ops_facts diff --git a/network/sros/sros_command.py b/network/sros/sros_command.py index e8b6a654fc1..3c7dd21e203 100644 --- a/network/sros/sros_command.py +++ b/network/sros/sros_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: sros_command diff --git a/network/sros/sros_config.py b/network/sros/sros_config.py index 27c62fd5362..2efca84dc12 100644 --- a/network/sros/sros_config.py +++ b/network/sros/sros_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: sros_config diff --git a/network/sros/sros_rollback.py b/network/sros/sros_rollback.py index f952b092124..85d7bdc5067 100644 --- a/network/sros/sros_rollback.py +++ b/network/sros/sros_rollback.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: sros_rollback diff --git a/network/vyos/vyos_command.py b/network/vyos/vyos_command.py index cced7b2cc5d..14180e305d7 100644 --- a/network/vyos/vyos_command.py +++ b/network/vyos/vyos_command.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vyos_command diff --git a/network/vyos/vyos_config.py b/network/vyos/vyos_config.py index 7ee3a272bdf..8cf2c3d6047 100644 --- a/network/vyos/vyos_config.py +++ b/network/vyos/vyos_config.py @@ -16,6 +16,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = """ --- module: vyos_config diff --git a/network/vyos/vyos_facts.py b/network/vyos/vyos_facts.py index 8e068484ff7..ff081f8abc5 100644 --- a/network/vyos/vyos_facts.py +++ b/network/vyos/vyos_facts.py @@ -15,6 +15,10 @@ # 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: vyos_facts diff --git a/packaging/language/easy_install.py b/packaging/language/easy_install.py index 31d1df9abc2..40b1026a76a 100644 --- a/packaging/language/easy_install.py +++ b/packaging/language/easy_install.py @@ -22,6 +22,10 @@ import tempfile import os.path +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: easy_install diff --git a/packaging/language/gem.py b/packaging/language/gem.py index 9407e9c5d4d..41a0961b2af 100644 --- a/packaging/language/gem.py +++ b/packaging/language/gem.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: gem diff --git a/packaging/language/pip.py b/packaging/language/pip.py index aa78ead6e59..ddf6e4af013 100755 --- a/packaging/language/pip.py +++ b/packaging/language/pip.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pip diff --git a/packaging/os/apt.py b/packaging/os/apt.py index 5d20dbee86b..5637680cd9a 100644 --- a/packaging/os/apt.py +++ b/packaging/os/apt.py @@ -19,6 +19,10 @@ # along with this software. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apt diff --git a/packaging/os/apt_key.py b/packaging/os/apt_key.py index 753fb830e11..a5fd723f6c3 100644 --- a/packaging/os/apt_key.py +++ b/packaging/os/apt_key.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apt_key diff --git a/packaging/os/apt_repository.py b/packaging/os/apt_repository.py index 0b86b6bddba..dc60a2a722f 100644 --- a/packaging/os/apt_repository.py +++ b/packaging/os/apt_repository.py @@ -21,6 +21,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apt_repository diff --git a/packaging/os/apt_rpm.py b/packaging/os/apt_rpm.py index 452d2ff8ca6..47f6b194cf1 100755 --- a/packaging/os/apt_rpm.py +++ b/packaging/os/apt_rpm.py @@ -19,6 +19,10 @@ # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apt_rpm diff --git a/packaging/os/package.py b/packaging/os/package.py index d40ed3d4eee..85712b6d903 100644 --- a/packaging/os/package.py +++ b/packaging/os/package.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: package diff --git a/packaging/os/redhat_subscription.py b/packaging/os/redhat_subscription.py index b3b0aabb356..0b56c6eafbf 100644 --- a/packaging/os/redhat_subscription.py +++ b/packaging/os/redhat_subscription.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: redhat_subscription diff --git a/packaging/os/rhn_channel.py b/packaging/os/rhn_channel.py index e77972b30ce..c2e87f1bd91 100644 --- a/packaging/os/rhn_channel.py +++ b/packaging/os/rhn_channel.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rhn_channel diff --git a/packaging/os/rhn_register.py b/packaging/os/rhn_register.py index fda8416e153..c228f0b1b7a 100644 --- a/packaging/os/rhn_register.py +++ b/packaging/os/rhn_register.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rhn_register diff --git a/packaging/os/rpm_key.py b/packaging/os/rpm_key.py index 42d75670a53..9cb058c56aa 100644 --- a/packaging/os/rpm_key.py +++ b/packaging/os/rpm_key.py @@ -19,6 +19,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: rpm_key diff --git a/packaging/os/yum.py b/packaging/os/yum.py index 0714303acbf..18e71713902 100644 --- a/packaging/os/yum.py +++ b/packaging/os/yum.py @@ -36,6 +36,10 @@ except: transaction_helpers = False +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: yum diff --git a/source_control/git.py b/source_control/git.py index d8307023d23..6c79e8a3668 100644 --- a/source_control/git.py +++ b/source_control/git.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: git diff --git a/source_control/hg.py b/source_control/hg.py index 5874857ad19..89845c197c9 100644 --- a/source_control/hg.py +++ b/source_control/hg.py @@ -23,6 +23,10 @@ # 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: hg diff --git a/source_control/subversion.py b/source_control/subversion.py index dc9cedbbedf..09477700b9f 100644 --- a/source_control/subversion.py +++ b/source_control/subversion.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: subversion diff --git a/system/authorized_key.py b/system/authorized_key.py index 7ac85eae0de..c2023e30124 100644 --- a/system/authorized_key.py +++ b/system/authorized_key.py @@ -21,6 +21,10 @@ along with Ansible. If not, see . """ +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: authorized_key diff --git a/system/cron.py b/system/cron.py index 3157b48b69b..6e87147f39e 100644 --- a/system/cron.py +++ b/system/cron.py @@ -31,6 +31,10 @@ # This module is based on python-crontab by Martin Owens. # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = """ --- module: cron diff --git a/system/group.py b/system/group.py index a642a3ee3e2..132d71ad2d3 100644 --- a/system/group.py +++ b/system/group.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: group diff --git a/system/hostname.py b/system/hostname.py index 92fd6758c58..c6432428fca 100644 --- a/system/hostname.py +++ b/system/hostname.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'committer', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: hostname diff --git a/system/mount.py b/system/mount.py index 9fe7fe32cfe..b8fad7747b3 100644 --- a/system/mount.py +++ b/system/mount.py @@ -30,6 +30,10 @@ import os +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: mount diff --git a/system/ping.py b/system/ping.py index a701b009132..bee23a95c4a 100644 --- a/system/ping.py +++ b/system/ping.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: ping diff --git a/system/seboolean.py b/system/seboolean.py index 6d3b312d588..9246d912a07 100644 --- a/system/seboolean.py +++ b/system/seboolean.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: seboolean diff --git a/system/selinux.py b/system/selinux.py index 2afd47566aa..634ef4efcad 100644 --- a/system/selinux.py +++ b/system/selinux.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: selinux diff --git a/system/service.py b/system/service.py index 4b22f2654f7..b1cb289b49f 100644 --- a/system/service.py +++ b/system/service.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: service diff --git a/system/setup.py b/system/setup.py index ac06b478b4b..81bbf43ddba 100644 --- a/system/setup.py +++ b/system/setup.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: setup diff --git a/system/sysctl.py b/system/sysctl.py index 9a6787e2a7e..43312b0922e 100644 --- a/system/sysctl.py +++ b/system/sysctl.py @@ -20,6 +20,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: sysctl diff --git a/system/systemd.py b/system/systemd.py index dcc493b812b..0b6c05351bc 100644 --- a/system/systemd.py +++ b/system/systemd.py @@ -17,6 +17,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' module: systemd author: diff --git a/system/user.py b/system/user.py index dd079b64f2d..ed5503583ae 100644 --- a/system/user.py +++ b/system/user.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: user diff --git a/utilities/helper/_accelerate.py b/utilities/helper/_accelerate.py index c00646fcd4f..5bd38931f22 100644 --- a/utilities/helper/_accelerate.py +++ b/utilities/helper/_accelerate.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: accelerate diff --git a/utilities/helper/_fireball.py b/utilities/helper/_fireball.py index ba0770d7839..d3bc837fa4f 100644 --- a/utilities/helper/_fireball.py +++ b/utilities/helper/_fireball.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: fireball diff --git a/utilities/helper/meta.py b/utilities/helper/meta.py index 76bcf73996e..a89bfc5d394 100644 --- a/utilities/helper/meta.py +++ b/utilities/helper/meta.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' module: meta short_description: Execute Ansible 'actions' diff --git a/utilities/logic/assert.py b/utilities/logic/assert.py index f37e6bf5da7..875fc6e8565 100644 --- a/utilities/logic/assert.py +++ b/utilities/logic/assert.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: assert diff --git a/utilities/logic/async_status.py b/utilities/logic/async_status.py index c0e2526d549..7093cd32db7 100644 --- a/utilities/logic/async_status.py +++ b/utilities/logic/async_status.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: async_status diff --git a/utilities/logic/debug.py b/utilities/logic/debug.py index f28dc538337..8e8f16ca554 100644 --- a/utilities/logic/debug.py +++ b/utilities/logic/debug.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: debug diff --git a/utilities/logic/fail.py b/utilities/logic/fail.py index 0957b597184..544758ad100 100644 --- a/utilities/logic/fail.py +++ b/utilities/logic/fail.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: fail diff --git a/utilities/logic/include.py b/utilities/logic/include.py index e5c2face7cf..3de1e198a2a 100644 --- a/utilities/logic/include.py +++ b/utilities/logic/include.py @@ -8,6 +8,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: diff --git a/utilities/logic/include_role.py b/utilities/logic/include_role.py index 40208764a6f..843a873b8ae 100644 --- a/utilities/logic/include_role.py +++ b/utilities/logic/include_role.py @@ -8,6 +8,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: diff --git a/utilities/logic/include_vars.py b/utilities/logic/include_vars.py index 571ddf58e9f..8e7f4aa1bda 100644 --- a/utilities/logic/include_vars.py +++ b/utilities/logic/include_vars.py @@ -8,6 +8,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: "Allen Sanabria (@linuxdynasty)" diff --git a/utilities/logic/pause.py b/utilities/logic/pause.py index 5290a632230..0fed099b700 100644 --- a/utilities/logic/pause.py +++ b/utilities/logic/pause.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: pause diff --git a/utilities/logic/set_fact.py b/utilities/logic/set_fact.py index 881f69feffe..c106ef74e63 100644 --- a/utilities/logic/set_fact.py +++ b/utilities/logic/set_fact.py @@ -18,6 +18,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- author: "Dag Wieers (@dagwieers)" diff --git a/utilities/logic/wait_for.py b/utilities/logic/wait_for.py index 46be2ff04c2..55f88fc8c84 100644 --- a/utilities/logic/wait_for.py +++ b/utilities/logic/wait_for.py @@ -37,6 +37,10 @@ except ImportError: pass +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: wait_for diff --git a/web_infrastructure/apache2_module.py b/web_infrastructure/apache2_module.py index 2e5060d3a54..34d736d4d7c 100644 --- a/web_infrastructure/apache2_module.py +++ b/web_infrastructure/apache2_module.py @@ -16,6 +16,10 @@ # You should have received a copy of the GNU General Public License # along with this software. If not, see . +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: apache2_module diff --git a/web_infrastructure/django_manage.py b/web_infrastructure/django_manage.py index b21c2834a81..efd32a33a18 100644 --- a/web_infrastructure/django_manage.py +++ b/web_infrastructure/django_manage.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: django_manage diff --git a/web_infrastructure/htpasswd.py b/web_infrastructure/htpasswd.py index 84a2cbdd29c..0c5d8bea9d6 100644 --- a/web_infrastructure/htpasswd.py +++ b/web_infrastructure/htpasswd.py @@ -18,6 +18,10 @@ # 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: htpasswd version_added: "1.3" diff --git a/web_infrastructure/supervisorctl.py b/web_infrastructure/supervisorctl.py index 35d40928ca4..84c8ece749f 100644 --- a/web_infrastructure/supervisorctl.py +++ b/web_infrastructure/supervisorctl.py @@ -21,6 +21,10 @@ import os from ansible.module_utils.basic import AnsibleModule, is_executable +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: supervisorctl diff --git a/windows/win_command.py b/windows/win_command.py index 2d76a0b8534..2a131c4bc45 100644 --- a/windows/win_command.py +++ b/windows/win_command.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_command diff --git a/windows/win_copy.py b/windows/win_copy.py index 89bab5e913b..1b81f0cac89 100755 --- a/windows/win_copy.py +++ b/windows/win_copy.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_copy diff --git a/windows/win_feature.py b/windows/win_feature.py index c16f3891238..2fb6fe3718b 100644 --- a/windows/win_feature.py +++ b/windows/win_feature.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_feature diff --git a/windows/win_file.py b/windows/win_file.py index 989c128e3b3..82c5510c3cf 100644 --- a/windows/win_file.py +++ b/windows/win_file.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_file diff --git a/windows/win_get_url.py b/windows/win_get_url.py index 4091a596920..6c5d5c67c0d 100644 --- a/windows/win_get_url.py +++ b/windows/win_get_url.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_get_url diff --git a/windows/win_group.py b/windows/win_group.py index 409065cb96a..035c05eff5c 100644 --- a/windows/win_group.py +++ b/windows/win_group.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_group diff --git a/windows/win_lineinfile.py b/windows/win_lineinfile.py index 35c478d21bc..df250d6d414 100644 --- a/windows/win_lineinfile.py +++ b/windows/win_lineinfile.py @@ -16,6 +16,10 @@ # 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: win_lineinfile diff --git a/windows/win_msi.py b/windows/win_msi.py index b167aadc870..cfc7e089822 100644 --- a/windows/win_msi.py +++ b/windows/win_msi.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['deprecated'], + 'supported_by': 'community', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_msi diff --git a/windows/win_ping.py b/windows/win_ping.py index ecb5149f8c3..6f650e977fd 100644 --- a/windows/win_ping.py +++ b/windows/win_ping.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_ping diff --git a/windows/win_reboot.py b/windows/win_reboot.py index eeb39a9279d..c8f179e7dd4 100644 --- a/windows/win_reboot.py +++ b/windows/win_reboot.py @@ -19,6 +19,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION=''' --- module: win_reboot diff --git a/windows/win_service.py b/windows/win_service.py index 0fb9ec48652..e4f009a361b 100644 --- a/windows/win_service.py +++ b/windows/win_service.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_service diff --git a/windows/win_shell.py b/windows/win_shell.py index 7c4dc68df63..6441f2018fa 100644 --- a/windows/win_shell.py +++ b/windows/win_shell.py @@ -19,6 +19,10 @@ # along with Ansible. If not, see . # +ANSIBLE_METADATA = {'status': ['preview'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_shell diff --git a/windows/win_stat.py b/windows/win_stat.py index f2624b45862..823dde0d15e 100644 --- a/windows/win_stat.py +++ b/windows/win_stat.py @@ -17,6 +17,10 @@ # this is a windows documentation stub, actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_stat diff --git a/windows/win_template.py b/windows/win_template.py index c3a511cad48..f93307a0fcc 100644 --- a/windows/win_template.py +++ b/windows/win_template.py @@ -15,6 +15,10 @@ # You should have received a copy of the GNU General Public License # along with Ansible. If not, see . +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = r''' --- module: win_template diff --git a/windows/win_user.py b/windows/win_user.py index 376ff487fb3..3158208db6f 100644 --- a/windows/win_user.py +++ b/windows/win_user.py @@ -21,6 +21,10 @@ # this is a windows documentation stub. actual code lives in the .ps1 # file of the same name +ANSIBLE_METADATA = {'status': ['stableinterface'], + 'supported_by': 'core', + 'version': '1.0'} + DOCUMENTATION = ''' --- module: win_user From 567a9675b233d062535f0fefd13eb93322b4dd30 Mon Sep 17 00:00:00 2001 From: James Cammarata Date: Tue, 6 Dec 2016 10:10:55 -0500 Subject: [PATCH 768/770] Removing unnecessary files before repo merge --- CONTRIBUTING.md | 32 -- COPYING | 675 ------------------------------------------ README.md | 30 -- VERSION | 1 - __init__.py | 0 shippable.yml | 63 ---- test-requirements.txt | 2 - 7 files changed, 803 deletions(-) delete mode 100644 CONTRIBUTING.md delete mode 100644 COPYING delete mode 100644 README.md delete mode 100644 VERSION delete mode 100644 __init__.py delete mode 100644 shippable.yml delete mode 100644 test-requirements.txt diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 7ec0a622a71..00000000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1,32 +0,0 @@ -Welcome To Ansible GitHub -========================= - -Hi! Nice to see you here! - -If you'd like to ask a question -=============================== - -Please see [this web page ](http://docs.ansible.com/community.html) for community information, which includes pointers on how to ask questions on the [mailing lists](http://docs.ansible.com/community.html#mailing-list-information) and IRC. - -The github issue tracker is not the best place for questions for various reasons, but both IRC and the mailing list are very helpful places for those things, and that page has the pointers to those. - -If you'd like to contribute code -================================ - -Please see [this web page](http://docs.ansible.com/community.html) for information about the contribution process. Important license agreement information is also included on that page. - -If you'd like to file a bug -=========================== - -I'd also read the community page above, but in particular, make sure you copy [this issue template](https://github.com/ansible/ansible-modules-core/blob/devel/.github/ISSUE_TEMPLATE.md) into your ticket description. We have a friendly neighborhood bot that will remind you if you forget :) This template helps us organize tickets faster and prevents asking some repeated questions, so it's very helpful to us and we appreciate your help with it. - -Also please make sure you are testing on the latest released version of Ansible or the development branch. - -If you'd like to contribute code to an existing module -====================================================== -Each module in Core is maintained by the owner of that module; each module's owner is indicated in the documentation section of the module itself. Any pull request for a module that is given a +1 by the owner in the comments will be merged by the Ansible team. - -Thanks! - - - diff --git a/COPYING b/COPYING deleted file mode 100644 index 10926e87f11..00000000000 --- a/COPYING +++ /dev/null @@ -1,675 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program 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. - - This program 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 this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. - diff --git a/README.md b/README.md deleted file mode 100644 index fb71dafe030..00000000000 --- a/README.md +++ /dev/null @@ -1,30 +0,0 @@ -[![Build Status](https://api.shippable.com/projects/573f79d02a8192902e20e34e/badge?branch=devel)](https://app.shippable.com/projects/573f79d02a8192902e20e34e) - -ansible-modules-core -==================== - -This repo contains Ansible's most popular modules that are shipped with Ansible. - -New module submissions for modules that do not yet exist should be submitted to ansible-modules-extras, rather than this repo. - -Take care to submit tickets to the appropriate repo where modules are contained. The docs.ansible.com website indicates this at the bottom of each module documentation page. - -Reporting bugs -============== - -Take care to submit tickets to the appropriate repo where modules are contained. The repo is mentioned at the bottom of module documentation page at [docs.ansible.com](http://docs.ansible.com/). - -Testing modules -=============== - -Ansible [module development guide](http://docs.ansible.com/developing_modules.html#testing-modules) contains the latest info about that. - -License -======= - -As with Ansible, modules distributed with Ansible are GPLv3 licensed. User generated modules not part of this project can be of any license. - -Installation -============ - -There should be no need to install this repo separately as it should be included in any Ansible install using the official documented methods. diff --git a/VERSION b/VERSION deleted file mode 100644 index 47c909bbc53..00000000000 --- a/VERSION +++ /dev/null @@ -1 +0,0 @@ -2.0.0-0.5.beta3 diff --git a/__init__.py b/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/shippable.yml b/shippable.yml deleted file mode 100644 index f5314c723fa..00000000000 --- a/shippable.yml +++ /dev/null @@ -1,63 +0,0 @@ -language: python - -env: - matrix: - - TEST=none - -matrix: - exclude: - - env: TEST=none - include: - - env: TEST=integration IMAGE=ansible/ansible:centos6 - - env: TEST=integration IMAGE=ansible/ansible:centos7 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora-rawhide PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:fedora23 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:opensuseleap PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1204 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1404 PRIVILEGED=true - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604 PRIVILEGED=true - - - env: TEST=integration IMAGE=ansible/ansible:ubuntu1604py3 PYTHON3=1 PRIVILEGED=true - - - env: TEST=integration PLATFORM=windows VERSION=2008-SP2 - - env: TEST=integration PLATFORM=windows VERSION=2008-R2_SP1 - - env: TEST=integration PLATFORM=windows VERSION=2012-RTM - - env: TEST=integration PLATFORM=windows VERSION=2012-R2_RTM - - - env: TEST=integration PLATFORM=freebsd VERSION=10.3-STABLE PRIVILEGED=true - - - env: TEST=integration PLATFORM=osx VERSION=10.11 - - - env: TEST=sanity INSTALL_DEPS=1 - - - env: TEST=docs -build: - pre_ci_boot: - options: "--privileged=false --net=bridge" - ci: - - test/utils/shippable/ci.sh - -integrations: - notifications: - - integrationName: email - type: email - on_success: never - on_failure: never - on_start: never - on_pull_request: never - - integrationName: irc - type: irc - recipients: - - "chat.freenode.net#ansible-notices" - on_success: change - on_failure: always - on_start: never - on_pull_request: always - - integrationName: slack - type: slack - recipients: - - "#shippable" - on_success: change - on_failure: always - on_start: never - on_pull_request: never diff --git a/test-requirements.txt b/test-requirements.txt deleted file mode 100644 index 93253de97a3..00000000000 --- a/test-requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -mock -pytest From 5f86be7927f1bb3c514898905d59ef46fbe4508a Mon Sep 17 00:00:00 2001 From: jctanner Date: Fri, 9 Dec 2016 10:50:15 -0500 Subject: [PATCH 769/770] Update templates (#5869) --- .github/ISSUE_TEMPLATE.md | 56 ++------------------------------ .github/PULL_REQUEST_TEMPLATE.md | 37 ++------------------- 2 files changed, 6 insertions(+), 87 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index cc50f9530da..300886a6973 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,55 +1,5 @@ - +# This repository is locked -##### ISSUE TYPE - - - Bug Report - - Feature Idea - - Documentation Report +Please open all new issues and pull requests in https://github.com/ansible/ansible -##### COMPONENT NAME - - -##### ANSIBLE VERSION - -``` - -``` - -##### CONFIGURATION - - -##### OS / ENVIRONMENT - - -##### SUMMARY - - -##### STEPS TO REPRODUCE - - - -``` - -``` - - - -##### EXPECTED RESULTS - - -##### ACTUAL RESULTS - - - -``` - -``` +For more information please see http://docs.ansible.com/ansible/dev_guide/repomerge.html diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 4bfadfb69e3..300886a6973 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,36 +1,5 @@ -##### ISSUE TYPE - - - Feature Pull Request - - New Module Pull Request - - Bugfix Pull Request - - Docs Pull Request +# This repository is locked -##### COMPONENT NAME - +Please open all new issues and pull requests in https://github.com/ansible/ansible -##### ANSIBLE VERSION - -``` - -``` - -##### SUMMARY - - - - - -``` - -``` +For more information please see http://docs.ansible.com/ansible/dev_guide/repomerge.html From 00911a75ad6635834b6d28eef41f197b2f73c381 Mon Sep 17 00:00:00 2001 From: Matt Clay Date: Tue, 13 Dec 2016 17:13:47 -0800 Subject: [PATCH 770/770] Add README.md explaining repo merge. --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 00000000000..3bb1f395c56 --- /dev/null +++ b/README.md @@ -0,0 +1,6 @@ +**NOTE:** As of Ansible 2.3, modules are now in the +[main Ansible repository](https://github.com/ansible/ansible/tree/devel/lib/ansible/modules). + +See the [repo merge guide](https://docs.ansible.com/ansible/dev_guide/repomerge.html) for more information. + +This repo still exists to allow bug fixes for `stable-2.2` and older releases.