From 5c84272dac04be7eeacb71d65434d9d381717951 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Tue, 29 Nov 2022 10:33:41 -0500 Subject: [PATCH 1/5] Initial cli_backup module --- README.md | 1 + docs/ansible.netcommon.cli_backup_module.rst | 149 +++++++++++++++++++ plugins/action/cli_backup.py | 30 ++++ plugins/action/network.py | 9 +- plugins/modules/cli_backup.py | 128 ++++++++++++++++ 5 files changed, 314 insertions(+), 3 deletions(-) create mode 100644 docs/ansible.netcommon.cli_backup_module.rst create mode 100644 plugins/action/cli_backup.py create mode 100644 plugins/modules/cli_backup.py diff --git a/README.md b/README.md index bdc08c141..e3213e478 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,7 @@ Name | Description ### Modules Name | Description --- | --- +[ansible.netcommon.cli_backup](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_backup_module.rst)|Back up device configuration from network devices over network_cli [ansible.netcommon.cli_command](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_command_module.rst)|Run a cli command on cli-based network devices [ansible.netcommon.cli_config](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.cli_config_module.rst)|Push text based configuration to network devices over network_cli [ansible.netcommon.grpc_config](https://github.com/ansible-collections/ansible.netcommon/blob/main/docs/ansible.netcommon.grpc_config_module.rst)|Fetch configuration/state data from gRPC enabled target hosts. diff --git a/docs/ansible.netcommon.cli_backup_module.rst b/docs/ansible.netcommon.cli_backup_module.rst new file mode 100644 index 000000000..6785696c9 --- /dev/null +++ b/docs/ansible.netcommon.cli_backup_module.rst @@ -0,0 +1,149 @@ +.. _ansible.netcommon.cli_backup_module: + + +**************************** +ansible.netcommon.cli_backup +**************************** + +**Back up device configuration from network devices over network_cli** + + +Version added: 4.2.0 + +.. contents:: + :local: + :depth: 1 + + +Synopsis +-------- +- This module provides platform agnostic way of backing up text based configuration from network devices over network_cli connection plugin. + + + + +Parameters +---------- + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + +
ParameterChoices/DefaultsComments
+
+ defaults + +
+ boolean +
+
+
    Choices: +
  • no ←
  • +
  • yes
  • +
+
+
The defaults argument will influence how the running-config is collected from the device. When the value is set to true, the command used to collect the running-config is append with the all keyword. When the value is set to false, the command is issued without the all keyword.
+
+
+ dir_path + +
+ path +
+
+ +
This option provides the path ending with directory name in which the backup configuration file will be stored. If the directory does not exist it will be first created and the filename is either the value of filename or default filename as described in filename options description. If the path value is not given in that case a backup directory will be created in the current working directory and backup configuration will be copied in filename within backup directory.
+
+
+ filename + +
+ string +
+
+ +
The filename to be used to store the backup configuration. If the filename is not given it will be generated based on the hostname, current time and date in format defined by <hostname>_config.<current-date>@<current-time>
+
+
+ + +Notes +----- + +.. note:: + - This module is supported on ``ansible_network_os`` network platforms. See the :ref:`Network Platform Options ` for details. + + + +Examples +-------- + +.. code-block:: yaml + + - name: configurable backup path + ansible.netcommon.cli_backup: + filename: backup.cfg + dir_path: /home/user + + + +Return Values +------------- +Common return values are documented `here `_, the following are the fields unique to this module: + +.. raw:: html + + + + + + + + + + + + +
KeyReturnedDescription
+
+ backup_path + +
+ string +
+
always +
The full path to the backup file
+
+
Sample:
+
/playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34
+
+

