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_l2_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
22
23
24class L2_Interfaces(ConfigBase):
25    """
26    The iosxr_interfaces class
27    """
28
29    gather_subset = [
30        '!all',
31        '!min',
32    ]
33
34    gather_network_resources = [
35        'l2_interfaces',
36    ]
37
38    def get_l2_interfaces_facts(self):
39        """ Get the 'facts' (the current configuration)
40        :rtype: A dictionary
41        :returns: The current configuration as a dictionary
42        """
43        facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources)
44        l2_interfaces_facts = facts['ansible_network_resources'].get('l2_interfaces')
45
46        if not l2_interfaces_facts:
47            return []
48        return l2_interfaces_facts
49
50    def execute_module(self):
51        """ Execute the module
52        :rtype: A dictionary
53        :returns: The result from module execution
54        """
55        result = {'changed': False}
56        commands = list()
57        warnings = list()
58
59        existing_l2_interfaces_facts = self.get_l2_interfaces_facts()
60        commands.extend(self.set_config(existing_l2_interfaces_facts))
61        if commands:
62            if not self._module.check_mode:
63                self._connection.edit_config(commands)
64            result['changed'] = True
65        result['commands'] = commands
66
67        changed_l2_interfaces_facts = self.get_l2_interfaces_facts()
68
69        result['before'] = existing_l2_interfaces_facts
70        if result['changed']:
71            result['after'] = changed_l2_interfaces_facts
72
73        result['warnings'] = warnings
74        return result
75
76    def set_config(self, existing_l2_interfaces_facts):
77        """ Collect the configuration from the args passed to the module,
78            collect the current configuration (as a dict from facts)
79        :rtype: A list
80        :returns: the commands necessary to migrate the current configuration
81                  to the desired configuration
82        """
83        want = self._module.params['config']
84        have = existing_l2_interfaces_facts
85        resp = self.set_state(want, have)
86        return to_list(resp)
87
88    def set_state(self, want, have):
89        """ Select the appropriate function based on the state provided
90        :param want: the desired configuration as a dictionary
91        :param have: the current configuration as a dictionary
92        :rtype: A list
93        :returns: the commands necessary to migrate the current configuration
94                  to the desired configuration
95        """
96        commands = []
97
98        state = self._module.params['state']
99
100        if state in ('overridden', 'merged', 'replaced') and not want:
101            self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(state))
102
103        if state == 'overridden':
104            commands = self._state_overridden(want, have, self._module)
105        elif state == 'deleted':
106            commands = self._state_deleted(want, have)
107        elif state == 'merged':
108            commands = self._state_merged(want, have, self._module)
109        elif state == 'replaced':
110            commands = self._state_replaced(want, have, self._module)
111
112        return commands
113
114    def _state_replaced(self, want, have, module):
115        """ The command generator when state is replaced
116        :rtype: A list
117        :returns: the commands necessary to migrate the current configuration
118                  to the desired configuration
119        """
120        commands = []
121
122        for interface in want:
123            interface['name'] = normalize_interface(interface['name'])
124            for each in have:
125                if each['name'] == interface['name']:
126                    break
127            else:
128                commands.extend(self._set_config(interface, {}, module))
129                continue
130            have_dict = filter_dict_having_none_value(interface, each)
131            commands.extend(self._clear_config(dict(), have_dict))
132            commands.extend(self._set_config(interface, each, module))
133        # Remove the duplicate interface call
134        commands = remove_duplicate_interface(commands)
135
136        return commands
137
138    def _state_overridden(self, want, have, module):
139        """ The command generator when state is overridden
140        :rtype: A list
141        :returns: the commands necessary to migrate the current configuration
142                  to the desired configuration
143        """
144        commands = []
145        not_in_have = set()
146        in_have = set()
147        for each in have:
148            for interface in want:
149                interface['name'] = normalize_interface(interface['name'])
150                if each['name'] == interface['name']:
151                    in_have.add(interface['name'])
152                    break
153                elif interface['name'] != each['name']:
154                    not_in_have.add(interface['name'])
155            else:
156                # We didn't find a matching desired state, which means we can
157                # pretend we recieved an empty desired state.
158                interface = dict(name=each['name'])
159                commands.extend(self._clear_config(interface, each))
160                continue
161            have_dict = filter_dict_having_none_value(interface, each)
162            commands.extend(self._clear_config(dict(), have_dict))
163            commands.extend(self._set_config(interface, each, module))
164        # Add the want interface that's not already configured in have interface
165        for each in (not_in_have - in_have):
166            for every in want:
167                interface = 'interface {0}'.format(every['name'])
168                if each and interface not in commands:
169                    commands.extend(self._set_config(every, {}, module))
170
171        # Remove the duplicate interface call
172        commands = remove_duplicate_interface(commands)
173
174        return commands
175
176    def _state_merged(self, want, have, module):
177        """ The command generator when state is merged
178        :rtype: A list
179        :returns: the commands necessary to merge the provided into
180                  the current configuration
181        """
182        commands = []
183
184        for interface in want:
185            interface['name'] = normalize_interface(interface['name'])
186            for each in have:
187                if each['name'] == interface['name']:
188                    break
189                elif interface['name'] in each['name']:
190                    break
191            else:
192                commands.extend(self._set_config(interface, {}, 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 _set_config(self, want, have, module):
226        # Set the interface config based on the want and have config
227        commands = []
228        interface = 'interface ' + want['name']
229        l2_protocol_bool = False
230
231        # Get the diff b/w want and have
232        want_dict = dict_to_set(want)
233        have_dict = dict_to_set(have)
234        diff = want_dict - have_dict
235
236        if diff:
237            # For merging with already configured l2protocol
238            if have.get('l2protocol') and len(have.get('l2protocol')) > 1:
239                l2_protocol_diff = []
240                for each in want.get('l2protocol'):
241                    for every in have.get('l2protocol'):
242                        if every == each:
243                            break
244                    if each not in have.get('l2protocol'):
245                        l2_protocol_diff.append(each)
246                l2_protocol_bool = True
247                l2protocol = tuple(l2_protocol_diff)
248            else:
249                l2protocol = {}
250
251            diff = dict(diff)
252            wants_native = diff.get('native_vlan')
253            l2transport = diff.get('l2transport')
254            q_vlan = diff.get('q_vlan')
255            propagate = diff.get('propagate')
256            if l2_protocol_bool is False:
257                l2protocol = diff.get('l2protocol')
258
259            if wants_native:
260                cmd = 'dot1q native vlan {0}'.format(wants_native)
261                add_command_to_config_list(interface, cmd, commands)
262
263            if l2transport or l2protocol:
264                for each in l2protocol:
265                    each = dict(each)
266                    if isinstance(each, dict):
267                        cmd = 'l2transport l2protocol {0} {1}'.format(list(each.keys())[0], list(each.values())[0])
268                    add_command_to_config_list(interface, cmd, commands)
269                if propagate and not have.get('propagate'):
270                    cmd = 'l2transport propagate remote-status'
271                    add_command_to_config_list(interface, cmd, commands)
272            elif want.get('l2transport') is False and (want.get('l2protocol') or want.get('propagate')):
273                module.fail_json(msg='L2transport L2protocol or Propagate can only be configured when '
274                                     'L2transport set to True!')
275
276            if q_vlan and '.' in interface:
277                q_vlans = (" ".join(map(str, want.get('q_vlan'))))
278                if q_vlans != have.get('q_vlan'):
279                    cmd = 'dot1q vlan {0}'.format(q_vlans)
280                    add_command_to_config_list(interface, cmd, commands)
281
282        return commands
283
284    def _clear_config(self, want, have):
285        # Delete the interface config based on the want and have config
286        commands = []
287
288        if want.get('name'):
289            interface = 'interface ' + want['name']
290        else:
291            interface = 'interface ' + have['name']
292        if have.get('native_vlan'):
293            remove_command_from_config_list(interface, 'dot1q native vlan', commands)
294
295        if have.get('q_vlan'):
296            remove_command_from_config_list(interface, 'encapsulation dot1q', commands)
297
298        if have.get('l2protocol') and (want.get('l2protocol') is None or want.get('propagate') is None):
299            if 'no l2transport' not in commands:
300                remove_command_from_config_list(interface, 'l2transport', commands)
301        elif have.get('l2transport') and have.get('l2transport') != want.get('l2transport'):
302            if 'no l2transport' not in commands:
303                remove_command_from_config_list(interface, 'l2transport', commands)
304
305        return commands
306