Add backup parameter to cli_config (#50206)

* Add backup parameter to cli_config

* Add a unit test for cli_config backup
This commit is contained in:
Nathaniel Case 2018-12-21 09:55:14 -05:00 committed by GitHub
parent 92de28756d
commit 0b3aa75b7f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 206 additions and 2 deletions

View file

@ -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:

View file

@ -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

View file

@ -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 <http://www.gnu.org/licenses/>.
# 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

View file

@ -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 <http://www.gnu.org/licenses/>.
# 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)