1# -*- coding: utf-8 -*-
2# Copyright 2019 Red Hat
3# GNU General Public License v3.0+
4# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5"""
6The eos_l2_interfaces class
7It is in this file where the current configuration (as dict)
8is compared to the provided configuration (as dict) and the command set
9necessary to bring the current configuration to it's desired end-state is
10created
11"""
12
13from __future__ import absolute_import, division, print_function
14
15__metaclass__ = type
16
17from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import (
18    ConfigBase,
19)
20from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
21    to_list,
22    param_list_to_dict,
23)
24from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import (
25    Facts,
26)
27from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import (
28    normalize_interface,
29    vlan_range_to_list,
30)
31
32
33class L2_interfaces(ConfigBase):
34    """
35    The eos_l2_interfaces class
36    """
37
38    gather_subset = ["!all", "!min"]
39
40    gather_network_resources = ["l2_interfaces"]
41
42    def get_l2_interfaces_facts(self, data=None):
43        """Get the 'facts' (the current configuration)
44
45        :rtype: A dictionary
46        :returns: The current configuration as a dictionary
47        """
48        facts, _warnings = Facts(self._module).get_facts(
49            self.gather_subset, self.gather_network_resources, data=data
50        )
51        l2_interfaces_facts = facts["ansible_network_resources"].get(
52            "l2_interfaces"
53        )
54        if not l2_interfaces_facts:
55            return []
56        return l2_interfaces_facts
57
58    def execute_module(self):
59        """Execute the module
60
61        :rtype: A dictionary
62        :returns: The result from module execution
63        """
64        result = {"changed": False}
65        commands = list()
66        warnings = list()
67
68        if self.state in self.ACTION_STATES:
69            existing_l2_interfaces_facts = self.get_l2_interfaces_facts()
70        else:
71            existing_l2_interfaces_facts = []
72
73        if self.state in self.ACTION_STATES or self.state == "rendered":
74            commands.extend(self.set_config(existing_l2_interfaces_facts))
75
76        if commands and self.state in self.ACTION_STATES:
77            if not self._module.check_mode:
78                self._connection.edit_config(commands)
79            result["changed"] = True
80
81        if self.state in self.ACTION_STATES:
82            result["commands"] = commands
83
84        if self.state in self.ACTION_STATES or self.state == "gathered":
85            changed_l2_interfaces_facts = self.get_l2_interfaces_facts()
86
87        elif self.state == "rendered":
88            result["rendered"] = commands
89
90        elif self.state == "parsed":
91            running_config = self._module.params["running_config"]
92            if not running_config:
93                self._module.fail_json(
94                    msg="value of running_config parameter must not be empty for state parsed"
95                )
96            result["parsed"] = self.get_l2_interfaces_facts(
97                data=running_config
98            )
99
100        if self.state in self.ACTION_STATES:
101            result["before"] = existing_l2_interfaces_facts
102            if result["changed"]:
103                result["after"] = changed_l2_interfaces_facts
104
105        elif self.state == "gathered":
106            result["gathered"] = changed_l2_interfaces_facts
107
108        result["warnings"] = warnings
109        return result
110
111    def set_config(self, existing_l2_interfaces_facts):
112        """Collect the configuration from the args passed to the module,
113            collect the current configuration (as a dict from facts)
114
115        :rtype: A list
116        :returns: the commands necessary to migrate the current configuration
117                  to the desired configuration
118        """
119        want = self._module.params["config"]
120        have = existing_l2_interfaces_facts
121        resp = self.set_state(want, have)
122        return to_list(resp)
123
124    def set_state(self, want, have):
125        """Select the appropriate function based on the state provided
126
127        :param want: the desired configuration as a dictionary
128        :param have: the current configuration as a dictionary
129        :rtype: A list
130        :returns: the commands necessary to migrate the current configuration
131                  to the desired configuration
132        """
133        state = self._module.params["state"]
134        if (
135            state in ("merged", "replaced", "overridden", "rendered")
136            and not want
137        ):
138            self._module.fail_json(
139                msg="value of config parameter must not be empty for state {0}".format(
140                    state
141                )
142            )
143        want = param_list_to_dict(want)
144        have = param_list_to_dict(have)
145        commands = []
146        if state == "overridden":
147            commands = self._state_overridden(want, have)
148        elif state == "deleted":
149            commands = self._state_deleted(want, have)
150        elif state == "merged" or state == "rendered":
151            commands = self._state_merged(want, have)
152        elif state == "replaced":
153            commands = self._state_replaced(want, have)
154        return commands
155
156    @staticmethod
157    def _state_replaced(want, have):
158        """The command generator when state is replaced
159
160        :rtype: A list
161        :returns: the commands necessary to migrate the current configuration
162                  to the desired configuration
163        """
164        commands = []
165        for key, desired in want.items():
166            interface_name = normalize_interface(key)
167            if interface_name in have:
168                extant = have[interface_name]
169            else:
170                extant = dict()
171
172            intf_commands = set_interface(desired, extant)
173            intf_commands.extend(clear_interface(desired, extant))
174
175            if intf_commands:
176                commands.append("interface {0}".format(interface_name))
177                commands.extend(intf_commands)
178
179        return commands
180
181    @staticmethod
182    def _state_overridden(want, have):
183        """The command generator when state is overridden
184
185        :rtype: A list
186        :returns: the commands necessary to migrate the current configuration
187                  to the desired configuration
188        """
189        commands = []
190        for key, extant in have.items():
191            if key in want:
192                desired = want[key]
193            else:
194                desired = dict()
195
196            intf_commands = set_interface(desired, extant)
197            intf_commands.extend(clear_interface(desired, extant))
198
199            if intf_commands:
200                commands.append("interface {0}".format(key))
201                commands.extend(intf_commands)
202
203        return commands
204
205    @staticmethod
206    def _state_merged(want, have):
207        """The command generator when state is merged
208
209        :rtype: A list
210        :returns: the commands necessary to merge the provided into
211                  the current configuration
212        """
213        commands = []
214        for key, desired in want.items():
215            interface_name = normalize_interface(key)
216            if interface_name in have:
217                extant = have[interface_name]
218            else:
219                extant = dict()
220
221            intf_commands = set_interface(desired, extant)
222
223            if intf_commands:
224                commands.append("interface {0}".format(interface_name))
225                commands.extend(intf_commands)
226
227        return commands
228
229    @staticmethod
230    def _state_deleted(want, have):
231        """The command generator when state is deleted
232
233        :rtype: A list
234        :returns: the commands necessary to remove the current configuration
235                  of the provided objects
236        """
237        commands = []
238        for key in want:
239            desired = dict()
240            if key in have:
241                extant = have[key]
242            else:
243                continue
244            intf_commands = clear_interface(desired, extant)
245
246            if intf_commands:
247                commands.append("interface {0}".format(key))
248                commands.extend(intf_commands)
249
250        return commands
251
252
253def set_interface(want, have):
254    commands = []
255
256    want_mode = want.get("mode")
257    if want_mode and want_mode != have.get("mode"):
258        commands.append("switchport mode {0}".format(want_mode))
259
260    wants_access = want.get("access")
261    if wants_access:
262        access_vlan = wants_access.get("vlan")
263        if access_vlan and access_vlan != have.get("access", {}).get("vlan"):
264            commands.append("switchport access vlan {0}".format(access_vlan))
265
266    wants_trunk = want.get("trunk")
267    if wants_trunk:
268        allowed_vlans = []
269        has_allowed_vlans = {}
270        want_allowed_vlans = {}
271        has_trunk = have.get("trunk", {})
272        native_vlan = wants_trunk.get("native_vlan")
273        if native_vlan and native_vlan != has_trunk.get("native_vlan"):
274            commands.append(
275                "switchport trunk native vlan {0}".format(native_vlan)
276            )
277        for con in [want, have]:
278            expand_trunk_allowed_vlans(con)
279        want_allowed_vlans = want["trunk"].get("trunk_allowed_vlans")
280        if has_trunk:
281            has_allowed_vlans = has_trunk.get("trunk_allowed_vlans")
282
283        if want_allowed_vlans and has_allowed_vlans:
284            allowed_vlans = list(
285                set(want_allowed_vlans.split(","))
286                - set(has_allowed_vlans.split(","))
287            )
288        elif want_allowed_vlans:
289            allowed_vlans = want_allowed_vlans.split(",")
290        if allowed_vlans:
291            allowed_vlans.sort()
292            allowed_vlans = ",".join(
293                ["{0}".format(vlan) for vlan in allowed_vlans]
294            )
295            if has_allowed_vlans:
296                commands.append(
297                    "switchport trunk allowed vlan add {0}".format(
298                        allowed_vlans
299                    )
300                )
301            else:
302                commands.append(
303                    "switchport trunk allowed vlan {0}".format(allowed_vlans)
304                )
305    return commands
306
307
308def expand_trunk_allowed_vlans(want):
309    if not want:
310        return None
311    if want.get("trunk"):
312        if "trunk_allowed_vlans" in want["trunk"]:
313            allowed_vlans = vlan_range_to_list(
314                want["trunk"]["trunk_allowed_vlans"]
315            )
316            vlans_list = [str(num) for num in sorted(allowed_vlans)]
317            want["trunk"]["trunk_allowed_vlans"] = ",".join(vlans_list)
318
319
320def clear_interface(want, have):
321    commands = []
322
323    if "mode" in have and want.get("mode") is None:
324        commands.append("no switchport mode")
325
326    if "access" in have and not want.get("access"):
327        commands.append("no switchport access vlan")
328
329    has_trunk = have.get("trunk") or {}
330    wants_trunk = want.get("trunk") or {}
331    if (
332        "trunk_allowed_vlans" in has_trunk
333        and "trunk_allowed_vlans" not in wants_trunk
334    ):
335        commands.append("no switchport trunk allowed vlan")
336    if (
337        "trunk_allowed_vlans" in has_trunk
338        and "trunk_allowed_vlans" in wants_trunk
339    ):
340        for con in [want, have]:
341            expand_trunk_allowed_vlans(con)
342        want_allowed_vlans = want["trunk"].get("trunk_allowed_vlans")
343        has_allowed_vlans = has_trunk.get("trunk_allowed_vlans")
344        allowed_vlans = list(
345            set(has_allowed_vlans.split(","))
346            - set(want_allowed_vlans.split(","))
347        )
348        if allowed_vlans:
349            allowed_vlans = ",".join(
350                ["{0}".format(vlan) for vlan in allowed_vlans]
351            )
352
353            commands.append(
354                "switchport trunk allowed vlan remove {0}".format(
355                    allowed_vlans
356                )
357            )
358
359    if "native_vlan" in has_trunk and "native_vlan" not in wants_trunk:
360        commands.append("no switchport trunk native vlan")
361    return commands
362