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/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.
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
+
+
+
+ Parameter |
+ Choices/Defaults |
+ Comments |
+
+
+
+
+ defaults
+
+
+ boolean
+
+ |
+
+
+ |
+
+ 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
+
+
+
+ Key |
+ Returned |
+ Description |
+
+
+
+
+ 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..9cfd41399
--- /dev/null
+++ b/plugins/action/cli_backup.py
@@ -0,0 +1,31 @@
+#
+# 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..b793fb204
--- /dev/null
+++ b/plugins/modules/cli_backup.py
@@ -0,0 +1,126 @@
+#!/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
+"""
+
+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()
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"] == (