From 6152328abbd0749f0b02e7138ddabf6b339f6da4 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Mon, 29 Aug 2016 21:10:29 -0700 Subject: [PATCH 1/4] 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 2/4] 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 @@ DOCUMENTATION = """ --- 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 @@ DOCUMENTATION = """ --- 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 2a06a594ec94e524db80d641c56dce8617526d63 Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Tue, 30 Aug 2016 20:06:52 -0700 Subject: [PATCH 3/4] 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 @@ 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 + - 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 @@ options: - 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 @@ 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 + - 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 @@ options: 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 @@ options: 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 @@ options: 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 @@ options: EXAMPLES = """ - dnos10_config: lines: ['hostname {{ inventory_hostname }}'] - force: yes + provider: "{{ cli }}" - dnos10_config: lines: @@ -130,9 +149,10 @@ EXAMPLES = """ - 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 @@ EXAMPLES = """ - 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 @@ responses: 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 51c13ad82db731604a7f9ffda0d6d4ffc893d4ae Mon Sep 17 00:00:00 2001 From: Senthil Kumar Ganesan Date: Wed, 31 Aug 2016 14:39:16 -0700 Subject: [PATCH 4/4] 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 @@ warnings: 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 @@ saved: 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):