1# -*- coding: utf-8 -*-
2#
3# (c) 2017, Ansible by Red Hat, inc
4#
5# This file is part of Ansible by Red Hat
6#
7# Ansible is free software: you can redistribute it and/or modify
8# it under the terms of the GNU General Public License as published by
9# the Free Software Foundation, either version 3 of the License, or
10# (at your option) any later version.
11#
12# Ansible is distributed in the hope that it will be useful,
13# but WITHOUT ANY WARRANTY; without even the implied warranty of
14# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15# GNU General Public License for more details.
16#
17# You should have received a copy of the GNU General Public License
18# along with Ansible.  If not, see <http://www.gnu.org/licenses/>.
19#
20from __future__ import (absolute_import, division, print_function)
21__metaclass__ = type
22
23import json
24
25from ansible.module_utils._text import to_text
26from ansible.module_utils.connection import Connection, ConnectionError
27from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, EntityCollection
28
29_DEVICE_CONFIGS = {}
30_CONNECTION = None
31
32_COMMAND_SPEC = {
33    'command': dict(key=True),
34    'prompt': dict(),
35    'answer': dict()
36}
37
38
39def get_connection(module):
40    global _CONNECTION
41    if _CONNECTION:
42        return _CONNECTION
43    _CONNECTION = Connection(module._socket_path)
44    return _CONNECTION
45
46
47def to_commands(module, commands):
48    if not isinstance(commands, list):
49        raise AssertionError('argument must be of type <list>')
50
51    transform = EntityCollection(module, _COMMAND_SPEC)
52    commands = transform(commands)
53    return commands
54
55
56def run_commands(module, commands, check_rc=True):
57    connection = get_connection(module)
58
59    commands = to_commands(module, to_list(commands))
60
61    responses = list()
62
63    for cmd in commands:
64        out = connection.get(**cmd)
65        responses.append(to_text(out, errors='surrogate_then_replace'))
66
67    return responses
68
69
70def get_config(module, source='running'):
71    conn = get_connection(module)
72    out = conn.get_config(source)
73    cfg = to_text(out, errors='surrogate_then_replace').strip()
74    return cfg
75
76
77def load_config(module, config):
78    try:
79        conn = get_connection(module)
80        conn.edit_config(config)
81    except ConnectionError as exc:
82        module.fail_json(msg=to_text(exc))
83
84
85def _parse_json_output(out):
86    out_list = out.split('\n')
87    first_index = 0
88    opening_char = None
89    lines_count = len(out_list)
90    while first_index < lines_count:
91        first_line = out_list[first_index].strip()
92        if not first_line or first_line[0] not in ("[", "{"):
93            first_index += 1
94            continue
95        opening_char = first_line[0]
96        break
97    if not opening_char:
98        return "null"
99    closing_char = ']' if opening_char == '[' else '}'
100    last_index = lines_count - 1
101    found = False
102    while last_index > first_index:
103        last_line = out_list[last_index].strip()
104        if not last_line or last_line[0] != closing_char:
105            last_index -= 1
106            continue
107        found = True
108        break
109    if not found:
110        return opening_char + closing_char
111    return "".join(out_list[first_index:last_index + 1])
112
113
114def show_cmd(module, cmd, json_fmt=True, fail_on_error=True):
115    if json_fmt:
116        cmd += " | json-print"
117    conn = get_connection(module)
118    command_obj = to_commands(module, to_list(cmd))[0]
119    try:
120        out = conn.get(**command_obj)
121    except ConnectionError:
122        if fail_on_error:
123            raise
124        return None
125    if json_fmt:
126        out = _parse_json_output(out)
127        try:
128            cfg = json.loads(out)
129        except ValueError:
130            module.fail_json(
131                msg="got invalid json",
132                stderr=to_text(out, errors='surrogate_then_replace'))
133    else:
134        cfg = to_text(out, errors='surrogate_then_replace').strip()
135    return cfg
136
137
138def get_interfaces_config(module, interface_type, flags=None, json_fmt=True):
139    cmd = "show interfaces %s" % interface_type
140    if flags:
141        cmd += " %s" % flags
142    return show_cmd(module, cmd, json_fmt)
143
144
145def get_bgp_summary(module):
146    cmd = "show running-config protocol bgp"
147    return show_cmd(module, cmd, json_fmt=False, fail_on_error=False)
148
149
150def get_capabilities(module):
151    """Returns platform info of the remove device
152    """
153    if hasattr(module, '_capabilities'):
154        return module._capabilities
155
156    connection = get_connection(module)
157    try:
158        capabilities = connection.get_capabilities()
159    except ConnectionError as exc:
160        module.fail_json(msg=to_text(exc, errors='surrogate_then_replace'))
161
162    module._capabilities = json.loads(capabilities)
163    return module._capabilities
164
165
166class BaseOnyxModule(object):
167    ONYX_API_VERSION = "3.6.6000"
168
169    def __init__(self):
170        self._module = None
171        self._commands = list()
172        self._current_config = None
173        self._required_config = None
174        self._os_version = None
175
176    def init_module(self):
177        pass
178
179    def load_current_config(self):
180        pass
181
182    def get_required_config(self):
183        pass
184
185    def _get_os_version(self):
186        capabilities = get_capabilities(self._module)
187        device_info = capabilities['device_info']
188        return device_info['network_os_version']
189
190    # pylint: disable=unused-argument
191    def check_declarative_intent_params(self, result):
192        return None
193
194    def _validate_key(self, param, key):
195        validator = getattr(self, 'validate_%s' % key)
196        if callable(validator):
197            validator(param.get(key))
198
199    def validate_param_values(self, obj, param=None):
200        if param is None:
201            param = self._module.params
202        for key in obj:
203            # validate the param value (if validator func exists)
204            try:
205                self._validate_key(param, key)
206            except AttributeError:
207                pass
208
209    @classmethod
210    def get_config_attr(cls, item, arg):
211        return item.get(arg)
212
213    @classmethod
214    def get_mtu(cls, item):
215        mtu = cls.get_config_attr(item, "MTU")
216        mtu_parts = mtu.split()
217        try:
218            return int(mtu_parts[0])
219        except ValueError:
220            return None
221
222    def _validate_range(self, attr_name, min_val, max_val, value):
223        if value is None:
224            return True
225        if not min_val <= int(value) <= max_val:
226            msg = '%s must be between %s and %s' % (
227                attr_name, min_val, max_val)
228            self._module.fail_json(msg=msg)
229
230    def validate_mtu(self, value):
231        self._validate_range('mtu', 1500, 9612, value)
232
233    def generate_commands(self):
234        pass
235
236    def run(self):
237        self.init_module()
238
239        result = {'changed': False}
240
241        self.get_required_config()
242        self.load_current_config()
243
244        self.generate_commands()
245        result['commands'] = self._commands
246
247        if self._commands:
248            if not self._module.check_mode:
249                load_config(self._module, self._commands)
250            result['changed'] = True
251
252        failed_conditions = self.check_declarative_intent_params(result)
253
254        if failed_conditions:
255            msg = 'One or more conditional statements have not been satisfied'
256            self._module.fail_json(msg=msg,
257                                   failed_conditions=failed_conditions)
258
259        self._module.exit_json(**result)
260
261    @classmethod
262    def main(cls):
263        app = cls()
264        app.run()
265