+ + +Status +------ + + +Authors +~~~~~~~ + +- Kate Case (@Qalthos) diff --git a/plugins/action/cli_backup.py b/plugins/action/cli_backup.py new file mode 100644 index 000000000..271cfb74e --- /dev/null +++ b/plugins/action/cli_backup.py @@ -0,0 +1,30 @@ +# +# Copyright 2018 Red Hat Inc. +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + +from ansible_collections.ansible.netcommon.plugins.action.network import ( + ActionModule as ActionNetworkModule, +) + + +class ActionModule(ActionNetworkModule): + def run(self, tmp=None, task_vars=None): + if self._play_context.connection.split(".")[-1] != "network_cli": + return { + "failed": True, + "msg": "Connection type %s is not valid for this module" + % self._play_context.connection, + } + result = super(ActionModule, self).run(task_vars=task_vars) + self._handle_backup_option( + result, + task_vars, + self._task.args, + ) + + return result diff --git a/plugins/action/network.py b/plugins/action/network.py index 7e5962239..f9051ac28 100644 --- a/plugins/action/network.py +++ b/plugins/action/network.py @@ -81,11 +81,15 @@ def run(self, tmp=None, task_vars=None): result = super(ActionModule, self).run(task_vars=task_vars) if config_module and self._task.args.get("backup") and not result.get("failed"): - self._handle_backup_option(result, task_vars) + self._handle_backup_option( + result, + task_vars, + self._task.args.get("backup_options"), + ) return result - def _handle_backup_option(self, result, task_vars): + def _handle_backup_option(self, result, task_vars, backup_options): filename = None backup_path = None try: @@ -99,7 +103,6 @@ def _handle_backup_option(self, result, task_vars): except KeyError: raise AnsibleError("Failed while reading configuration backup") - backup_options = self._task.args.get("backup_options") if backup_options: filename = backup_options.get("filename") backup_path = backup_options.get("dir_path") diff --git a/plugins/modules/cli_backup.py b/plugins/modules/cli_backup.py new file mode 100644 index 000000000..54d6ca949 --- /dev/null +++ b/plugins/modules/cli_backup.py @@ -0,0 +1,128 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# (c) 2018, Ansible by Red Hat, inc +# GNU General Public License v3.0+ (see LICENSES/GPL-3.0-or-later.txt or https://www.gnu.org/licenses/gpl-3.0.txt) +# SPDX-License-Identifier: GPL-3.0-or-later + +from __future__ import absolute_import, division, print_function + +__metaclass__ = type + + +DOCUMENTATION = """ +module: cli_backup +author: Kate Case (@Qalthos) +short_description: Back up device configuration from network devices over network_cli +description: +- This module provides platform agnostic way of backing up text based configuration from + network devices over network_cli connection plugin. +version_added: 4.2.0 +extends_documentation_fragment: +- ansible.netcommon.network_agnostic +options: + defaults: + description: + - The I(defaults) argument will influence how the running-config is collected + from the device. When the value is set to true, the command used to collect + the running-config is append with the all keyword. When the value is set to + false, the command is issued without the all keyword. + default: no + type: bool + filename: + description: + - The filename to be used to store the backup configuration. If the filename + is not given it will be generated based on the hostname, current time and + date in format defined by _config.@ + type: str + dir_path: + description: + - This option provides the path ending with directory name in which the backup + configuration file will be stored. If the directory does not exist it will + be first created and the filename is either the value of C(filename) or + default filename as described in C(filename) options description. If the + path value is not given in that case a I(backup) directory will be created + in the current working directory and backup configuration will be copied + in C(filename) within I(backup) directory. + type: path +""" + +EXAMPLES = """ +- name: configurable backup path + ansible.netcommon.cli_backup: + filename: backup.cfg + dir_path: /home/user +""" + +RETURN = """ +backup_path: + description: The full path to the backup file + returned: always + type: str + sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34 +""" + +import json + +from ansible.module_utils._text import to_text +from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import Connection + + +def validate_args(module, device_operations): + """validate param if it is supported on the platform""" + feature_list = [ + "defaults", + ] + + for feature in feature_list: + if module.params[feature]: + supports_feature = device_operations.get("supports_%s" % feature) + if supports_feature is None: + module.fail_json( + msg="This platform does not specify whether %s is supported or not. " + "Please report an issue against this platform's cliconf plugin." % feature + ) + elif not supports_feature: + module.fail_json(msg="Option %s is not supported on this platform" % feature) + + +def main(): + """main entry point for execution""" + argument_spec = dict( + defaults=dict(default=False, type="bool"), + filename=dict(), + dir_path=dict(type="path"), + ) + + module = AnsibleModule( + argument_spec=argument_spec, + ) + + result = {"changed": False} + + connection = Connection(module._socket_path) + capabilities = module.from_json(connection.get_capabilities()) + + if capabilities: + device_operations = capabilities.get("device_operations", dict()) + validate_args(module, device_operations) + else: + device_operations = dict() + + if module.params["defaults"]: + if "get_default_flag" in capabilities.get("rpc"): + flags = connection.get_default_flag() + else: + flags = "all" + else: + flags = [] + + running = connection.get_config(flags=flags) + result["__backup__"] = running + + module.exit_json(**result) + + +if __name__ == "__main__": + main() From 5e3b0fcb458721448ad9108c684f1914f72ee8a4 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Tue, 29 Nov 2022 14:51:50 -0500 Subject: [PATCH 2/5] Fix tests --- .../plugins/action/network/test_network.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/tests/unit/plugins/action/network/test_network.py b/tests/unit/plugins/action/network/test_network.py index bdd10d009..296b9bb7d 100644 --- a/tests/unit/plugins/action/network/test_network.py +++ b/tests/unit/plugins/action/network/test_network.py @@ -65,8 +65,9 @@ def test_backup_options(plugin, backup_dir, backup_file, role_path): # This doesn't need to be conditional, but doing so tests the equivalent # `if backup_options:` in the action plugin itself. + backup_options = None if backup_dir or backup_file: - plugin._task.args["backup_options"] = { + backup_options = { "dir_path": backup_dir, "filename": backup_file, } @@ -81,7 +82,7 @@ def test_backup_options(plugin, backup_dir, backup_file, role_path): try: # result is updated in place, nothing is returned - plugin._handle_backup_option(result, task_vars) + plugin._handle_backup_option(result, task_vars, backup_options) assert not result.get("failed") with open(result["backup_path"]) as backup_file_obj: @@ -108,7 +109,7 @@ def test_backup_options(plugin, backup_dir, backup_file, role_path): if backup_file: # check for idempotency result = {"__backup__": content} - plugin._handle_backup_option(result, task_vars) + plugin._handle_backup_option(result, task_vars, backup_options) assert not result.get("failed") assert result["changed"] is False @@ -121,7 +122,7 @@ def test_backup_no_content(plugin): result = {} task_vars = {} with pytest.raises(AnsibleError, match="Failed while reading configuration backup"): - plugin._handle_backup_option(result, task_vars) + plugin._handle_backup_option(result, task_vars, backup_options=None) def test_backup_options_error(plugin): @@ -129,13 +130,11 @@ def test_backup_options_error(plugin): task_vars = {} with tempfile.NamedTemporaryFile() as existing_file: - plugin._task.args = { - "backup_options": { - "dir_path": existing_file.name, - "filename": "backup_file", - } + backup_options = { + "dir_path": existing_file.name, + "filename": "backup_file", } - plugin._handle_backup_option(result, task_vars) + plugin._handle_backup_option(result, task_vars, backup_options) assert result["failed"] is True assert result["msg"] == ( From e6b273f58498358462a0244cb509752f6d09652a Mon Sep 17 00:00:00 2001 From: Kate Case Date: Wed, 7 Jun 2023 10:30:57 -0400 Subject: [PATCH 3/5] Add changelog --- changelogs/fragments/cli_backup.yaml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/cli_backup.yaml diff --git a/changelogs/fragments/cli_backup.yaml b/changelogs/fragments/cli_backup.yaml new file mode 100644 index 000000000..cad6c5b5b --- /dev/null +++ b/changelogs/fragments/cli_backup.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - Add new module cli_backup that exclusively handles configuration backup. From bc8357e2d279c9c76a7577eeb91b2e9105df8007 Mon Sep 17 00:00:00 2001 From: Kate Case Date: Wed, 7 Jun 2023 10:37:52 -0400 Subject: [PATCH 4/5] Remove unused imports --- plugins/modules/cli_backup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/modules/cli_backup.py b/plugins/modules/cli_backup.py index 54d6ca949..fc69a20f2 100644 --- a/plugins/modules/cli_backup.py +++ b/plugins/modules/cli_backup.py @@ -62,9 +62,6 @@ sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34 """ -import json - -from ansible.module_utils._text import to_text from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.connection import Connection From d0d50b9247d93b96256df6f9ee783ad01ad19086 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 22 Aug 2023 13:14:23 +0000 Subject: [PATCH 5/5] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- plugins/action/cli_backup.py | 1 + plugins/modules/cli_backup.py | 1 + 2 files changed, 2 insertions(+) diff --git a/plugins/action/cli_backup.py b/plugins/action/cli_backup.py index 271cfb74e..9cfd41399 100644 --- a/plugins/action/cli_backup.py +++ b/plugins/action/cli_backup.py @@ -5,6 +5,7 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type from ansible_collections.ansible.netcommon.plugins.action.network import ( diff --git a/plugins/modules/cli_backup.py b/plugins/modules/cli_backup.py index fc69a20f2..b793fb204 100644 --- a/plugins/modules/cli_backup.py +++ b/plugins/modules/cli_backup.py @@ -7,6 +7,7 @@ from __future__ import absolute_import, division, print_function + __metaclass__ = type