1#
2# -*- coding: utf-8 -*-
3# © Copyright 2020 Dell Inc. or its subsidiaries. All Rights Reserved
4# GNU General Public License v3.0+
5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6"""
7The sonic_interfaces class
8It is in this file where the current configuration (as dict)
9is compared to the provided configuration (as dict) and the command set
10necessary to bring the current configuration to it's desired end-state is
11created
12"""
13from __future__ import absolute_import, division, print_function
14__metaclass__ = type
15
16try:
17    from urllib import quote
18except ImportError:
19    from urllib.parse import quote
20
21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import (
22    ConfigBase,
23)
24from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
25    to_list,
26)
27from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.facts.facts import (
28    Facts,
29)
30from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.sonic import (
31    to_request,
32    edit_config
33)
34from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.interfaces_util import (
35    build_interfaces_create_request,
36)
37from ansible_collections.dellemc.enterprise_sonic.plugins.module_utils.network.sonic.utils.utils import (
38    get_diff,
39    update_states,
40    normalize_interface_name
41)
42from ansible.module_utils._text import to_native
43from ansible.module_utils.connection import ConnectionError
44import traceback
45
46LIB_IMP_ERR = None
47ERR_MSG = None
48try:
49    import requests
50    HAS_LIB = True
51except Exception as e:
52    HAS_LIB = False
53    ERR_MSG = to_native(e)
54    LIB_IMP_ERR = traceback.format_exc()
55
56PATCH = 'patch'
57DELETE = 'delete'
58
59
60class Interfaces(ConfigBase):
61    """
62    The sonic_interfaces class
63    """
64
65    gather_subset = [
66        '!all',
67        '!min',
68    ]
69
70    gather_network_resources = [
71        'interfaces',
72    ]
73
74    params = ('description', 'mtu', 'enabled')
75    delete_flag = False
76
77    def __init__(self, module):
78        super(Interfaces, self).__init__(module)
79
80    def get_interfaces_facts(self):
81        """ Get the 'facts' (the current configuration)
82
83        :rtype: A dictionary
84        :returns: The current configuration as a dictionary
85        """
86        facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources)
87        interfaces_facts = facts['ansible_network_resources'].get('interfaces')
88        if not interfaces_facts:
89            return []
90
91        return interfaces_facts
92
93    def execute_module(self):
94        """ Execute the module
95
96        :rtype: A dictionary
97        :returns: The result from module execution
98        """
99        result = {'changed': False}
100        warnings = list()
101
102        existing_interfaces_facts = self.get_interfaces_facts()
103        commands, requests = self.set_config(existing_interfaces_facts)
104        if commands and len(requests) > 0:
105            if not self._module.check_mode:
106                try:
107                    edit_config(self._module, to_request(self._module, requests))
108                except ConnectionError as exc:
109                    self._module.fail_json(msg=str(exc), code=exc.code)
110            result['changed'] = True
111        result['commands'] = commands
112
113        changed_interfaces_facts = self.get_interfaces_facts()
114
115        result['before'] = existing_interfaces_facts
116        if result['changed']:
117            result['after'] = changed_interfaces_facts
118
119        result['warnings'] = warnings
120        return result
121
122    def set_config(self, existing_interfaces_facts):
123        """ Collect the configuration from the args passed to the module,
124            collect the current configuration (as a dict from facts)
125
126        :rtype: A list
127        :returns: the commands necessary to migrate the current configuration
128                  to the desired configuration
129        """
130        want = self._module.params['config']
131        normalize_interface_name(want, self._module)
132        have = existing_interfaces_facts
133
134        resp = self.set_state(want, have)
135        return to_list(resp)
136
137    def set_state(self, want, have):
138        """ Select the appropriate function based on the state provided
139
140        :param want: the desired configuration as a dictionary
141        :param have: the current configuration as a dictionary
142        :rtype: A list
143        :returns: the commands necessary to migrate the current configuration
144                  to the desired configuration
145        """
146        state = self._module.params['state']
147        # diff method works on dict, so creating temp dict
148        diff = get_diff(want, have)
149        # removing the dict in case diff found
150
151        if state == 'overridden':
152            have = [each_intf for each_intf in have if each_intf['name'].startswith('Ethernet')]
153            commands, requests = self._state_overridden(want, have, diff)
154        elif state == 'deleted':
155            commands, requests = self._state_deleted(want, have, diff)
156        elif state == 'merged':
157            commands, requests = self._state_merged(want, have, diff)
158        elif state == 'replaced':
159            commands, requests = self._state_replaced(want, have, diff)
160
161        return commands, requests
162
163    def _state_replaced(self, want, have, diff):
164        """ The command generator when state is replaced
165
166        :param want: the desired configuration as a dictionary
167        :param have: the current configuration as a dictionary
168        :param interface_type: interface type
169        :rtype: A list
170        :returns: the commands necessary to migrate the current configuration
171                  to the desired configuration
172        """
173        commands = self.filter_comands_to_change(diff, have)
174        requests = self.get_delete_interface_requests(commands, have)
175        requests.extend(self.get_modify_interface_requests(commands, have))
176        if commands and len(requests) > 0:
177            commands = update_states(commands, "replaced")
178        else:
179            commands = []
180
181        return commands, requests
182
183    def _state_overridden(self, want, have, diff):
184        """ The command generator when state is overridden
185
186        :param want: the desired configuration as a dictionary
187        :param obj_in_have: the current configuration as a dictionary
188        :rtype: A list
189        :returns: the commands necessary to migrate the current configuration
190                  to the desired configuration
191        """
192        commands = []
193        commands_del = self.filter_comands_to_change(want, have)
194        requests = self.get_delete_interface_requests(commands_del, have)
195        del_req_count = len(requests)
196        if commands_del and del_req_count > 0:
197            commands_del = update_states(commands_del, "deleted")
198            commands.extend(commands_del)
199
200        commands_over = diff
201        requests.extend(self.get_modify_interface_requests(commands_over, have))
202        if commands_over and len(requests) > del_req_count:
203            commands_over = update_states(commands_over, "overridden")
204            commands.extend(commands_over)
205
206        return commands, requests
207
208    def _state_merged(self, want, have, diff):
209        """ The command generator when state is merged
210
211        :param want: the additive configuration as a dictionary
212        :param obj_in_have: the current configuration as a dictionary
213        :rtype: A list
214        :returns: the commands necessary to merge the provided into
215                  the current configuration
216        """
217        commands = diff
218        requests = self.get_modify_interface_requests(commands, have)
219        if commands and len(requests) > 0:
220            commands = update_states(commands, "merged")
221        else:
222            commands = []
223
224        return commands, requests
225
226    def _state_deleted(self, want, have, diff):
227        """ The command generator when state is deleted
228
229        :param want: the objects from which the configuration should be removed
230        :param obj_in_have: the current configuration as a dictionary
231        :param interface_type: interface type
232        :rtype: A list
233        :returns: the commands necessary to remove the current configuration
234                  of the provided objects
235        """
236        # if want is none, then delete all the interfaces
237        if not want:
238            commands = have
239        else:
240            commands = want
241
242        requests = self.get_delete_interface_requests(commands, have)
243
244        if commands and len(requests) > 0:
245            commands = update_states(commands, "deleted")
246        else:
247            commands = []
248
249        return commands, requests
250
251    def filter_comands_to_delete(self, configs, have):
252        commands = []
253
254        for conf in configs:
255            if self.is_this_delete_required(conf, have):
256                temp_conf = dict()
257                temp_conf['name'] = conf['name']
258                temp_conf['description'] = ''
259                temp_conf['mtu'] = 9100
260                temp_conf['enabled'] = True
261                commands.append(temp_conf)
262        return commands
263
264    def filter_comands_to_change(self, configs, have):
265        commands = []
266        if configs:
267            for conf in configs:
268                if self.is_this_change_required(conf, have):
269                    commands.append(conf)
270        return commands
271
272    def get_modify_interface_requests(self, configs, have):
273        self.delete_flag = False
274        commands = self.filter_comands_to_change(configs, have)
275
276        return self.get_interface_requests(commands, have)
277
278    def get_delete_interface_requests(self, configs, have):
279        self.delete_flag = True
280        commands = self.filter_comands_to_delete(configs, have)
281
282        return self.get_interface_requests(commands, have)
283
284    def get_interface_requests(self, configs, have):
285        requests = []
286        if not configs:
287            return requests
288
289        # Create URL and payload
290        for conf in configs:
291            name = conf["name"]
292            if self.delete_flag and name.startswith('Loopback'):
293                method = DELETE
294                url = 'data/openconfig-interfaces:interfaces/interface=%s' % quote(name, safe='')
295                request = {"path": url, "method": method}
296            else:
297                # Create Loopback in case not availble in have
298                if name.startswith('Loopback'):
299                    have_conf = next((cfg for cfg in have if cfg['name'] == name), None)
300                    if not have_conf:
301                        loopback_create_request = build_interfaces_create_request(name)
302                        requests.append(loopback_create_request)
303                method = PATCH
304                url = 'data/openconfig-interfaces:interfaces/interface=%s/config' % quote(name, safe='')
305                payload = self.build_create_payload(conf)
306                request = {"path": url, "method": method, "data": payload}
307            requests.append(request)
308
309        return requests
310
311    def is_this_delete_required(self, conf, have):
312        if conf['name'] == "eth0":
313            return False
314        intf = next((e_intf for e_intf in have if conf['name'] == e_intf['name']), None)
315        if intf:
316            if (intf['name'].startswith('Loopback') or not ((intf.get('description') is None or intf.get('description') == '') and
317               (intf.get('enabled') is None or intf.get('enabled') is True) and (intf.get('mtu') is None or intf.get('mtu') == 9100))):
318                return True
319        return False
320
321    def is_this_change_required(self, conf, have):
322        if conf['name'] == "eth0":
323            return False
324        ret_flag = False
325        intf = next((e_intf for e_intf in have if conf['name'] == e_intf['name']), None)
326        if intf:
327            # Check all parameter if any one is differen from existing
328            for param in self.params:
329                if conf.get(param) is not None and conf.get(param) != intf.get(param):
330                    ret_flag = True
331                    break
332        # if given interface is not present
333        else:
334            ret_flag = True
335
336        return ret_flag
337
338    def build_create_payload(self, conf):
339        temp_conf = dict()
340        temp_conf['name'] = conf['name']
341
342        if not temp_conf['name'].startswith('Loopback'):
343            if conf.get('enabled') is not None:
344                if conf.get('enabled'):
345                    temp_conf['enabled'] = True
346                else:
347                    temp_conf['enabled'] = False
348            if conf.get('description') is not None:
349                temp_conf['description'] = conf['description']
350            if conf.get('mtu') is not None:
351                temp_conf['mtu'] = conf['mtu']
352
353        payload = {'openconfig-interfaces:config': temp_conf}
354        return payload
355