From 8ddffcb1a6a1cc7929d35fdcf278e1bb4381757c Mon Sep 17 00:00:00 2001 From: Peter Sprygada Date: Wed, 25 Jan 2017 10:15:26 -0500 Subject: [PATCH] new connection plugin netconf (#20636) * adds connection plugin for creating modules that use netconf * adds basic unit test cases for connection plugin --- lib/ansible/plugins/connection/netconf.py | 121 ++++++++++++++++ test/units/plugins/connection/test_netconf.py | 133 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 lib/ansible/plugins/connection/netconf.py create mode 100644 test/units/plugins/connection/test_netconf.py diff --git a/lib/ansible/plugins/connection/netconf.py b/lib/ansible/plugins/connection/netconf.py new file mode 100644 index 00000000000..30648767442 --- /dev/null +++ b/lib/ansible/plugins/connection/netconf.py @@ -0,0 +1,121 @@ +# +# (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 . +from __future__ import (absolute_import, division, print_function) +__metaclass__ = type + +import os +import re +import socket +import json +import signal + +from ansible import constants as C +from ansible.errors import AnsibleConnectionFailure, AnsibleError +from ansible.plugins.connection import ConnectionBase, ensure_connect +from ansible.module_utils.six.moves import StringIO + +try: + from ncclient import manager + from ncclient.operations import RPCError + from ncclient.transport.errors import SSHUnknownHostError + from ncclient.xml_ import to_ele, to_xml +except ImportError: + raise AnsibleError("ncclient is not installed") + +try: + from __main__ import display +except ImportError: + from ansible.utils.display import Display + display = Display() + +class Connection(ConnectionBase): + ''' NetConf base connections ''' + + transport = 'netconf' + has_pipelining = False + action_handler = 'network' + + def __init__(self, play_context, new_stdin, *args, **kwargs): + super(Connection, self).__init__(play_context, new_stdin, *args, **kwargs) + + self._network_os = self._play_context.network_os or 'default' + display.vvv('network_os is set to %s' % self._network_os, play_context.remote_addr) + + self._manager = None + self._connected = False + + def _connect(self): + super(Connection, self)._connect() + + allow_agent = True + if self._play_context.password is not None: + allow_agent = False + + key_filename = None + if self._play_context.private_key_file: + key_filename = os.path.expanduser(self._play_context.private_key_file) + + try: + self._manager = manager.connect( + host=self._play_context.remote_addr, + port=self._play_context.port or 830, + username=self._play_context.remote_user, + password=self._play_context.password, + key_filename=str(key_filename), + hostkey_verify=C.HOST_KEY_CHECKING, + look_for_keys=C.PARAMIKO_LOOK_FOR_KEYS, + allow_agent=allow_agent, + timeout=self._play_context.timeout, + device_params={'name': self._network_os} + ) + except SSHUnknownHostError as exc: + raise AnsibleConnectionFailure(str(exc)) + + if not self._manager.connected: + return (1, '', 'not connected') + + self._connected = True + return (0, self._manager.session_id, '') + + def close(self): + if self._manager: + self._manager.close_session() + self._connected = False + super(Connection, self).close() + + @ensure_connect + def exec_command(self, request): + """Sends the request to the node and returns the reply + """ + req = to_ele(request) + if req is None: + return (1, '', 'unable to parse request') + + try: + reply = self._manager.rpc(req) + except RPCError as exc: + return (1, '', to_xml(exc.xml)) + + return (0, reply.data_xml, '') + + def fetch_file(self): + pass + + def put_file(self): + pass + diff --git a/test/units/plugins/connection/test_netconf.py b/test/units/plugins/connection/test_netconf.py new file mode 100644 index 00000000000..2b4985603fc --- /dev/null +++ b/test/units/plugins/connection/test_netconf.py @@ -0,0 +1,133 @@ +# +# (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 sys +import re +import json + +from io import StringIO + +from ansible.compat.tests import unittest +from ansible.compat.tests.mock import patch, MagicMock, PropertyMock + +from ansible.errors import AnsibleConnectionFailure +from ansible.playbook.play_context import PlayContext + +PY3 = sys.version_info[0] == 3 + +builtin_import = __import__ + +mock_ncclient = MagicMock(name='ncclient') + +def import_mock(name, *args): + if name.startswith('ncclient'): + return mock_ncclient + return builtin_import(name, *args) + +if PY3: + with patch('builtins.__import__', side_effect=import_mock): + from ansible.plugins.connection import netconf +else: + with patch('__builtin__.__import__', side_effect=import_mock): + from ansible.plugins.connection import netconf + +class TestNetconfConnectionClass(unittest.TestCase): + + def test_netconf_init(self): + pc = PlayContext() + new_stdin = StringIO() + + conn = netconf.Connection(pc, new_stdin) + + self.assertEqual('default', conn._network_os) + self.assertIsNone(conn._manager) + self.assertFalse(conn._connected) + + def test_netconf__connect(self): + pc = PlayContext() + new_stdin = StringIO() + + conn = netconf.Connection(pc, new_stdin) + + mock_manager = MagicMock(name='self._manager.connect') + type(mock_manager).session_id = PropertyMock(return_value='123456789') + netconf.manager.connect.return_value = mock_manager + + rc, out, err = conn._connect() + + self.assertEqual(0, rc) + self.assertEqual('123456789', out) + self.assertEqual('', err) + self.assertTrue(conn._connected) + + def test_netconf_exec_command(self): + pc = PlayContext() + new_stdin = StringIO() + + conn = netconf.Connection(pc, new_stdin) + conn._connected = True + + mock_manager = MagicMock(name='self._manager') + + mock_reply = MagicMock(name='reply') + type(mock_reply).data_xml = PropertyMock(return_value='') + + mock_manager.rpc.return_value = mock_reply + + conn._manager = mock_manager + + rc, out, err = conn.exec_command('') + + netconf.to_ele.assert_called_with('') + + self.assertEqual(0, rc) + self.assertEqual('', out) + self.assertEqual('', err) + + def test_netconf_exec_command_invalid_request(self): + pc = PlayContext() + new_stdin = StringIO() + + conn = netconf.Connection(pc, new_stdin) + conn._connected = True + + netconf.to_ele.return_value = None + + rc, out, err = conn.exec_command('test string') + + self.assertEqual(1, rc) + self.assertEqual('', out) + self.assertEqual('unable to parse request', err) + + def test_fetch_file(self): + pc = PlayContext() + new_stdin = StringIO() + conn = netconf.Connection(pc, new_stdin) + self.assertIsNone(conn.fetch_file()) + + def test_put_file(self): + pc = PlayContext() + new_stdin = StringIO() + conn = netconf.Connection(pc, new_stdin) + self.assertIsNone(conn.put_file()) + +