1# -*- coding: utf-8 -*-
2# Copyright 2019 Red Hat Inc.
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4"""
5The iosxr_l3_interfaces class
6It is in this file where the current configuration (as dict)
7is compared to the provided configuration (as dict) and the command set
8necessary to bring the current configuration to it's desired end-state is
9created
10"""
11
12from __future__ import absolute_import, division, print_function
13__metaclass__ = type
14
15
16from ansible.module_utils.network.common.cfg.base import ConfigBase
17from ansible.module_utils.network.common.utils import to_list
18from ansible.module_utils.network.iosxr.facts.facts import Facts
19from ansible.module_utils.network.iosxr.utils.utils import normalize_interface, dict_to_set
20from ansible.module_utils.network.iosxr.utils.utils import remove_command_from_config_list, add_command_to_config_list
21from ansible.module_utils.network.iosxr.utils.utils import filter_dict_having_none_value, remove_duplicate_interface
22from ansible.module_utils.network.iosxr.utils.utils import validate_n_expand_ipv4, validate_ipv6
23
24
25class L3_Interfaces(ConfigBase):
26    """
27    The iosxr_l3_interfaces class
28    """
29
30    gather_subset = [
31        '!all',
32        '!min',
33    ]
34
35    gather_network_resources = [
36        'l3_interfaces',
37    ]
38
39    def get_l3_interfaces_facts(self):
40        """ Get the 'facts' (the current configuration)
41        :rtype: A dictionary
42        :returns: The current configuration as a dictionary
43        """
44        facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources)
45        l3_interfaces_facts = facts['ansible_network_resources'].get('l3_interfaces')
46        if not l3_interfaces_facts:
47            return []
48
49        return l3_interfaces_facts
50
51    def execute_module(self):
52        """ Execute the module
53        :rtype: A dictionary
54        :returns: The result from module execution
55        """
56        result = {'changed': False}
57        commands = list()
58        warnings = list()
59
60        existing_l3_interfaces_facts = self.get_l3_interfaces_facts()
61        commands.extend(self.set_config(existing_l3_interfaces_facts))
62        if commands:
63            if not self._module.check_mode:
64                self._connection.edit_config(commands)
65            result['changed'] = True
66        result['commands'] = commands
67
68        changed_l3_interfaces_facts = self.get_l3_interfaces_facts()
69
70        result['before'] = existing_l3_interfaces_facts
71        if result['changed']:
72            result['after'] = changed_l3_interfaces_facts
73
74        result['warnings'] = warnings
75        return result
76
77    def set_config(self, existing_l3_interfaces_facts):
78        """ Collect the configuration from the args passed to the module,
79            collect the current configuration (as a dict from facts)
80        :rtype: A list
81        :returns: the commands necessary to migrate the current configuration
82                  to the desired configuration
83        """
84        want = self._module.params['config']
85        have = existing_l3_interfaces_facts
86        resp = self.set_state(want, have)
87        return to_list(resp)
88
89    def set_state(self, want, have):
90        """ Select the appropriate function based on the state provided
91        :param want: the desired configuration as a dictionary
92        :param have: the current configuration as a dictionary
93        :rtype: A list
94        :returns: the commands necessary to migrate the current configuration
95                  to the desired configuration
96        """
97        commands = []
98
99        state = self._module.params['state']
100
101        if state in ('overridden', 'merged', 'replaced') and not want:
102            self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(state))
103
104        if state == 'overridden':
105            commands = self._state_overridden(want, have, self._module)
106        elif state == 'deleted':
107            commands = self._state_deleted(want, have)
108        elif state == 'merged':
109            commands = self._state_merged(want, have, self._module)
110        elif state == 'replaced':
111            commands = self._state_replaced(want, have, self._module)
112
113        return commands
114
115    def _state_replaced(self, want, have, module):
116        """ The command generator when state is replaced
117        :rtype: A list
118        :returns: the commands necessary to migrate the current configuration
119                  to the desired configuration
120        """
121        commands = []
122
123        for interface in want:
124            interface['name'] = normalize_interface(interface['name'])
125            for each in have:
126                if each['name'] == interface['name']:
127                    break
128            else:
129                commands.extend(self._set_config(interface, dict(), module))
130                continue
131            have_dict = filter_dict_having_none_value(interface, each)
132            commands.extend(self._clear_config(dict(), have_dict))
133            commands.extend(self._set_config(interface, each, module))
134        # Remove the duplicate interface call
135        commands = remove_duplicate_interface(commands)
136
137        return commands
138
139    def _state_overridden(self, want, have, module):
140        """ The command generator when state is overridden
141        :rtype: A list
142        :returns: the commands necessary to migrate the current configuration
143                  to the desired configuration
144        """
145        commands = []
146        not_in_have = set()
147        in_have = set()
148
149        for each in have:
150            for interface in want:
151                interface['name'] = normalize_interface(interface['name'])
152                if each['name'] == interface['name']:
153                    in_have.add(interface['name'])
154                    break
155                elif interface['name'] != each['name']:
156                    not_in_have.add(interface['name'])
157            else:
158                # We didn't find a matching desired state, which means we can
159                # pretend we recieved an empty desired state.
160                interface = dict(name=each['name'])
161                kwargs = {'want': interface, 'have': each}
162                commands.extend(self._clear_config(**kwargs))
163                continue
164            have_dict = filter_dict_having_none_value(interface, each)
165            commands.extend(self._clear_config(dict(), have_dict))
166            commands.extend(self._set_config(interface, each, module))
167        # Add the want interface that's not already configured in have interface
168        for each in (not_in_have - in_have):
169            for every in want:
170                interface = 'interface {0}'.format(every['name'])
171                if each and interface not in commands:
172                    commands.extend(self._set_config(every, {}, module))
173        # Remove the duplicate interface call
174        commands = remove_duplicate_interface(commands)
175
176        return commands
177
178    def _state_merged(self, want, have, module):
179        """ The command generator when state is merged
180        :rtype: A list
181        :returns: the commands necessary to merge the provided into
182                  the current configuration
183        """
184        commands = []
185
186        for interface in want:
187            interface['name'] = normalize_interface(interface['name'])
188            for each in have:
189                if each['name'] == interface['name']:
190                    break
191            else:
192                commands.extend(self._set_config(interface, dict(), module))
193                continue
194            commands.extend(self._set_config(interface, each, module))
195
196        return commands
197
198    def _state_deleted(self, want, have):
199        """ The command generator when state is deleted
200        :rtype: A list
201        :returns: the commands necessary to remove the current configuration
202                  of the provided objects
203        """
204        commands = []
205
206        if want:
207            for interface in want:
208                interface['name'] = normalize_interface(interface['name'])
209                for each in have:
210                    if each['name'] == interface['name']:
211                        break
212                    elif interface['name'] in each['name']:
213                        break
214                else:
215                    continue
216                interface = dict(name=interface['name'])
217                commands.extend(self._clear_config(interface, each))
218        else:
219            for each in have:
220                want = dict()
221                commands.extend(self._clear_config(want, each))
222
223        return commands
224
225    def verify_diff_again(self, want, have):
226        """
227        Verify the IPV4 difference again as sometimes due to
228        change in order of set, set difference may result into change,
229        when there's actually no difference between want and have
230        :param want: want_dict IPV4
231        :param have: have_dict IPV4
232        :return: diff
233        """
234        diff = False
235        for each in want:
236            each_want = dict(each)
237            for every in have:
238                every_have = dict(every)
239                if each_want.get('address') != every_have.get('address') and \
240                        each_want.get('secondary') != every_have.get('secondary') and \
241                        len(each_want.keys()) == len(every_have.keys()):
242                    diff = True
243                    break
244                elif each_want.get('address') != every_have.get('address') and len(each_want.keys()) == len(
245                        every_have.keys()):
246                    diff = True
247                    break
248            if diff:
249                break
250
251        return diff
252
253    def _set_config(self, want, have, module):
254        # Set the interface config based on the want and have config
255        commands = []
256        interface = 'interface ' + want['name']
257
258        # To handle L3 IPV4 configuration
259        if want.get("ipv4"):
260            for each in want.get("ipv4"):
261                if each.get('address') != 'dhcp':
262                    ip_addr_want = validate_n_expand_ipv4(module, each)
263                    each['address'] = ip_addr_want
264
265        # Get the diff b/w want and have
266        want_dict = dict_to_set(want)
267        have_dict = dict_to_set(have)
268
269        # To handle L3 IPV4 configuration
270        want_ipv4 = dict(want_dict).get('ipv4')
271        have_ipv4 = dict(have_dict).get('ipv4')
272        if want_ipv4:
273            if have_ipv4:
274                diff_ipv4 = set(want_ipv4) - set(dict(have_dict).get('ipv4'))
275                if diff_ipv4:
276                    diff_ipv4 = diff_ipv4 if self.verify_diff_again(want_ipv4, have_ipv4) else ()
277            else:
278                diff_ipv4 = set(want_ipv4)
279            for each in diff_ipv4:
280                ipv4_dict = dict(each)
281                if ipv4_dict.get('address') != 'dhcp':
282                    cmd = "ipv4 address {0}".format(ipv4_dict['address'])
283                    if ipv4_dict.get("secondary"):
284                        cmd += " secondary"
285                add_command_to_config_list(interface, cmd, commands)
286
287        # To handle L3 IPV6 configuration
288        want_ipv6 = dict(want_dict).get('ipv6')
289        have_ipv6 = dict(have_dict).get('ipv6')
290        if want_ipv6:
291            if have_ipv6:
292                diff_ipv6 = set(want_ipv6) - set(have_ipv6)
293            else:
294                diff_ipv6 = set(want_ipv6)
295            for each in diff_ipv6:
296                ipv6_dict = dict(each)
297                validate_ipv6(ipv6_dict.get('address'), module)
298                cmd = "ipv6 address {0}".format(ipv6_dict.get('address'))
299                add_command_to_config_list(interface, cmd, commands)
300
301        return commands
302
303    def _clear_config(self, want, have):
304        # Delete the interface config based on the want and have config
305        count = 0
306        commands = []
307        if want.get('name'):
308            interface = 'interface ' + want['name']
309        else:
310            interface = 'interface ' + have['name']
311
312        if have.get('ipv4') and want.get('ipv4'):
313            for each in have.get('ipv4'):
314                if each.get('secondary') and not (want.get('ipv4')[count].get('secondary')):
315                    cmd = 'ipv4 address {0} secondary'.format(each.get('address'))
316                    remove_command_from_config_list(interface, cmd, commands)
317                count += 1
318        if have.get('ipv4') and not (want.get('ipv4')):
319            remove_command_from_config_list(interface, 'ipv4 address', commands)
320        if have.get('ipv6') and not (want.get('ipv6')):
321            remove_command_from_config_list(interface, 'ipv6 address', commands)
322
323        return commands
324