From 3f0f7c4f4e36b5563169f8536ce647bbac6ffe00 Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Sun, 12 Mar 2017 08:37:45 -0500 Subject: [PATCH] updates junos shared lib and action handler (#22541) * removes cli functions from shared lib * adds cli functions to junos_netconf module * statically pins junos_netconf to cli transport * all other modules use netconf transport * adds command rpc function to junos shared --- lib/ansible/module_utils/junos.py | 91 +++---------- .../modules/network/junos/junos_netconf.py | 37 ++++-- lib/ansible/plugins/action/junos.py | 10 +- test/units/modules/network/junos/__init__.py | 0 .../fixtures/junos_command_show_version.txt | 4 - .../modules/network/junos/junos_module.py | 122 ------------------ .../network/junos/test_junos_command.py | 105 --------------- 7 files changed, 48 insertions(+), 321 deletions(-) delete mode 100644 test/units/modules/network/junos/__init__.py delete mode 100644 test/units/modules/network/junos/fixtures/junos_command_show_version.txt delete mode 100644 test/units/modules/network/junos/junos_module.py delete mode 100644 test/units/modules/network/junos/test_junos_command.py diff --git a/lib/ansible/module_utils/junos.py b/lib/ansible/module_utils/junos.py index 2475feb724..17873b3937 100644 --- a/lib/ansible/module_utils/junos.py +++ b/lib/ansible/module_utils/junos.py @@ -31,8 +31,6 @@ JSON_ACTIONS = frozenset(['merge', 'override', 'update']) FORMATS = frozenset(['xml', 'text', 'json']) CONFIG_FORMATS = frozenset(['xml', 'text', 'json', 'set']) -_DEVICE_CONFIGS = {} - junos_argument_spec = { 'host': dict(), 'port': dict(type='int'), @@ -41,17 +39,17 @@ junos_argument_spec = { 'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'), 'timeout': dict(type='int', default=10), 'provider': dict(type='dict'), - 'transport': dict(choices=['cli', 'netconf']) + 'transport': dict() } def check_args(module, warnings): provider = module.params['provider'] or {} for key in junos_argument_spec: - if key in ('provider', 'transport') and module.params[key]: + if key in ('provider',) and module.params[key]: warnings.append('argument %s has been deprecated and will be ' 'removed in a future version' % key) -def validate_rollback_id(value): +def _validate_rollback_id(value): try: if not 0 <= int(value) <= 49: raise ValueError @@ -77,7 +75,7 @@ def load_configuration(module, candidate=None, action='merge', rollback=None, fo module.fail_json(msg='format must be text when action is set') if rollback is not None: - validate_rollback_id(rollback) + _validate_rollback_id(rollback) xattrs = {'rollback': str(rollback)} else: xattrs = {'action': action, 'format': format} @@ -102,7 +100,7 @@ def get_configuration(module, compare=False, format='xml', rollback='0'): module.fail_json(msg='invalid config format specified') xattrs = {'format': format} if compare: - validate_rollback_id(rollback) + _validate_rollback_id(rollback) xattrs['compare'] = 'rollback' xattrs['rollback'] = str(rollback) return send_request(module, Element('get-configuration', xattrs)) @@ -119,6 +117,13 @@ def commit_configuration(module, confirm=False, check=False, comment=None, confi children(obj, ('confirm-timeout', int(confirm_timeout))) return send_request(module, obj) +def command(module, command, format='text', rpc_only=False): + xattrs = {'format': format} + if rpc_only: + command += ' | display xml rpc' + xattrs['format'] = 'text' + return send_request(module, Element('command', xattrs, text=command)) + lock_configuration = lambda x: send_request(x, Element('lock-configuration')) unlock_configuration = lambda x: send_request(x, Element('unlock-configuration')) @@ -136,9 +141,9 @@ def get_diff(module): if output: return output[0].text -def load(module, candidate, action='merge', commit=False, format='xml'): - """Loads a configuration element into the target system - """ +def load_config(module, candidate, action='merge', commit=False, format='xml', + comment=None, confirm=False, confirm_timeout=None): + with locked_config(module): resp = load_configuration(module, candidate, action=action, format=format) @@ -148,72 +153,10 @@ def load(module, candidate, action='merge', commit=False, format='xml'): if diff: diff = str(diff).strip() if commit: - commit_configuration(module) + commit_configuration(module, confirm=confirm, comment=comment, + confirm_timeout=confirm_timeout) else: discard_changes(module) return diff - - -# START CLI FUNCTIONS - -def get_config(module, flags=[]): - cmd = 'show configuration ' - cmd += ' '.join(flags) - cmd = cmd.strip() - - try: - return _DEVICE_CONFIGS[cmd] - except KeyError: - rc, out, err = exec_command(module, cmd) - if rc != 0: - module.fail_json(msg='unable to retrieve current config', stderr=err) - cfg = str(out).strip() - _DEVICE_CONFIGS[cmd] = cfg - return cfg - -def run_commands(module, commands, check_rc=True): - responses = list() - for cmd in to_list(commands): - cmd = module.jsonify(cmd) - rc, out, err = exec_command(module, cmd) - if check_rc and rc != 0: - module.fail_json(msg=err, rc=rc) - - try: - out = module.from_json(out) - except ValueError: - out = str(out).strip() - - responses.append(out) - return responses - -def load_config(module, config, commit=False, comment=None, - confirm=False, confirm_timeout=None): - - exec_command(module, 'configure') - - for item in to_list(config): - rc, out, err = exec_command(module, item) - if rc != 0: - module.fail_json(msg=str(err)) - - exec_command(module, 'top') - rc, diff, err = exec_command(module, 'show | compare') - - if commit: - cmd = 'commit' - if commit: - cmd = 'commit confirmed' - if confirm_timeout: - cmd +' %s' % confirm_timeout - if comment: - cmd += ' comment "%s"' % comment - cmd += ' and-quit' - exec_command(module, cmd) - else: - for cmd in ['rollback 0', 'exit']: - exec_command(module, cmd) - - return str(diff).strip() diff --git a/lib/ansible/modules/network/junos/junos_netconf.py b/lib/ansible/modules/network/junos/junos_netconf.py index 0691ba8208..acf860ed2f 100644 --- a/lib/ansible/modules/network/junos/junos_netconf.py +++ b/lib/ansible/modules/network/junos/junos_netconf.py @@ -77,19 +77,13 @@ commands: """ import re -from ansible.module_utils.junos import load_config, get_config from ansible.module_utils.junos import junos_argument_spec, check_args from ansible.module_utils.basic import AnsibleModule +from ansible.module_utils.connection import exec_command from ansible.module_utils.six import iteritems USE_PERSISTENT_CONNECTION = True -def check_transport(module): - transport = (module.params['provider'] or {}).get('transport') - - if transport == 'netconf': - module.fail_json(msg='junos_netconf module is only supported over cli transport') - def map_obj_to_commands(updates, module): want, have = updates @@ -144,6 +138,32 @@ def map_params_to_obj(module): return obj +def get_config(module, flags=[]): + rc, out, err = exec_command(module, cmd) + if rc != 0: + module.fail_json(msg='unable to retrieve current config', stderr=err) + return str(out).strip() + +def load_config(module, config, commit=False): + + exec_command(module, 'configure') + + for item in to_list(config): + rc, out, err = exec_command(module, item) + if rc != 0: + module.fail_json(msg=str(err)) + + exec_command(module, 'top') + rc, diff, err = exec_command(module, 'show | compare') + + if commit: + exec_command(module, 'commit and-quit') + else: + for cmd in ['rollback 0', 'exit']: + exec_command(module, cmd) + + return str(diff).strip() + def main(): """main entry point for module execution """ @@ -153,13 +173,10 @@ def main(): ) argument_spec.update(junos_argument_spec) - argument_spec['transport'] = dict(choices=['cli'], default='cli') module = AnsibleModule(argument_spec=argument_spec, supports_check_mode=True) - check_transport(module) - warnings = list() check_args(module, warnings) diff --git a/lib/ansible/plugins/action/junos.py b/lib/ansible/plugins/action/junos.py index c5cbc2be72..ca00916bf5 100644 --- a/lib/ansible/plugins/action/junos.py +++ b/lib/ansible/plugins/action/junos.py @@ -29,9 +29,6 @@ from ansible.plugins import connection_loader, module_loader from ansible.compat.six import iteritems from ansible.module_utils.junos import junos_argument_spec from ansible.module_utils.basic import AnsibleFallbackNotFound -from ansible.module_utils._text import to_bytes - -from ncclient.xml_ import new_ele, sub_ele, to_xml try: from __main__ import display @@ -56,12 +53,11 @@ class ActionModule(_ActionModule): return super(ActionModule, self).run(tmp, task_vars) provider = self.load_provider() - transport = provider['transport'] or 'cli' pc = copy.deepcopy(self._play_context) pc.network_os = 'junos' - if transport == 'cli': + if self._task.action == 'junos_netconf': pc.connection = 'network_cli' pc.port = provider['port'] or self._play_context.port or 22 else: @@ -98,7 +94,6 @@ class ActionModule(_ActionModule): connection.exec_command('exit') rc, out, err = connection.exec_command('prompt()') - task_vars['ansible_socket'] = socket_path return super(ActionModule, self).run(tmp, task_vars) @@ -106,6 +101,9 @@ class ActionModule(_ActionModule): def _get_socket_path(self, play_context): ssh = connection_loader.get('ssh', class_only=True) path = unfrackpath("$HOME/.ansible/pc") + # use play_context.connection instea of play_context.port to avoid + # collision if netconf is listening on port 22 + #cp = ssh._create_control_path(play_context.remote_addr, play_context.connection, play_context.remote_user) cp = ssh._create_control_path(play_context.remote_addr, play_context.port, play_context.remote_user) return cp % dict(directory=path) diff --git a/test/units/modules/network/junos/__init__.py b/test/units/modules/network/junos/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/test/units/modules/network/junos/fixtures/junos_command_show_version.txt b/test/units/modules/network/junos/fixtures/junos_command_show_version.txt deleted file mode 100644 index 1e0b8706e5..0000000000 --- a/test/units/modules/network/junos/fixtures/junos_command_show_version.txt +++ /dev/null @@ -1,4 +0,0 @@ -Hostname: vsrx01 -Model: vSRX -Junos: 15.1X49-D15.4 -JUNOS Software Release [15.1X49-D15.4] \ No newline at end of file diff --git a/test/units/modules/network/junos/junos_module.py b/test/units/modules/network/junos/junos_module.py deleted file mode 100644 index 0edbff24d8..0000000000 --- a/test/units/modules/network/junos/junos_module.py +++ /dev/null @@ -1,122 +0,0 @@ -# (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 os -import json -import sys - -from ansible.compat.tests import unittest -from ansible.compat.tests.mock import patch, Mock -from ansible.module_utils import basic -from ansible.module_utils._text import to_bytes - - -def set_module_args(args): - args = json.dumps({'ANSIBLE_MODULE_ARGS': args}) - basic._ANSIBLE_ARGS = to_bytes(args) - -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 f: - data = f.read() - - try: - data = json.loads(data) - except: - pass - - fixture_data[path] = data - return data - -class AnsibleExitJson(Exception): - pass - -class AnsibleFailJson(Exception): - pass - - -mock_modules = { - 'ncclient': Mock(), - 'ncclient.xml_': Mock() -} -patch_import = patch.dict('sys.modules', mock_modules) -patch_import.start() - - -class TestJunosModule(unittest.TestCase): - - def execute_module(self, failed=False, changed=False, commands=None, - sort=True, defaults=False): - - 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: - if sort: - self.assertEqual(sorted(commands), sorted(result['commands']), result['commands']) - else: - self.assertEqual(commands, result['commands'], result['commands']) - - return result - - def failed(self): - def fail_json(*args, **kwargs): - kwargs['failed'] = True - raise AnsibleFailJson(kwargs) - - with patch.object(basic.AnsibleModule, 'fail_json', fail_json): - 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): - def exit_json(*args, **kwargs): - if 'changed' not in kwargs: - kwargs['changed'] = False - raise AnsibleExitJson(kwargs) - - with patch.object(basic.AnsibleModule, 'exit_json', exit_json): - 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/junos/test_junos_command.py b/test/units/modules/network/junos/test_junos_command.py deleted file mode 100644 index 5907371c44..0000000000 --- a/test/units/modules/network/junos/test_junos_command.py +++ /dev/null @@ -1,105 +0,0 @@ -# (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 - -from ansible.compat.tests.mock import patch -from .junos_module import TestJunosModule, load_fixture, set_module_args - -from ansible.modules.network.junos import junos_command - - -class TestJunosCommandModule(TestJunosModule): - - module = junos_command - - def setUp(self): - self.mock_run_commands = patch('ansible.modules.network.junos.junos_command.run_commands') - self.run_commands = self.mock_run_commands.start() - - def tearDown(self): - self.mock_run_commands.stop() - - def load_fixtures(self, commands=None): - - def load_from_file(*args, **kwargs): - module, commands = args - output = list() - - for item in commands: - try: - obj = json.loads(item['command']) - command = obj['command'] - except ValueError: - command = item['command'] - filename = 'junos_command_%s.txt' % str(command).replace(' ', '_') - output.append(load_fixture(filename)) - return output - - self.run_commands.side_effect = load_from_file - - def test_junos_command_format_text(self): - set_module_args(dict(commands=['show version'], display='text')) - result = self.execute_module() - self.assertEqual(len(result['stdout']), 1) - self.assertTrue(result['stdout'][0].startswith('Hostname')) - - def test_junos_command_multiple(self): - set_module_args(dict(commands=['show version', 'show version'], display='text')) - result = self.execute_module() - self.assertEqual(len(result['stdout']), 2) - self.assertTrue(result['stdout'][0].startswith('Hostname')) - - def test_junos_command_wait_for(self): - wait_for = 'result[0] contains "Hostname"' - set_module_args(dict(commands=['show version'], wait_for=wait_for, display='text')) - self.execute_module() - - def test_junos_command_wait_for_fails(self): - wait_for = 'result[0] contains "test string"' - set_module_args(dict(commands=['show version'], wait_for=wait_for, display='text')) - self.execute_module(failed=True) - self.assertEqual(self.run_commands.call_count, 10) - - def test_junos_command_retries(self): - wait_for = 'result[0] contains "test string"' - set_module_args(dict(commands=['show version'], wait_for=wait_for, retries=2, display='text')) - self.execute_module(failed=True) - self.assertEqual(self.run_commands.call_count, 2) - - def test_junos_command_match_any(self): - wait_for = ['result[0] contains "Hostname"', - 'result[0] contains "test string"'] - set_module_args(dict(commands=['show version'], wait_for=wait_for, match='any', display='text')) - self.execute_module() - - def test_junos_command_match_all(self): - wait_for = ['result[0] contains "Hostname"', - 'result[0] contains "Model"'] - set_module_args(dict(commands=['show version'], wait_for=wait_for, match='all', display='text')) - self.execute_module() - - def test_junos_command_match_all_failure(self): - wait_for = ['result[0] contains "Hostname"', - 'result[0] contains "test string"'] - commands = ['show version', 'show version'] - set_module_args(dict(commands=commands, wait_for=wait_for, match='all', display='text')) - self.execute_module(failed=True)