1#
2# This code is part of Ansible, but is an independent component.
3#
4# This particular file snippet, and this file snippet only, is BSD licensed.
5# Modules you write using this snippet, which is embedded dynamically by Ansible
6# still belong to the author of the module, and may assign their own license
7# to the complete work.
8#
9# (c) 2017 Red Hat, Inc.
10#
11# Redistribution and use in source and binary forms, with or without modification,
12# are permitted provided that the following conditions are met:
13#
14#    * Redistributions of source code must retain the above copyright
15#      notice, this list of conditions and the following disclaimer.
16#    * Redistributions in binary form must reproduce the above copyright notice,
17#      this list of conditions and the following disclaimer in the documentation
18#      and/or other materials provided with the distribution.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
21# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
22# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
23# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
24# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
25# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
27# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
28# USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29#
30
31import re
32import socket
33import sys
34import traceback
35
36from ansible.module_utils.basic import env_fallback
37from ansible.module_utils.network.common.utils import to_list, ComplexList
38from ansible.module_utils.connection import exec_command, ConnectionError
39from ansible.module_utils.six import iteritems
40from ansible.module_utils._text import to_native
41from ansible.module_utils.network.common.netconf import NetconfConnection
42
43
44try:
45    from ncclient.xml_ import to_xml, new_ele_ns
46    HAS_NCCLIENT = True
47except ImportError:
48    HAS_NCCLIENT = False
49
50
51try:
52    from lxml import etree
53except ImportError:
54    from xml.etree import ElementTree as etree
55
56_DEVICE_CLI_CONNECTION = None
57_DEVICE_NC_CONNECTION = None
58
59ce_provider_spec = {
60    'host': dict(),
61    'port': dict(type='int'),
62    'username': dict(fallback=(env_fallback, ['ANSIBLE_NET_USERNAME'])),
63    'password': dict(fallback=(env_fallback, ['ANSIBLE_NET_PASSWORD']), no_log=True),
64    'ssh_keyfile': dict(fallback=(env_fallback, ['ANSIBLE_NET_SSH_KEYFILE']), type='path'),
65    'use_ssl': dict(type='bool'),
66    'validate_certs': dict(type='bool'),
67    'timeout': dict(type='int'),
68    'transport': dict(default='cli', choices=['cli', 'netconf']),
69}
70ce_argument_spec = {
71    'provider': dict(type='dict', options=ce_provider_spec),
72}
73ce_top_spec = {
74    'host': dict(removed_in_version=2.9),
75    'port': dict(removed_in_version=2.9, type='int'),
76    'username': dict(removed_in_version=2.9),
77    'password': dict(removed_in_version=2.9, no_log=True),
78    'ssh_keyfile': dict(removed_in_version=2.9, type='path'),
79    'use_ssl': dict(removed_in_version=2.9, type='bool'),
80    'validate_certs': dict(removed_in_version=2.9, type='bool'),
81    'timeout': dict(removed_in_version=2.9, type='int'),
82    'transport': dict(removed_in_version=2.9, choices=['cli', 'netconf']),
83}
84ce_argument_spec.update(ce_top_spec)
85
86
87def to_string(data):
88    return re.sub(r'<data\s+.+?(/>|>)', r'<data\1', data)
89
90
91def check_args(module, warnings):
92    pass
93
94
95def load_params(module):
96    """load_params"""
97    provider = module.params.get('provider') or dict()
98    for key, value in iteritems(provider):
99        if key in ce_argument_spec:
100            if module.params.get(key) is None and value is not None:
101                module.params[key] = value
102
103
104def get_connection(module):
105    """get_connection"""
106    global _DEVICE_CLI_CONNECTION
107    if not _DEVICE_CLI_CONNECTION:
108        load_params(module)
109        conn = Cli(module)
110        _DEVICE_CLI_CONNECTION = conn
111    return _DEVICE_CLI_CONNECTION
112
113
114def rm_config_prefix(cfg):
115    if not cfg:
116        return cfg
117
118    cmds = cfg.split("\n")
119    for i in range(len(cmds)):
120        if not cmds[i]:
121            continue
122        if '~' in cmds[i]:
123            index = cmds[i].index('~')
124            if cmds[i][:index] == ' ' * index:
125                cmds[i] = cmds[i].replace("~", "", 1)
126    return '\n'.join(cmds)
127
128
129class Cli:
130
131    def __init__(self, module):
132        self._module = module
133        self._device_configs = {}
134
135    def exec_command(self, command):
136        if isinstance(command, dict):
137            command = self._module.jsonify(command)
138
139        return exec_command(self._module, command)
140
141    def get_config(self, flags=None):
142        """Retrieves the current config from the device or cache
143        """
144        flags = [] if flags is None else flags
145
146        cmd = 'display current-configuration '
147        cmd += ' '.join(flags)
148        cmd = cmd.strip()
149
150        try:
151            return self._device_configs[cmd]
152        except KeyError:
153            rc, out, err = self.exec_command(cmd)
154            if rc != 0:
155                self._module.fail_json(msg=err)
156            cfg = str(out).strip()
157            # remove default configuration prefix '~'
158            for flag in flags:
159                if "include-default" in flag:
160                    cfg = rm_config_prefix(cfg)
161                    break
162
163            self._device_configs[cmd] = cfg
164            return cfg
165
166    def run_commands(self, commands, check_rc=True):
167        """Run list of commands on remote device and return results
168        """
169        responses = list()
170
171        for item in to_list(commands):
172
173            rc, out, err = self.exec_command(item)
174
175            if check_rc and rc != 0:
176                self._module.fail_json(msg=cli_err_msg(item['command'].strip(), err))
177
178            try:
179                out = self._module.from_json(out)
180            except ValueError:
181                out = str(out).strip()
182
183            responses.append(out)
184        return responses
185
186    def load_config(self, config):
187        """Sends configuration commands to the remote device
188        """
189        rc, out, err = self.exec_command('mmi-mode enable')
190        if rc != 0:
191            self._module.fail_json(msg='unable to set mmi-mode enable', output=err)
192        rc, out, err = self.exec_command('system-view immediately')
193        if rc != 0:
194            self._module.fail_json(msg='unable to enter system-view', output=err)
195
196        for cmd in config:
197            rc, out, err = self.exec_command(cmd)
198            if rc != 0:
199                self._module.fail_json(msg=cli_err_msg(cmd.strip(), err))
200
201        self.exec_command('return')
202
203
204def cli_err_msg(cmd, err):
205    """ get cli exception message"""
206
207    if not err:
208        return "Error: Fail to get cli exception message."
209
210    msg = list()
211    err_list = str(err).split("\r\n")
212    for err in err_list:
213        err = err.strip('.,\r\n\t ')
214        if not err:
215            continue
216        if cmd and cmd == err:
217            continue
218        if " at '^' position" in err:
219            err = err.replace(" at '^' position", "").strip()
220        err = err.strip('.,\r\n\t ')
221        if err == "^":
222            continue
223        if len(err) > 2 and err[0] in ["<", "["] and err[-1] in [">", "]"]:
224            continue
225        err.strip('.,\r\n\t ')
226        if err:
227            msg.append(err)
228
229    if cmd:
230        msg.insert(0, "Command: %s" % cmd)
231
232    return ", ".join(msg).capitalize() + "."
233
234
235def to_command(module, commands):
236    default_output = 'text'
237    transform = ComplexList(dict(
238        command=dict(key=True),
239        output=dict(default=default_output),
240        prompt=dict(),
241        answer=dict()
242    ), module)
243
244    commands = transform(to_list(commands))
245
246    return commands
247
248
249def get_config(module, flags=None):
250    flags = [] if flags is None else flags
251
252    conn = get_connection(module)
253    return conn.get_config(flags)
254
255
256def run_commands(module, commands, check_rc=True):
257    conn = get_connection(module)
258    return conn.run_commands(to_command(module, commands), check_rc)
259
260
261def load_config(module, config):
262    """load_config"""
263    conn = get_connection(module)
264    return conn.load_config(config)
265
266
267def ce_unknown_host_cb(host, fingerprint):
268    """ ce_unknown_host_cb """
269
270    return True
271
272
273def get_nc_set_id(xml_str):
274    """get netconf set-id value"""
275
276    result = re.findall(r'<rpc-reply.+?set-id=\"(\d+)\"', xml_str)
277    if not result:
278        return None
279    return result[0]
280
281
282def get_xml_line(xml_list, index):
283    """get xml specified line valid string data"""
284
285    ele = None
286    while xml_list and not ele:
287        if index >= 0 and index >= len(xml_list):
288            return None
289        if index < 0 and abs(index) > len(xml_list):
290            return None
291
292        ele = xml_list[index]
293        if not ele.replace(" ", ""):
294            xml_list.pop(index)
295            ele = None
296    return ele
297
298
299def merge_nc_xml(xml1, xml2):
300    """merge xml1 and xml2"""
301
302    xml1_list = xml1.split("</data>")[0].split("\n")
303    xml2_list = xml2.split("<data>")[1].split("\n")
304
305    while True:
306        xml1_ele1 = get_xml_line(xml1_list, -1)
307        xml1_ele2 = get_xml_line(xml1_list, -2)
308        xml2_ele1 = get_xml_line(xml2_list, 0)
309        xml2_ele2 = get_xml_line(xml2_list, 1)
310        if not xml1_ele1 or not xml1_ele2 or not xml2_ele1 or not xml2_ele2:
311            return xml1
312
313        if "xmlns" in xml2_ele1:
314            xml2_ele1 = xml2_ele1.lstrip().split(" ")[0] + ">"
315        if "xmlns" in xml2_ele2:
316            xml2_ele2 = xml2_ele2.lstrip().split(" ")[0] + ">"
317        if xml1_ele1.replace(" ", "").replace("/", "") == xml2_ele1.replace(" ", "").replace("/", ""):
318            if xml1_ele2.replace(" ", "").replace("/", "") == xml2_ele2.replace(" ", "").replace("/", ""):
319                xml1_list.pop()
320                xml2_list.pop(0)
321            else:
322                break
323        else:
324            break
325
326    return "\n".join(xml1_list + xml2_list)
327
328
329def get_nc_connection(module):
330    global _DEVICE_NC_CONNECTION
331    if not _DEVICE_NC_CONNECTION:
332        load_params(module)
333        conn = NetconfConnection(module._socket_path)
334        _DEVICE_NC_CONNECTION = conn
335    return _DEVICE_NC_CONNECTION
336
337
338def set_nc_config(module, xml_str):
339    """ set_config """
340
341    conn = get_nc_connection(module)
342    try:
343        out = conn.edit_config(target='running', config=xml_str, default_operation='merge',
344                               error_option='rollback-on-error')
345    finally:
346        # conn.unlock(target = 'candidate')
347        pass
348    return to_string(to_xml(out))
349
350
351def get_nc_next(module, xml_str):
352    """ get_nc_next for exchange capability """
353
354    conn = get_nc_connection(module)
355    result = None
356    if xml_str is not None:
357        response = conn.get(xml_str, if_rpc_reply=True)
358        result = response.find('./*')
359        set_id = response.get('set-id')
360        while True and set_id is not None:
361            try:
362                fetch_node = new_ele_ns('get-next', 'http://www.huawei.com/netconf/capability/base/1.0', {'set-id': set_id})
363                next_xml = conn.dispatch_rpc(etree.tostring(fetch_node))
364                if next_xml is not None:
365                    result.extend(next_xml.find('./*'))
366                set_id = next_xml.get('set-id')
367            except ConnectionError:
368                break
369    if result is not None:
370        return to_string(to_xml(result))
371    return result
372
373
374def get_nc_config(module, xml_str):
375    """ get_config """
376
377    conn = get_nc_connection(module)
378    if xml_str is not None:
379        response = conn.get(xml_str)
380    else:
381        return None
382
383    return to_string(to_xml(response))
384
385
386def execute_nc_action(module, xml_str):
387    """ huawei execute-action """
388
389    conn = get_nc_connection(module)
390    response = conn.execute_action(xml_str)
391    return to_string(to_xml(response))
392
393
394def execute_nc_cli(module, xml_str):
395    """ huawei execute-cli """
396
397    if xml_str is not None:
398        try:
399            conn = get_nc_connection(module)
400            out = conn.execute_nc_cli(command=xml_str)
401            return to_string(to_xml(out))
402        except Exception as exc:
403            raise Exception(exc)
404
405
406def check_ip_addr(ipaddr):
407    """ check ip address, Supports IPv4 and IPv6 """
408
409    if not ipaddr or '\x00' in ipaddr:
410        return False
411
412    try:
413        res = socket.getaddrinfo(ipaddr, 0, socket.AF_UNSPEC,
414                                 socket.SOCK_STREAM,
415                                 0, socket.AI_NUMERICHOST)
416        return bool(res)
417    except socket.gaierror:
418        err = sys.exc_info()[1]
419        if err.args[0] == socket.EAI_NONAME:
420            return False
421        raise
422