1# Copyright 2019 Red Hat
2# GNU General Public License v3.0+
3# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4"""
5The vyos_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
14__metaclass__ = type
15
16from copy import deepcopy
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    dict_diff,
23    remove_empties,
24)
25from ansible.module_utils.six import iteritems
26from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.facts.facts import (
27    Facts,
28)
29from ansible_collections.vyos.vyos.plugins.module_utils.network.vyos.utils.utils import (
30    search_obj_in_list,
31    get_interface_type,
32    dict_delete,
33)
34
35
36class Interfaces(ConfigBase):
37    """
38    The vyos_interfaces class
39    """
40
41    gather_subset = [
42        "!all",
43        "!min",
44    ]
45
46    gather_network_resources = ["interfaces"]
47
48    def __init__(self, module):
49        super(Interfaces, self).__init__(module)
50
51    def get_interfaces_facts(self, data=None):
52        """Get the 'facts' (the current configuration)
53
54        :rtype: A dictionary
55        :returns: The current configuration as a dictionary
56        """
57        facts, _warnings = Facts(self._module).get_facts(
58            self.gather_subset, self.gather_network_resources, data=data
59        )
60        interfaces_facts = facts["ansible_network_resources"].get("interfaces")
61        if not interfaces_facts:
62            return []
63        return interfaces_facts
64
65    def execute_module(self):
66        """Execute the module
67         :rtype: A dictionary
68        :returns: The result from module execution
69        """
70        result = {"changed": False}
71        commands = list()
72        warnings = list()
73
74        if self.state in self.ACTION_STATES:
75            existing_interfaces_facts = self.get_interfaces_facts()
76        else:
77            existing_interfaces_facts = []
78
79        if self.state in self.ACTION_STATES or self.state == "rendered":
80            commands.extend(self.set_config(existing_interfaces_facts))
81
82        if commands and self.state in self.ACTION_STATES:
83            if not self._module.check_mode:
84                self._connection.edit_config(commands)
85            result["changed"] = True
86
87        if self.state in self.ACTION_STATES:
88            result["commands"] = commands
89
90        if self.state in self.ACTION_STATES or self.state == "gathered":
91            changed_interfaces_facts = self.get_interfaces_facts()
92        elif self.state == "rendered":
93            result["rendered"] = commands
94        elif self.state == "parsed":
95            running_config = self._module.params["running_config"]
96            if not running_config:
97                self._module.fail_json(
98                    msg="value of running_config parameter must not be empty for state parsed"
99                )
100            result["parsed"] = self.get_interfaces_facts(data=running_config)
101        else:
102            changed_interfaces_facts = []
103
104        if self.state in self.ACTION_STATES:
105            result["before"] = existing_interfaces_facts
106            if result["changed"]:
107                result["after"] = changed_interfaces_facts
108        elif self.state == "gathered":
109            result["gathered"] = changed_interfaces_facts
110
111        result["warnings"] = warnings
112        return result
113
114    def set_config(self, existing_interfaces_facts):
115        """Collect the configuration from the args passed to the module,
116            collect the current configuration (as a dict from facts)
117
118        :rtype: A list
119        :returns: the commands necessary to migrate the current configuration
120                  to the desired configuration
121        """
122        want = self._module.params["config"]
123        have = existing_interfaces_facts
124        resp = self.set_state(want, have)
125        return to_list(resp)
126
127    def set_state(self, want, have):
128        """Select the appropriate function based on the state provided
129
130        :param want: the desired configuration as a dictionary
131        :param have: the current configuration as a dictionary
132        :rtype: A list
133        :returns: the commands necessary to migrate the current configuration
134                  to the desired configuration
135        """
136        commands = []
137
138        if (
139            self.state in ("merged", "replaced", "overridden", "rendered")
140            and not want
141        ):
142            self._module.fail_json(
143                msg="value of config parameter must not be empty for state {0}".format(
144                    self.state
145                )
146            )
147
148        if self.state == "overridden":
149            commands.extend(self._state_overridden(want=want, have=have))
150
151        elif self.state == "deleted":
152            if not want:
153                for intf in have:
154                    commands.extend(
155                        self._state_deleted({"name": intf["name"]}, intf)
156                    )
157            else:
158                for item in want:
159                    obj_in_have = search_obj_in_list(item["name"], have)
160                    commands.extend(self._state_deleted(item, obj_in_have))
161        else:
162            for item in want:
163                name = item["name"]
164                enable_state = item["enabled"]
165                obj_in_have = search_obj_in_list(name, have)
166                if not obj_in_have:
167                    obj_in_have = {"name": name, "enabled": enable_state}
168
169                if self.state in ("merged", "rendered"):
170                    commands.extend(self._state_merged(item, obj_in_have))
171
172                elif self.state == "replaced":
173                    commands.extend(self._state_replaced(item, obj_in_have))
174
175        return commands
176
177    def _state_replaced(self, want, have):
178        """The command generator when state is replaced
179
180        :rtype: A list
181        :returns: the commands necessary to migrate the current configuration
182                  to the desired configuration
183        """
184        commands = []
185        if have:
186            commands.extend(self._state_deleted(want, have))
187
188        commands.extend(self._state_merged(want, have))
189
190        return commands
191
192    def _state_overridden(self, want, have):
193        """The command generator when state is overridden
194
195        :rtype: A list
196        :returns: the commands necessary to migrate the current configuration
197                  to the desired configuration
198        """
199        commands = []
200
201        for intf in have:
202            intf_in_want = search_obj_in_list(intf["name"], want)
203            if not intf_in_want:
204                commands.extend(
205                    self._state_deleted({"name": intf["name"]}, intf)
206                )
207
208        for intf in want:
209            intf_in_have = search_obj_in_list(intf["name"], have)
210            if not intf_in_have:
211                intf_in_have = {
212                    "name": intf["name"],
213                    "enabled": intf["enabled"],
214                }
215            commands.extend(self._state_replaced(intf, intf_in_have))
216
217        return commands
218
219    def _state_merged(self, want, have):
220        """The command generator when state is merged
221
222        :rtype: A list
223        :returns: the commands necessary to merge the provided into
224                  the current configuration
225        """
226        commands = []
227        want_copy = deepcopy(remove_empties(want))
228        have_copy = deepcopy(have)
229
230        want_vifs = want_copy.pop("vifs", [])
231        have_vifs = have_copy.pop("vifs", [])
232
233        updates = dict_diff(have_copy, want_copy)
234
235        if updates:
236            for key, value in iteritems(updates):
237                commands.append(
238                    self._compute_commands(
239                        key=key, value=value, interface=want_copy["name"]
240                    )
241                )
242
243        if want_vifs:
244            for want_vif in want_vifs:
245                have_vif = search_obj_in_list(
246                    want_vif["vlan_id"], have_vifs, key="vlan_id"
247                )
248                if not have_vif:
249                    have_vif = {
250                        "vlan_id": want_vif["vlan_id"],
251                        "enabled": True,
252                    }
253
254                vif_updates = dict_diff(have_vif, want_vif)
255                if vif_updates:
256                    for key, value in iteritems(vif_updates):
257                        commands.append(
258                            self._compute_commands(
259                                key=key,
260                                value=value,
261                                interface=want_copy["name"],
262                                vif=want_vif["vlan_id"],
263                            )
264                        )
265
266        return commands
267
268    def _state_deleted(self, want, have):
269        """The command generator when state is deleted
270
271        :rtype: A list
272        :returns: the commands necessary to remove the current configuration
273                  of the provided objects
274        """
275        commands = []
276
277        want_copy = deepcopy(remove_empties(want))
278        have_copy = deepcopy(have)
279        want_vifs = want_copy.pop("vifs", [])
280        have_vifs = have_copy.pop("vifs", [])
281
282        for key in dict_delete(have_copy, want_copy).keys():
283            if key == "enabled":
284                continue
285            commands.append(
286                self._compute_commands(
287                    key=key, interface=want_copy["name"], remove=True
288                )
289            )
290        if have_copy["enabled"] is False:
291            commands.append(
292                self._compute_commands(
293                    key="enabled", value=True, interface=want_copy["name"]
294                )
295            )
296
297        if have_vifs:
298            for have_vif in have_vifs:
299                want_vif = search_obj_in_list(
300                    have_vif["vlan_id"], want_vifs, key="vlan_id"
301                )
302                if not want_vif:
303                    want_vif = {
304                        "vlan_id": have_vif["vlan_id"],
305                        "enabled": True,
306                    }
307
308                for key in dict_delete(have_vif, want_vif).keys():
309                    if key == "enabled":
310                        continue
311                    commands.append(
312                        self._compute_commands(
313                            key=key,
314                            interface=want_copy["name"],
315                            vif=want_vif["vlan_id"],
316                            remove=True,
317                        )
318                    )
319                if have_vif["enabled"] is False:
320                    commands.append(
321                        self._compute_commands(
322                            key="enabled",
323                            value=True,
324                            interface=want_copy["name"],
325                            vif=want_vif["vlan_id"],
326                        )
327                    )
328        return commands
329
330    def _compute_commands(
331        self, interface, key, vif=None, value=None, remove=False
332    ):
333        intf_context = "interfaces {0} {1}".format(
334            get_interface_type(interface), interface
335        )
336        set_cmd = "set {0}".format(intf_context)
337        del_cmd = "delete {0}".format(intf_context)
338
339        if vif:
340            set_cmd = set_cmd + (" vif {0}".format(vif))
341            del_cmd = del_cmd + (" vif {0}".format(vif))
342
343        if key == "enabled":
344            if not value:
345                command = "{0} disable".format(set_cmd)
346            else:
347                command = "{0} disable".format(del_cmd)
348        else:
349            if not remove:
350                command = "{0} {1} '{2}'".format(set_cmd, key, value)
351            else:
352                command = "{0} {1}".format(del_cmd, key)
353
354        return command
355