1#
2# (c) 2017 Red Hat Inc.
3#
4# This file is part of Ansible
5#
6# Ansible is free software: you can redistribute it and/or modify
7# it under the terms of the GNU General Public License as published by
8# the Free Software Foundation, either version 3 of the License, or
9# (at your option) any later version.
10#
11# Ansible is distributed in the hope that it will be useful,
12# but WITHOUT ANY WARRANTY; without even the implied warranty of
13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14# GNU General Public License for more details.
15#
16# You should have received a copy of the GNU General Public License
17# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
18#
19from __future__ import absolute_import, division, print_function
20
21__metaclass__ = type
22
23DOCUMENTATION = """
24author: Ansible Networking Team
25netconf: junos
26short_description: Use junos netconf plugin to run netconf commands on Juniper JUNOS
27  platform
28description:
29- This junos plugin provides low level abstraction apis for sending and receiving
30  netconf commands from Juniper JUNOS network devices.
31version_added: 1.0.0
32options:
33  ncclient_device_handler:
34    type: str
35    default: junos
36    description:
37    - Specifies the ncclient device handler name for Juniper junos network os. To
38      identify the ncclient device handler name refer ncclient library documentation.
39"""
40
41import json
42import re
43
44from ansible.module_utils._text import to_text, to_native
45from ansible.module_utils.six import string_types
46from ansible.errors import AnsibleConnectionFailure
47from ansible.plugins.netconf import NetconfBase, ensure_ncclient
48
49try:
50    from ncclient import manager
51    from ncclient.operations import RPCError
52    from ncclient.transport.errors import SSHUnknownHostError
53    from ncclient.xml_ import to_ele, to_xml, new_ele, sub_ele
54
55    HAS_NCCLIENT = True
56except (
57    ImportError,
58    AttributeError,
59):  # paramiko and gssapi are incompatible and raise AttributeError not ImportError
60    HAS_NCCLIENT = False
61
62
63class Netconf(NetconfBase):
64    def get_text(self, ele, tag):
65        try:
66            return to_text(
67                ele.find(tag).text, errors="surrogate_then_replace"
68            ).strip()
69        except AttributeError:
70            pass
71
72    @ensure_ncclient
73    def get_device_info(self):
74        device_info = dict()
75        device_info["network_os"] = "junos"
76        ele = new_ele("get-software-information")
77        data = self.execute_rpc(to_xml(ele))
78        reply = to_ele(data)
79        sw_info = reply.find(".//software-information")
80
81        device_info["network_os_version"] = self.get_text(
82            sw_info, "junos-version"
83        )
84        device_info["network_os_hostname"] = self.get_text(
85            sw_info, "host-name"
86        )
87        device_info["network_os_model"] = self.get_text(
88            sw_info, "product-model"
89        )
90
91        return device_info
92
93    def execute_rpc(self, name):
94        """
95        RPC to be execute on remote device
96        :param name: Name of rpc in string format
97        :return: Received rpc response from remote host
98        """
99        return self.rpc(name)
100
101    @ensure_ncclient
102    def load_configuration(
103        self, format="xml", action="merge", target="candidate", config=None
104    ):
105        """
106        Load given configuration on device
107        :param format: Format of configuration (xml, text, set)
108        :param action: Action to be performed (merge, replace, override, update)
109        :param target: The name of the configuration datastore being edited
110        :param config: The configuration to be loaded on remote host in string format
111        :return: Received rpc response from remote host in string format
112        """
113        if config:
114            if format == "xml":
115                config = to_ele(config)
116
117        try:
118            return self.m.load_configuration(
119                format=format, action=action, target=target, config=config
120            ).data_xml
121        except RPCError as exc:
122            raise Exception(to_xml(exc.xml))
123
124    def get_capabilities(self):
125        result = dict()
126        result["rpc"] = self.get_base_rpc() + [
127            "commit",
128            "discard_changes",
129            "validate",
130            "lock",
131            "unlock",
132            "copy_copy",
133            "execute_rpc",
134            "load_configuration",
135            "get_configuration",
136            "command",
137            "reboot",
138            "halt",
139        ]
140        result["network_api"] = "netconf"
141        result["device_info"] = self.get_device_info()
142        result["server_capabilities"] = list(self.m.server_capabilities)
143        result["client_capabilities"] = list(self.m.client_capabilities)
144        result["session_id"] = self.m.session_id
145        result["device_operations"] = self.get_device_operations(
146            result["server_capabilities"]
147        )
148        return json.dumps(result)
149
150    @staticmethod
151    @ensure_ncclient
152    def guess_network_os(obj):
153        """
154        Guess the remote network os name
155        :param obj: Netconf connection class object
156        :return: Network OS name
157        """
158        try:
159            m = manager.connect(
160                host=obj._play_context.remote_addr,
161                port=obj._play_context.port or 830,
162                username=obj._play_context.remote_user,
163                password=obj._play_context.password,
164                key_filename=obj.key_filename,
165                hostkey_verify=obj.get_option("host_key_checking"),
166                look_for_keys=obj.get_option("look_for_keys"),
167                allow_agent=obj._play_context.allow_agent,
168                timeout=obj.get_option("persistent_connect_timeout"),
169                # We need to pass in the path to the ssh_config file when guessing
170                # the network_os so that a jumphost is correctly used if defined
171                ssh_config=obj._ssh_config,
172            )
173        except SSHUnknownHostError as exc:
174            raise AnsibleConnectionFailure(to_native(exc))
175
176        guessed_os = None
177        for c in m.server_capabilities:
178            if re.search("junos", c):
179                guessed_os = "junos"
180
181        m.close_session()
182        return guessed_os
183
184    def get_configuration(self, format="xml", filter=None):
185        """
186        Retrieve all or part of a specified configuration.
187        :param format: format in which configuration should be retrieved
188        :param filter: specifies the portion of the configuration to retrieve
189               as either xml string rooted in <configuration> element
190        :return: Received rpc response from remote host in string format
191        """
192        if filter is not None:
193            if not isinstance(filter, string_types):
194                raise AnsibleConnectionFailure(
195                    "get configuration filter should be of type string,"
196                    " received value '%s' is of type '%s'"
197                    % (filter, type(filter))
198                )
199            filter = to_ele(filter)
200
201        return self.m.get_configuration(format=format, filter=filter).data_xml
202
203    def compare_configuration(self, rollback=0):
204        """
205        Compare the candidate configuration with running configuration
206        by default. The candidate configuration can be compared with older
207        committed configuration by providing rollback id.
208        :param rollback: Rollback id of previously commited configuration
209        :return: Received rpc response from remote host in string format
210        """
211        return self.m.compare_configuration(rollback=rollback).data_xml
212
213    def halt(self):
214        """reboot the device"""
215        return self.m.halt().data_xml
216
217    def reboot(self):
218        """reboot the device"""
219        return self.m.reboot().data_xml
220
221    # Due to issue in ncclient commit() method for Juniper (https://github.com/ncclient/ncclient/issues/238)
222    # below commit() is a workaround which build's raw `commit-configuration` xml with required tags and uses
223    # ncclient generic rpc() method to execute rpc on remote host.
224    # Remove below method after the issue in ncclient is fixed.
225    @ensure_ncclient
226    def commit(
227        self,
228        confirmed=False,
229        check=False,
230        timeout=None,
231        comment=None,
232        synchronize=False,
233        at_time=None,
234    ):
235        """
236        Commit the candidate configuration as the device's new current configuration.
237        Depends on the `:candidate` capability.
238        A confirmed commit (i.e. if *confirmed* is `True`) is reverted if there is no
239        followup commit within the *timeout* interval. If no timeout is specified the
240        confirm timeout defaults to 600 seconds (10 minutes).
241        A confirming commit may have the *confirmed* parameter but this is not required.
242        Depends on the `:confirmed-commit` capability.
243        :param confirmed: whether this is a confirmed commit
244        :param check: Check correctness of syntax
245        :param timeout: specifies the confirm timeout in seconds
246        :param comment: Message to write to commit log
247        :param synchronize: Synchronize commit on remote peers
248        :param at_time: Time at which to activate configuration changes
249        :return: Received rpc response from remote host
250        """
251        obj = new_ele("commit-configuration")
252        if confirmed:
253            sub_ele(obj, "confirmed")
254        if check:
255            sub_ele(obj, "check")
256        if synchronize:
257            sub_ele(obj, "synchronize")
258        if at_time:
259            subele = sub_ele(obj, "at-time")
260            subele.text = str(at_time)
261        if comment:
262            subele = sub_ele(obj, "log")
263            subele.text = str(comment)
264        if timeout:
265            subele = sub_ele(obj, "confirm-timeout")
266            subele.text = str(timeout)
267        return self.rpc(obj)
268