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_lag_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
15import re
16
17__metaclass__ = type
18
19from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
20    to_list,
21    dict_diff,
22)
23
24from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import (
25    ConfigBase,
26)
27from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import (
28    Facts,
29)
30from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import (
31    normalize_interface,
32)
33
34
35class Lag_interfaces(ConfigBase):
36    """
37    The eos_lag_interfaces class
38    """
39
40    gather_subset = ["!all", "!min"]
41
42    gather_network_resources = ["lag_interfaces"]
43
44    def get_lag_interfaces_facts(self, data=None):
45        """Get the 'facts' (the current configuration)
46
47        :rtype: A dictionary
48        :returns: The current configuration as a dictionary
49        """
50        facts, _warnings = Facts(self._module).get_facts(
51            self.gather_subset, self.gather_network_resources, data=data
52        )
53        lag_interfaces_facts = facts["ansible_network_resources"].get(
54            "lag_interfaces"
55        )
56        if not lag_interfaces_facts:
57            return []
58        return lag_interfaces_facts
59
60    def execute_module(self):
61        """Execute the module
62
63        :rtype: A dictionary
64        :returns: The result from module execution
65        """
66        result = {"changed": False}
67        commands = list()
68        warnings = list()
69
70        if self.state in self.ACTION_STATES:
71            existing_lag_interfaces_facts = self.get_lag_interfaces_facts()
72        else:
73            existing_lag_interfaces_facts = {}
74
75        if self.state in self.ACTION_STATES or self.state == "rendered":
76            commands.extend(self.set_config(existing_lag_interfaces_facts))
77
78        if commands and self.state in self.ACTION_STATES:
79            if not self._module.check_mode:
80                self._connection.edit_config(commands)
81            result["changed"] = True
82        if self.state in self.ACTION_STATES:
83            result["commands"] = commands
84
85        if self.state in self.ACTION_STATES or self.state == "gathered":
86            changed_lag_interfaces_facts = self.get_lag_interfaces_facts()
87        elif self.state == "rendered":
88            result["rendered"] = commands
89        elif self.state == "parsed":
90            running_config = self._module.params["running_config"]
91            if not running_config:
92                self._module.fail_json(
93                    msg="value of running_config parameter must not be empty for state parsed"
94                )
95            result["parsed"] = self.get_lag_interfaces_facts(
96                data=running_config
97            )
98
99        if self.state in self.ACTION_STATES:
100            result["before"] = existing_lag_interfaces_facts
101            if result["changed"]:
102                result["after"] = changed_lag_interfaces_facts
103        elif self.state == "gathered":
104            result["gathered"] = changed_lag_interfaces_facts
105
106        result["warnings"] = warnings
107        return result
108
109    def set_config(self, existing_lag_interfaces_facts):
110        """Collect the configuration from the args passed to the module,
111            collect the current configuration (as a dict from facts)
112
113        :rtype: A list
114        :returns: the commands necessary to migrate the current configuration
115                  to the desired configuration
116        """
117        want = self._module.params["config"]
118        have = existing_lag_interfaces_facts
119        resp = self.set_state(want, have)
120        return to_list(resp)
121
122    def set_state(self, want, have):
123        """Select the appropriate function based on the state provided
124
125        :param want: the desired configuration as a dictionary
126        :param have: the current configuration as a dictionary
127        :rtype: A list
128        :returns: the commands necessary to migrate the current configuration
129                  to the desired configuration
130        """
131        state = self._module.params["state"]
132        if (
133            state in ("merged", "replaced", "overridden", "rendered")
134            and not want
135        ):
136            self._module.fail_json(
137                msg="value of config parameter must not be empty for state {0}".format(
138                    state
139                )
140            )
141        if state == "overridden":
142            commands = self._state_overridden(want, have)
143        elif state == "deleted":
144            commands = self._state_deleted(want, have)
145        elif state == "merged" or state == "rendered":
146            commands = self._state_merged(want, have)
147        elif state == "replaced":
148            commands = self._state_replaced(want, have)
149        return commands
150
151    @staticmethod
152    def _state_replaced(want, have):
153        """The command generator when state is replaced
154        :rtype: A list
155        :returns: the commands necessary to migrate the current configuration
156                  to the desired configuration
157        """
158        commands = []
159        for interface in want:
160            interface_name = normalize_interface(interface["name"])
161            for extant in have:
162                if extant["name"] == interface_name:
163                    break
164            else:
165                extant = dict(name=interface_name)
166
167            commands.extend(set_config(interface, extant))
168            commands.extend(remove_config(interface, extant))
169
170        return commands
171
172    @staticmethod
173    def _state_overridden(want, have):
174        """The command generator when state is overridden
175        :rtype: A list
176        :returns: the commands necessary to migrate the current configuration
177                  to the desired configuration
178        """
179        commands = []
180        for extant in have:
181            for interface in want:
182                if normalize_interface(interface["name"]) == extant["name"]:
183                    break
184            else:
185                interface = dict(name=extant["name"])
186            commands.extend(remove_config(interface, extant))
187
188        for interface in want:
189            interface_name = normalize_interface(interface["name"])
190            for extant in have:
191                if extant["name"] == interface_name:
192                    break
193            else:
194                extant = dict(name=interface_name)
195            commands.extend(set_config(interface, extant))
196
197        return commands
198
199    @staticmethod
200    def _state_merged(want, have):
201        """The command generator when state is merged
202        :rtype: A list
203        :returns: the commands necessary to merge the provided into
204                  the current configuration
205        """
206        commands = []
207        for interface in want:
208            interface_name = normalize_interface(interface["name"])
209            for extant in have:
210                if extant["name"] == interface_name:
211                    break
212            else:
213                extant = dict(name=interface_name)
214
215            commands.extend(set_config(interface, extant))
216
217        return commands
218
219    @staticmethod
220    def _state_deleted(want, have):
221        """The command generator when state is deleted
222        :rtype: A list
223        :returns: the commands necessary to remove the current configuration
224                  of the provided objects
225        """
226        commands = []
227        for interface in want:
228            interface_name = normalize_interface(interface["name"])
229            for extant in have:
230                if extant["name"] == interface_name:
231                    break
232            else:
233                extant = dict(name=interface_name)
234
235            # Clearing all args, send empty dictionary
236            interface = dict(name=interface_name)
237            commands.extend(remove_config(interface, extant))
238
239        return commands
240
241
242def set_config(want, have):
243    commands = []
244    to_set = dict_diff(have, want)
245    for member in to_set.get("members", []):
246        channel_id = re.search(r"\d.*", want["name"])
247        if channel_id:
248            commands.extend(
249                [
250                    "interface {0}".format(member["member"]),
251                    "channel-group {0} mode {1}".format(
252                        channel_id.group(0), member["mode"]
253                    ),
254                ]
255            )
256
257    return commands
258
259
260def remove_config(want, have):
261    commands = []
262    if not want.get("members"):
263        return ["no interface {0}".format(want["name"])]
264
265    to_remove = dict_diff(want, have)
266    for member in to_remove.get("members", []):
267        commands.extend(
268            ["interface {0}".format(member["member"]), "no channel-group"]
269        )
270
271    return commands
272