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 ios_vlans 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
17
18from ansible.module_utils.network.common.cfg.base import ConfigBase
19from ansible.module_utils.network.common.utils import to_list
20from ansible.module_utils.network.ios.facts.facts import Facts
21from ansible.module_utils.network.ios.utils.utils import dict_to_set
22
23
24class Vlans(ConfigBase):
25    """
26    The ios_vlans class
27    """
28
29    gather_subset = [
30        '!all',
31        '!min',
32    ]
33
34    gather_network_resources = [
35        'vlans',
36    ]
37
38    def __init__(self, module):
39        super(Vlans, self).__init__(module)
40
41    def get_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, self.gather_network_resources)
48        interfaces_facts = facts['ansible_network_resources'].get('vlans')
49        if not interfaces_facts:
50            return []
51        return interfaces_facts
52
53    def execute_module(self):
54        """ Execute the module
55
56        :rtype: A dictionary
57        :returns: The result from module execution
58        """
59        result = {'changed': False}
60        commands = list()
61        warnings = list()
62
63        existing_interfaces_facts = self.get_interfaces_facts()
64        commands.extend(self.set_config(existing_interfaces_facts))
65        if commands:
66            if not self._module.check_mode:
67                self._connection.edit_config(commands)
68            result['changed'] = True
69        result['commands'] = commands
70
71        changed_interfaces_facts = self.get_interfaces_facts()
72
73        result['before'] = existing_interfaces_facts
74        if result['changed']:
75            result['after'] = changed_interfaces_facts
76
77        result['warnings'] = warnings
78        return result
79
80    def set_config(self, existing_interfaces_facts):
81        """ Collect the configuration from the args passed to the module,
82            collect the current configuration (as a dict from facts)
83
84        :rtype: A list
85        :returns: the commands necessary to migrate the current configuration
86                  to the desired configuration
87        """
88        want = self._module.params['config']
89        have = existing_interfaces_facts
90        resp = self.set_state(want, have)
91        return to_list(resp)
92
93    def set_state(self, want, have):
94        """ Select the appropriate function based on the state provided
95
96        :param want: the desired configuration as a dictionary
97        :param have: the current configuration as a dictionary
98        :rtype: A list
99        :returns: the commands necessary to migrate the current configuration
100                  to the desired configuration
101        """
102        state = self._module.params['state']
103        if state in ('overridden', 'merged', 'replaced') and not want:
104            self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(state))
105
106        if state == 'overridden':
107            commands = self._state_overridden(want, have, state)
108        elif state == 'deleted':
109            commands = self._state_deleted(want, have, state)
110        elif state == 'merged':
111            commands = self._state_merged(want, have)
112        elif state == 'replaced':
113            commands = self._state_replaced(want, have)
114        return commands
115
116    def _state_replaced(self, want, have):
117        """ The command generator when state is replaced
118
119        :rtype: A list
120        :returns: the commands necessary to migrate the current configuration
121                  to the desired configuration
122        """
123        commands = []
124
125        check = False
126        for each in want:
127            for every in have:
128                if every['vlan_id'] == each['vlan_id']:
129                    check = True
130                    break
131            else:
132                continue
133            if check:
134                commands.extend(self._set_config(each, every))
135            else:
136                commands.extend(self._set_config(each, dict()))
137
138        return commands
139
140    def _state_overridden(self, want, have, state):
141        """ The command generator when state is overridden
142
143        :rtype: A list
144        :returns: the commands necessary to migrate the current configuration
145                  to the desired configuration
146        """
147        commands = []
148
149        want_local = want
150        for each in have:
151            count = 0
152            for every in want_local:
153                if each['vlan_id'] == every['vlan_id']:
154                    break
155                count += 1
156            else:
157                # We didn't find a matching desired state, which means we can
158                # pretend we recieved an empty desired state.
159                commands.extend(self._clear_config(every, each, state))
160                continue
161            commands.extend(self._set_config(every, each))
162            # as the pre-existing VLAN are now configured by
163            # above set_config call, deleting the respective
164            # VLAN entry from the want_local list
165            del want_local[count]
166
167        # Iterating through want_local list which now only have new VLANs to be
168        # configured
169        for each in want_local:
170            commands.extend(self._set_config(each, dict()))
171
172        return commands
173
174    def _state_merged(self, want, have):
175        """ The command generator when state is merged
176
177        :rtype: A list
178        :returns: the commands necessary to merge the provided into
179                  the current configuration
180        """
181        commands = []
182
183        check = False
184        for each in want:
185            for every in have:
186                if each.get('vlan_id') == every.get('vlan_id'):
187                    check = True
188                    break
189                else:
190                    continue
191            if check:
192                commands.extend(self._set_config(each, every))
193            else:
194                commands.extend(self._set_config(each, dict()))
195
196        return commands
197
198    def _state_deleted(self, want, have, state):
199        """ The command generator when state is deleted
200
201        :rtype: A list
202        :returns: the commands necessary to remove the current configuration
203                  of the provided objects
204        """
205        commands = []
206
207        if want:
208            check = False
209            for each in want:
210                for every in have:
211                    if each.get('vlan_id') == every.get('vlan_id'):
212                        check = True
213                        break
214                    else:
215                        check = False
216                        continue
217                if check:
218                    commands.extend(self._clear_config(each, every, state))
219        else:
220            for each in have:
221                commands.extend(self._clear_config(dict(), each, state))
222
223        return commands
224
225    def remove_command_from_config_list(self, vlan, cmd, commands):
226        if vlan not in commands and cmd != 'vlan':
227            commands.insert(0, vlan)
228        elif cmd == 'vlan':
229            commands.append('no %s' % vlan)
230            return commands
231        commands.append('no %s' % cmd)
232        return commands
233
234    def add_command_to_config_list(self, vlan_id, cmd, commands):
235        if vlan_id not in commands:
236            commands.insert(0, vlan_id)
237        if cmd not in commands:
238            commands.append(cmd)
239
240    def _set_config(self, want, have):
241        # Set the interface config based on the want and have config
242        commands = []
243        vlan = 'vlan {0}'.format(want.get('vlan_id'))
244
245        # Get the diff b/w want n have
246        want_dict = dict_to_set(want)
247        have_dict = dict_to_set(have)
248        diff = want_dict - have_dict
249
250        if diff:
251            name = dict(diff).get('name')
252            state = dict(diff).get('state')
253            shutdown = dict(diff).get('shutdown')
254            mtu = dict(diff).get('mtu')
255            remote_span = dict(diff).get('remote_span')
256            if name:
257                cmd = 'name {0}'.format(name)
258                self.add_command_to_config_list(vlan, cmd, commands)
259            if state:
260                cmd = 'state {0}'.format(state)
261                self.add_command_to_config_list(vlan, cmd, commands)
262            if mtu:
263                cmd = 'mtu {0}'.format(mtu)
264                self.add_command_to_config_list(vlan, cmd, commands)
265            if remote_span:
266                self.add_command_to_config_list(vlan, 'remote-span', commands)
267            if shutdown == 'enabled':
268                self.add_command_to_config_list(vlan, 'shutdown', commands)
269            elif shutdown == 'disabled':
270                self.add_command_to_config_list(vlan, 'no shutdown', commands)
271
272        return commands
273
274    def _clear_config(self, want, have, state):
275        # Delete the interface config based on the want and have config
276        commands = []
277        vlan = 'vlan {0}'.format(have.get('vlan_id'))
278
279        if have.get('vlan_id') and 'default' not in have.get('name')\
280                and (have.get('vlan_id') != want.get('vlan_id') or state == 'deleted'):
281            self.remove_command_from_config_list(vlan, 'vlan', commands)
282        elif 'default' not in have.get('name'):
283            if have.get('mtu') != want.get('mtu'):
284                self.remove_command_from_config_list(vlan, 'mtu', commands)
285            if have.get('remote_span') != want.get('remote_span') and want.get('remote_span'):
286                self.remove_command_from_config_list(vlan, 'remote-span', commands)
287            if have.get('shutdown') != want.get('shutdown') and want.get('shutdown'):
288                self.remove_command_from_config_list(vlan, 'shutdown', commands)
289            if have.get('state') != want.get('state') and want.get('state'):
290                self.remove_command_from_config_list(vlan, 'state', commands)
291
292        return commands
293