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