diff --git a/lib/ansible/modules/network/cli/cli_config.py b/lib/ansible/modules/network/cli/cli_config.py
index b6e7ef36a8..5782085f6b 100644
--- a/lib/ansible/modules/network/cli/cli_config.py
+++ b/lib/ansible/modules/network/cli/cli_config.py
@@ -45,6 +45,17 @@ options:
Use I(net_put) or I(nxos_file_copy) module to copy the flat file
to remote device and then use set the fullpath to this argument.
type: 'str'
+ backup:
+ description:
+ - This argument will cause the module to create a full backup of
+ the current 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 or role root directory, if
+ playbook is part of an ansible role. If the directory does not exist,
+ it is created.
+ type: bool
+ default: 'no'
+ version_added: "2.8"
rollback:
description:
- The C(rollback) argument instructs the module to rollback the
@@ -140,6 +151,11 @@ commands:
returned: always
type: list
sample: ['interface Loopback999', 'no shutdown']
+backup_path:
+ description: The full path to the backup file
+ returned: when backup is yes
+ type: str
+ sample: /playbooks/ansible/backup/hostname_config.2016-07-16@22:28:34
"""
import json
@@ -284,6 +300,7 @@ def main():
"""main entry point for execution
"""
argument_spec = dict(
+ backup=dict(default=False, type='bool'),
config=dict(type='str'),
commit=dict(type='bool'),
replace=dict(type='str'),
@@ -297,7 +314,7 @@ def main():
)
mutually_exclusive = [('config', 'rollback')]
- required_one_of = [['config', 'rollback']]
+ required_one_of = [['backup', 'config', 'rollback']]
module = AnsibleModule(argument_spec=argument_spec,
mutually_exclusive=mutually_exclusive,
@@ -323,6 +340,9 @@ def main():
candidate = to_text(module.params['config'])
running = connection.get_config(flags=flags)
+ if module.params['backup']:
+ result['__backup__'] = running
+
try:
result.update(run(module, capabilities, connection, candidate, running))
except Exception as exc:
diff --git a/lib/ansible/plugins/action/cli_config.py b/lib/ansible/plugins/action/cli_config.py
index 3041b10049..433d52ad84 100644
--- a/lib/ansible/plugins/action/cli_config.py
+++ b/lib/ansible/plugins/action/cli_config.py
@@ -19,13 +19,54 @@
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
+import glob
+import os
+import re
+import time
+
from ansible.plugins.action.normal import ActionModule as _ActionModule
+PRIVATE_KEYS_RE = re.compile('__.+__')
+
+
class ActionModule(_ActionModule):
def run(self, tmp=None, task_vars=None):
if self._play_context.connection != 'network_cli':
return {'failed': True, 'msg': 'Connection type %s is not valid for cli_config module' % self._play_context.connection}
- return super(ActionModule, self).run(task_vars=task_vars)
+ result = super(ActionModule, self).run(task_vars=task_vars)
+
+ if self._task.args.get('backup') and result.get('__backup__'):
+ # User requested backup and no error occurred in module.
+ # NOTE: If there is a parameter error, _backup key may not be in results.
+ filepath = self._write_backup(task_vars['inventory_hostname'],
+ result['__backup__'])
+
+ result['backup_path'] = filepath
+
+ # strip out any keys that have two leading and two trailing
+ # underscore characters
+ for key in list(result.keys()):
+ if PRIVATE_KEYS_RE.match(key):
+ del result[key]
+
+ return result
+
+ def _get_working_path(self):
+ cwd = self._loader.get_basedir()
+ if self._task._role is not None:
+ cwd = self._task._role._role_path
+ return cwd
+
+ def _write_backup(self, host, contents):
+ backup_path = self._get_working_path() + '/backup'
+ if not os.path.exists(backup_path):
+ os.mkdir(backup_path)
+ for existing_backup in glob.glob('%s/%s_config.*' % (backup_path, host)):
+ os.remove(existing_backup)
+ tstamp = time.strftime("%Y-%m-%d@%H:%M:%S", time.localtime(time.time()))
+ filename = '%s/%s_config.%s' % (backup_path, host, tstamp)
+ open(filename, 'w').write(contents)
+ return filename
diff --git a/test/units/modules/network/cli/__init__.py b/test/units/modules/network/cli/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/test/units/modules/network/cli/cli_module.py b/test/units/modules/network/cli/cli_module.py
new file mode 100644
index 0000000000..e92bc8ffc0
--- /dev/null
+++ b/test/units/modules/network/cli/cli_module.py
@@ -0,0 +1,88 @@
+# (c) 2016 Red Hat 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 .
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+import json
+import os
+
+from units.modules.utils import AnsibleExitJson, AnsibleFailJson, ModuleTestCase
+
+
+fixture_path = os.path.join(os.path.dirname(__file__), 'fixtures')
+fixture_data = {}
+
+
+def load_fixture(name):
+ path = os.path.join(fixture_path, name)
+
+ if path in fixture_data:
+ return fixture_data[path]
+
+ with open(path) as fixture:
+ data = fixture.read()
+
+ try:
+ data = json.loads(data)
+ except ValueError:
+ pass
+
+ fixture_data[path] = data
+ return data
+
+
+class TestCliModule(ModuleTestCase):
+
+ def execute_module(self, failed=False, changed=False, commands=None, sort=True):
+
+ self.load_fixtures(commands)
+
+ if failed:
+ result = self.failed()
+ self.assertTrue(result['failed'], result)
+ else:
+ result = self.changed(changed)
+ self.assertEqual(result['changed'], changed, result)
+
+ if commands is not None:
+ if sort:
+ self.assertEqual(sorted(commands), sorted(result['commands']), result['commands'])
+ else:
+ self.assertEqual(commands, result['commands'], result['commands'])
+
+ return result
+
+ def failed(self):
+ with self.assertRaises(AnsibleFailJson) as exc:
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertTrue(result['failed'], result)
+ return result
+
+ def changed(self, changed=False):
+ with self.assertRaises(AnsibleExitJson) as exc:
+ self.module.main()
+
+ result = exc.exception.args[0]
+ self.assertEqual(result['changed'], changed, result)
+ return result
+
+ def load_fixtures(self, commands=None):
+ pass
diff --git a/test/units/modules/network/cli/test_cli_config.py b/test/units/modules/network/cli/test_cli_config.py
new file mode 100644
index 0000000000..4fa5f27a9e
--- /dev/null
+++ b/test/units/modules/network/cli/test_cli_config.py
@@ -0,0 +1,55 @@
+# (c) 2016 Red Hat 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 .
+
+# Make coding more python3-ish
+from __future__ import (absolute_import, division, print_function)
+__metaclass__ = type
+
+from units.compat.mock import patch, MagicMock
+from ansible.modules.network.cli import cli_config
+from units.modules.utils import set_module_args
+from .cli_module import TestCliModule, load_fixture
+
+
+class TestCliConfigModule(TestCliModule):
+
+ module = cli_config
+
+ def setUp(self):
+ super(TestCliConfigModule, self).setUp()
+
+ self.mock_connection = patch('ansible.modules.network.cli.cli_config.Connection')
+ self.get_connection = self.mock_connection.start()
+
+ self.conn = self.get_connection()
+
+ def tearDown(self):
+ super(TestCliConfigModule, self).tearDown()
+
+ self.mock_connection.stop()
+
+ @patch('ansible.modules.network.cli.cli_config.run')
+ def test_cli_config_backup_returns__backup__(self, run_mock):
+ self.conn.get_capabilities = MagicMock(return_value='{}')
+
+ args = dict(backup=True)
+ set_module_args(args)
+
+ run_mock.return_value = {}
+
+ result = self.execute_module()
+ self.assertIn('__backup__', result)