1#
2# -*- coding: utf-8 -*-
3# Copyright 2019 Red Hat
4# GNU General Public License v3.0+
5# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6"""
7The iosxr_lag_interfaces class
8It is in this file where the current configuration (as dict)
9is compared to the provided configuration (as dict) and the command set
10necessary to bring the current configuration to it's desired end-state is
11created
12"""
13
14from __future__ import absolute_import, division, print_function
15
16__metaclass__ = type
17
18from copy import deepcopy
19from distutils.version import LooseVersion
20from ansible.module_utils.six import iteritems
21from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import (
22    ConfigBase,
23)
24from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.facts.facts import (
25    Facts,
26)
27from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
28    to_list,
29    dict_diff,
30    remove_empties,
31    search_obj_in_list,
32    param_list_to_dict,
33)
34from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.utils.utils import (
35    diff_list_of_dicts,
36    pad_commands,
37    flatten_dict,
38    dict_delete,
39    normalize_interface,
40)
41from ansible_collections.cisco.iosxr.plugins.module_utils.network.iosxr.iosxr import (
42    get_os_version,
43)
44
45
46class Lag_interfaces(ConfigBase):
47    """
48    The iosxr_lag_interfaces class
49    """
50
51    gather_subset = ["!all", "!min"]
52
53    gather_network_resources = ["lag_interfaces"]
54
55    def __init__(self, module):
56        super(Lag_interfaces, self).__init__(module)
57
58    def get_lag_interfaces_facts(self, data=None):
59        """ Get the 'facts' (the current configuration)
60
61        :rtype: A dictionary
62        :returns: The current configuration as a dictionary
63        """
64        facts, _warnings = Facts(self._module).get_facts(
65            self.gather_subset, self.gather_network_resources, data=data
66        )
67        lag_interfaces_facts = facts["ansible_network_resources"].get(
68            "lag_interfaces"
69        )
70        if not lag_interfaces_facts:
71            return []
72        return lag_interfaces_facts
73
74    def execute_module(self):
75        """ Execute the module
76
77        :rtype: A dictionary
78        :returns: The result from module execution
79        """
80        result = {"changed": False}
81        warnings = list()
82        commands = list()
83        if self.state in self.ACTION_STATES:
84            existing_lag_interfaces_facts = self.get_lag_interfaces_facts()
85        else:
86            existing_lag_interfaces_facts = {}
87
88        if self.state in self.ACTION_STATES or self.state == "rendered":
89            commands.extend(self.set_config(existing_lag_interfaces_facts))
90
91        if commands and self.state in self.ACTION_STATES:
92            if not self._module.check_mode:
93                self._connection.edit_config(commands)
94            result["changed"] = True
95
96        if self.state in self.ACTION_STATES:
97            result["commands"] = commands
98
99        if self.state in self.ACTION_STATES or self.state == "gathered":
100            changed_lag_interfaces_facts = self.get_lag_interfaces_facts()
101
102        elif self.state == "rendered":
103            result["rendered"] = commands
104
105        elif self.state == "parsed":
106            running_config = self._module.params["running_config"]
107            if not running_config:
108                self._module.fail_json(
109                    msg="value of running_config parameter must not be empty for state parsed"
110                )
111            result["parsed"] = self.get_lag_interfaces_facts(
112                data=running_config
113            )
114
115        if self.state in self.ACTION_STATES:
116            result["before"] = existing_lag_interfaces_facts
117            if result["changed"]:
118                result["after"] = changed_lag_interfaces_facts
119
120        elif self.state == "gathered":
121            result["gathered"] = changed_lag_interfaces_facts
122
123        result["warnings"] = warnings
124        return result
125
126    def set_config(self, existing_lag_interfaces_facts):
127        """ Collect the configuration from the args passed to the module,
128            collect the current configuration (as a dict from facts)
129
130        :rtype: A list
131        :returns: the commands necessary to migrate the current configuration
132                  to the desired configuration
133        """
134        want = self._module.params["config"]
135        if want:
136            for item in want:
137                item["name"] = normalize_interface(item["name"])
138                if "members" in want and want["members"]:
139                    for item in want["members"]:
140                        item.update(
141                            {
142                                "member": normalize_interface(item["member"]),
143                                "mode": item["mode"],
144                            }
145                        )
146        have = existing_lag_interfaces_facts
147        resp = self.set_state(want, have)
148        return to_list(resp)
149
150    def set_state(self, want, have):
151        """ Select the appropriate function based on the state provided
152
153        :param want: the desired configuration as a dictionary
154        :param have: the current configuration as a dictionary
155        :rtype: A list
156        :returns: the commands necessary to migrate the current configuration
157                  to the desired configuration
158        """
159        state = self._module.params["state"]
160        commands = []
161
162        if (
163            self.state in ("merged", "replaced", "overridden", "rendered")
164            and not want
165        ):
166            self._module.fail_json(
167                msg="value of config parameter must not be empty for state {0}".format(
168                    state
169                )
170            )
171
172        if state == "overridden":
173            commands.extend(self._state_overridden(want, have))
174
175        elif state == "deleted":
176            commands.extend(self._state_deleted(want, have))
177
178        else:
179            # Instead of passing entire want and have
180            # list of dictionaries to the respective
181            # _state_* methods we are passing the want
182            # and have dictionaries per interface
183            for item in want:
184                name = item["name"]
185                obj_in_have = search_obj_in_list(name, have)
186
187                if state in ("merged", "rendered"):
188                    commands.extend(self._state_merged(item, obj_in_have))
189
190                elif state == "replaced":
191                    commands.extend(self._state_replaced(item, obj_in_have))
192
193        return commands
194
195    def _state_replaced(self, want, have):
196        """ The command generator when state is replaced
197
198        :rtype: A list
199        :returns: the commands necessary to migrate the current configuration
200                  to the desired configuration
201        """
202        commands = []
203        if have:
204            commands.extend(self._render_bundle_del_commands(want, have))
205        commands.extend(self._render_bundle_updates(want, have))
206
207        if commands or have == {}:
208            pad_commands(commands, want["name"])
209
210        if have:
211            commands.extend(self._render_interface_del_commands(want, have))
212        commands.extend(self._render_interface_updates(want, have))
213
214        return commands
215
216    def _state_overridden(self, want, have):
217        """ The command generator when state is overridden
218
219        :rtype: A list
220        :returns: the commands necessary to migrate the current configuration
221                  to the desired configuration
222        """
223        commands = []
224        for have_intf in have:
225            intf_in_want = search_obj_in_list(have_intf["name"], want)
226            if not intf_in_want:
227                commands.extend(self._purge_attribs(have_intf))
228
229        for intf in want:
230            intf_in_have = search_obj_in_list(intf["name"], have)
231            commands.extend(self._state_replaced(intf, intf_in_have))
232
233        return commands
234
235    def _state_merged(self, want, have):
236        """ The command generator when state is merged
237
238        :rtype: A list
239        :returns: the commands necessary to merge the provided into
240                  the current configuration
241        """
242        commands = []
243        commands.extend(self._render_bundle_updates(want, have))
244
245        if commands or have == {}:
246            pad_commands(commands, want["name"])
247
248        commands.extend(self._render_interface_updates(want, have))
249        return commands
250
251    def _state_deleted(self, want, have):
252        """ The command generator when state is deleted
253
254        :rtype: A list
255        :returns: the commands necessary to remove the current configuration
256                  of the provided objects
257        """
258        commands = []
259
260        if not want:
261            for item in have:
262                commands.extend(self._purge_attribs(intf=item))
263        else:
264            for item in want:
265                name = item["name"]
266                obj_in_have = search_obj_in_list(name, have)
267                if not obj_in_have:
268                    self._module.fail_json(
269                        msg=("interface {0} does not exist".format(name))
270                    )
271                commands.extend(self._purge_attribs(intf=obj_in_have))
272
273        return commands
274
275    def _render_bundle_updates(self, want, have):
276        """ The command generator for updates to bundles
277         :rtype: A list
278        :returns: the commands necessary to update bundles
279        """
280        commands = []
281        if not have:
282            have = {"name": want["name"]}
283
284        want_copy = deepcopy(want)
285        have_copy = deepcopy(have)
286
287        want_copy.pop("members", [])
288        have_copy.pop("members", [])
289
290        bundle_updates = dict_diff(have_copy, want_copy)
291
292        if bundle_updates:
293            for key, value in iteritems(
294                flatten_dict(remove_empties(bundle_updates))
295            ):
296                commands.append(self._compute_commands(key=key, value=value))
297
298        return commands
299
300    def _render_interface_updates(self, want, have):
301        """ The command generator for updates to member
302            interfaces
303        :rtype: A list
304        :returns: the commands necessary to update member
305                  interfaces
306        """
307        commands = []
308
309        if not have:
310            have = {"name": want["name"]}
311
312        member_diff = diff_list_of_dicts(
313            want["members"], have.get("members", [])
314        )
315
316        for diff in member_diff:
317            diff_cmd = []
318            bundle_cmd = "bundle id {0}".format(
319                want["name"].split("Bundle-Ether")[1]
320            )
321            if diff.get("mode"):
322                bundle_cmd += " mode {0}".format(diff.get("mode"))
323            diff_cmd.append(bundle_cmd)
324            pad_commands(diff_cmd, diff["member"])
325            commands.extend(diff_cmd)
326
327        return commands
328
329    def _render_bundle_del_commands(self, want, have):
330        """ The command generator for delete commands
331            w.r.t bundles
332        :rtype: A list
333        :returns: the commands necessary to update member
334                  interfaces
335        """
336        commands = []
337        if not want:
338            want = {"name": have["name"]}
339
340        want_copy = deepcopy(want)
341        have_copy = deepcopy(have)
342        want_copy.pop("members", [])
343        have_copy.pop("members", [])
344
345        to_delete = dict_delete(have_copy, remove_empties(want_copy))
346        if to_delete:
347            for key, value in iteritems(
348                flatten_dict(remove_empties(to_delete))
349            ):
350                commands.append(
351                    self._compute_commands(key=key, value=value, remove=True)
352                )
353
354        return commands
355
356    def _render_interface_del_commands(self, want, have):
357        """ The command generator for delete commands
358            w.r.t member interfaces
359        :rtype: A list
360        :returns: the commands necessary to update member
361                  interfaces
362        """
363        commands = []
364        if not want:
365            want = {}
366        have_members = have.get("members")
367
368        if have_members:
369            have_members = param_list_to_dict(
370                deepcopy(have_members), unique_key="member"
371            )
372            want_members = param_list_to_dict(
373                deepcopy(want).get("members", []), unique_key="member"
374            )
375
376            for key in have_members:
377                if key not in want_members:
378                    member_cmd = ["no bundle id"]
379                    pad_commands(member_cmd, key)
380                    commands.extend(member_cmd)
381
382        return commands
383
384    def _purge_attribs(self, intf):
385        """ The command generator for purging attributes
386        :rtype: A list
387        :returns: the commands necessary to purge attributes
388        """
389        commands = []
390        have_copy = deepcopy(intf)
391        members = have_copy.pop("members", [])
392
393        to_delete = dict_delete(
394            have_copy, remove_empties({"name": have_copy["name"]})
395        )
396        if to_delete:
397            for key, value in iteritems(
398                flatten_dict(remove_empties(to_delete))
399            ):
400                commands.append(
401                    self._compute_commands(key=key, value=value, remove=True)
402                )
403
404        if commands:
405            pad_commands(commands, intf["name"])
406
407        if members:
408            members = param_list_to_dict(
409                deepcopy(members), unique_key="member"
410            )
411            for key in members:
412                member_cmd = ["no bundle id"]
413                pad_commands(member_cmd, key)
414                commands.extend(member_cmd)
415
416        return commands
417
418    def _compute_commands(self, key, value, remove=False):
419        """ The method generates LAG commands based on the
420            key, value passed. When remove is set to True,
421            the command is negated.
422        :rtype: str
423        :returns: a command based on the `key`, `value` pair
424                  passed and the value of `remove`
425        """
426        if key == "mode":
427            cmd = "lacp mode {0}".format(value)
428
429        elif key == "load_balancing_hash":
430            os_version = get_os_version(self._module)
431            if os_version and LooseVersion(os_version) < LooseVersion("7.0.0"):
432                cmd = "bundle load-balancing hash {0}".format(value)
433
434        elif key == "max_active":
435            cmd = "bundle maximum-active links {0}".format(value)
436
437        elif key == "min_active":
438            cmd = "bundle minimum-active links {0}".format(value)
439
440        if remove:
441            cmd = "no {0}".format(cmd)
442
443        return cmd
444