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_lag_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"""
11from __future__ import absolute_import, division, print_function
12__metaclass__ = type
13from ansible.module_utils.network.common.cfg.base import ConfigBase
14from ansible.module_utils.network.vyos.facts.facts import Facts
15from ansible.module_utils.network.common.utils import to_list, dict_diff
16from ansible.module_utils.six import iteritems
17from ansible.module_utils.network. \
18    vyos.utils.utils import search_obj_in_list, \
19    get_lst_diff_for_dicts, list_diff_want_only, list_diff_have_only
20
21
22class Lag_interfaces(ConfigBase):
23    """
24    The vyos_lag_interfaces class
25    """
26
27    gather_subset = [
28        '!all',
29        '!min',
30    ]
31
32    gather_network_resources = [
33        'lag_interfaces',
34    ]
35
36    params = ['arp_monitor', 'hash_policy', 'members', 'mode', 'name', 'primary']
37
38    def __init__(self, module):
39        super(Lag_interfaces, self).__init__(module)
40
41    def get_lag_interfaces_facts(self):
42        """ Get the 'facts' (the current configuration)
43
44        :rtype: A dictionary
45        :returns: The current configuration as a dictionary
46        """
47        facts, _warnings = Facts(self._module).get_facts(self.gather_subset,
48                                                         self.gather_network_resources)
49        lag_interfaces_facts = facts['ansible_network_resources'].get('lag_interfaces')
50        if not lag_interfaces_facts:
51            return []
52        return lag_interfaces_facts
53
54    def execute_module(self):
55        """ Execute the module
56
57        :rtype: A dictionary
58        :returns: The result from module execution
59        """
60        result = {'changed': False}
61        commands = list()
62        warnings = list()
63        existing_lag_interfaces_facts = self.get_lag_interfaces_facts()
64        commands.extend(self.set_config(existing_lag_interfaces_facts))
65        if commands:
66            if self._module.check_mode:
67                resp = self._connection.edit_config(commands, commit=False)
68            else:
69                resp = self._connection.edit_config(commands)
70            result['changed'] = True
71
72        result['commands'] = commands
73
74        if self._module._diff:
75            result['diff'] = resp['diff'] if result['changed'] else None
76
77        changed_lag_interfaces_facts = self.get_lag_interfaces_facts()
78
79        result['before'] = existing_lag_interfaces_facts
80        if result['changed']:
81            result['after'] = changed_lag_interfaces_facts
82
83        result['warnings'] = warnings
84        return result
85
86    def set_config(self, existing_lag_interfaces_facts):
87        """ Collect the configuration from the args passed to the module,
88            collect the current configuration (as a dict from facts)
89
90        :rtype: A list
91        :returns: the commands necessary to migrate the current configuration
92                  to the desired configuration
93        """
94        want = self._module.params['config']
95        have = existing_lag_interfaces_facts
96        resp = self.set_state(want, have)
97        return to_list(resp)
98
99    def set_state(self, want, have):
100        """ Select the appropriate function based on the state provided
101
102        :param want: the desired configuration as a dictionary
103        :param have: the current configuration as a dictionary
104        :rtype: A list
105        :returns: the commands necessary to migrate the current configuration
106                  to the desired configuration
107        """
108        commands = []
109        state = self._module.params['state']
110        if state in ('merged', 'replaced', 'overridden') and not want:
111            self._module.fail_json(msg='config is required for state {0}'.format(state))
112        if state == 'overridden':
113            commands.extend(self._state_overridden(want, have))
114        elif state == 'deleted':
115            if want:
116                for want_item in want:
117                    name = want_item['name']
118                    obj_in_have = search_obj_in_list(name, have)
119                    commands.extend(self._state_deleted(obj_in_have))
120            else:
121                for have_item in have:
122                    commands.extend(self._state_deleted(have_item))
123        else:
124            for want_item in want:
125                name = want_item['name']
126                obj_in_have = search_obj_in_list(name, have)
127                if state == 'merged':
128                    commands.extend(self._state_merged(want_item, obj_in_have))
129                elif state == 'replaced':
130                    commands.extend(self._state_replaced(want_item, obj_in_have))
131        return commands
132
133    def _state_replaced(self, want, have):
134        """ The command generator when state is replaced
135
136        :rtype: A list
137        :returns: the commands necessary to migrate the current configuration
138                  to the desired configuration
139        """
140        commands = []
141        if have:
142            commands.extend(self._render_del_commands(want, have))
143        commands.extend(self._state_merged(want, have))
144        return commands
145
146    def _state_overridden(self, want, have):
147        """ The command generator when state is overridden
148
149        :rtype: A list
150        :returns: the commands necessary to migrate the current configuration
151                  to the desired configuration
152        """
153        commands = []
154        for have_item in have:
155            lag_name = have_item['name']
156            obj_in_want = search_obj_in_list(lag_name, want)
157            if not obj_in_want:
158                commands.extend(self._purge_attribs(have_item))
159
160        for want_item in want:
161            name = want_item['name']
162            obj_in_have = search_obj_in_list(name, have)
163            commands.extend(self._state_replaced(want_item, obj_in_have))
164        return commands
165
166    def _state_merged(self, want, have):
167        """ The command generator when state is merged
168
169        :rtype: A list
170        :returns: the commands necessary to merge the provided into
171                  the current configuration
172        """
173        commands = []
174        if have:
175            commands.extend(self._render_updates(want, have))
176        else:
177            commands.extend(self._render_set_commands(want))
178        return commands
179
180    def _state_deleted(self, have):
181        """ The command generator when state is deleted
182
183        :rtype: A list
184        :returns: the commands necessary to remove the current configuration
185                  of the provided objects
186        """
187        commands = []
188        if have:
189            commands.extend(self._purge_attribs(have))
190        return commands
191
192    def _render_updates(self, want, have):
193        commands = []
194
195        temp_have_members = have.pop('members', None)
196        temp_want_members = want.pop('members', None)
197
198        updates = dict_diff(have, want)
199
200        if temp_have_members:
201            have['members'] = temp_have_members
202        if temp_want_members:
203            want['members'] = temp_want_members
204
205        commands.extend(self._add_bond_members(want, have))
206
207        if updates:
208            for key, value in iteritems(updates):
209                if value:
210                    if key == 'arp_monitor':
211                        commands.extend(
212                            self._add_arp_monitor(updates, key, want, have)
213                        )
214                    else:
215                        commands.append(self._compute_command(have['name'], key, str(value)))
216        return commands
217
218    def _render_set_commands(self, want):
219        commands = []
220        have = []
221
222        params = Lag_interfaces.params
223
224        for attrib in params:
225            value = want[attrib]
226            if value:
227                if attrib == 'arp_monitor':
228                    commands.extend(
229                        self._add_arp_monitor(want, attrib, want, have)
230                    )
231                elif attrib == 'members':
232                    commands.extend(
233                        self._add_bond_members(want, have)
234                    )
235                elif attrib != 'name':
236                    commands.append(
237                        self._compute_command(want['name'], attrib, value=str(value))
238                    )
239        return commands
240
241    def _purge_attribs(self, have):
242        commands = []
243        for item in Lag_interfaces.params:
244            if have.get(item):
245                if item == 'members':
246                    commands.extend(
247                        self._delete_bond_members(have)
248                    )
249                elif item != 'name':
250                    commands.append(
251                        self._compute_command(have['name'], attrib=item, remove=True)
252                    )
253        return commands
254
255    def _render_del_commands(self, want, have):
256        commands = []
257
258        params = Lag_interfaces.params
259        for attrib in params:
260            if attrib == 'members':
261                commands.extend(
262                    self._update_bond_members(attrib, want, have)
263                )
264            elif attrib == 'arp_monitor':
265                commands.extend(
266                    self._update_arp_monitor(attrib, want, have)
267                )
268            elif have.get(attrib) and not want.get(attrib):
269                commands.append(
270                    self._compute_command(have['name'], attrib, remove=True)
271                )
272        return commands
273
274    def _add_bond_members(self, want, have):
275        commands = []
276        diff_members = get_lst_diff_for_dicts(want, have, 'members')
277        if diff_members:
278            for key in diff_members:
279                commands.append(
280                    self._compute_command(key['member'], 'bond-group', want['name'], type='ethernet')
281                )
282        return commands
283
284    def _add_arp_monitor(self, updates, key, want, have):
285        commands = []
286        arp_monitor = updates.get(key) or {}
287        diff_targets = self._get_arp_monitor_target_diff(want, have, key, 'target')
288
289        if 'interval' in arp_monitor:
290            commands.append(
291                self._compute_command(
292                    key=want['name'] + ' arp-monitor', attrib='interval', value=str(arp_monitor['interval'])
293                )
294            )
295        if diff_targets:
296            for target in diff_targets:
297                commands.append(
298                    self._compute_command(key=want['name'] + ' arp-monitor', attrib='target', value=target)
299                )
300        return commands
301
302    def _delete_bond_members(self, have):
303        commands = []
304        for member in have['members']:
305            commands.append(
306                self._compute_command(
307                    member['member'], 'bond-group', have['name'], remove=True, type='ethernet'
308                )
309            )
310        return commands
311
312    def _update_arp_monitor(self, key, want, have):
313        commands = []
314        want_arp_target = []
315        have_arp_target = []
316        want_arp_monitor = want.get(key) or {}
317        have_arp_monitor = have.get(key) or {}
318
319        if want_arp_monitor and 'target' in want_arp_monitor:
320            want_arp_target = want_arp_monitor['target']
321
322        if have_arp_monitor and 'target' in have_arp_monitor:
323            have_arp_target = have_arp_monitor['target']
324
325        if 'interval' in have_arp_monitor and not want_arp_monitor:
326            commands.append(
327                self._compute_command(
328                    key=have['name'] + ' arp-monitor', attrib='interval', remove=True
329                )
330            )
331        if 'target' in have_arp_monitor:
332            target_diff = list_diff_have_only(want_arp_target, have_arp_target)
333            if target_diff:
334                for target in target_diff:
335                    commands.append(
336                        self._compute_command(
337                            key=have['name'] + ' arp-monitor', attrib='target', value=target, remove=True
338                        )
339                    )
340
341        return commands
342
343    def _update_bond_members(self, key, want, have):
344        commands = []
345        want_members = want.get(key) or []
346        have_members = have.get(key) or []
347
348        members_diff = list_diff_have_only(want_members, have_members)
349        if members_diff:
350            for member in members_diff:
351                commands.append(
352                    self._compute_command(
353                        member['member'], 'bond-group', have['name'], True, 'ethernet'
354                    )
355                )
356        return commands
357
358    def _get_arp_monitor_target_diff(self, want_list, have_list, dict_name, lst):
359        want_arp_target = []
360        have_arp_target = []
361
362        want_arp_monitor = want_list.get(dict_name) or {}
363        if want_arp_monitor and lst in want_arp_monitor:
364            want_arp_target = want_arp_monitor[lst]
365
366        if not have_list:
367            diff = want_arp_target
368        else:
369            have_arp_monitor = have_list.get(dict_name) or {}
370            if have_arp_monitor and lst in have_arp_monitor:
371                have_arp_target = have_arp_monitor[lst]
372
373            diff = list_diff_want_only(want_arp_target, have_arp_target)
374        return diff
375
376    def _compute_command(self, key, attrib, value=None, remove=False, type='bonding'):
377        if remove:
378            cmd = 'delete interfaces ' + type
379        else:
380            cmd = 'set interfaces ' + type
381        cmd += (' ' + key)
382        if attrib == 'arp_monitor':
383            attrib = 'arp-monitor'
384        elif attrib == 'hash_policy':
385            attrib = 'hash-policy'
386        cmd += (' ' + attrib)
387        if value:
388            cmd += (" '" + value + "'")
389        return cmd
390