1#
2# -*- coding: utf-8 -*-
3# Copyright 2019 Red Hat Inc.
4# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
5"""
6The ios_interfaces class
7It is in this file where the current configuration (as dict)
8is compared to the provided configuration (as dict) and the command set
9necessary to bring the current configuration to it's desired end-state is
10created
11"""
12
13from __future__ import absolute_import, division, print_function
14__metaclass__ = type
15
16
17from ansible.module_utils.network.common.cfg.base import ConfigBase
18from ansible.module_utils.network.common.utils import to_list
19from ansible.module_utils.network.ios.facts.facts import Facts
20from ansible.module_utils.network.ios.utils.utils import get_interface_type, dict_to_set
21from ansible.module_utils.network.ios.utils.utils import remove_command_from_config_list, add_command_to_config_list
22from ansible.module_utils.network.ios.utils.utils import filter_dict_having_none_value, remove_duplicate_interface
23
24
25class Interfaces(ConfigBase):
26    """
27    The ios_interfaces class
28    """
29
30    gather_subset = [
31        '!all',
32        '!min',
33    ]
34
35    gather_network_resources = [
36        'interfaces',
37    ]
38
39    params = ('description', 'mtu', 'speed', 'duplex')
40
41    def __init__(self, module):
42        super(Interfaces, self).__init__(module)
43
44    def get_interfaces_facts(self):
45        """ Get the 'facts' (the current configuration)
46
47        :rtype: A dictionary
48        :returns: The current configuration as a dictionary
49        """
50        facts, _warnings = Facts(self._module).get_facts(self.gather_subset, self.gather_network_resources)
51        interfaces_facts = facts['ansible_network_resources'].get('interfaces')
52        if not interfaces_facts:
53            return []
54
55        return interfaces_facts
56
57    def execute_module(self):
58        """ Execute the module
59
60        :rtype: A dictionary
61        :returns: The result from moduel execution
62        """
63        result = {'changed': False}
64        commands = list()
65        warnings = list()
66
67        existing_interfaces_facts = self.get_interfaces_facts()
68        commands.extend(self.set_config(existing_interfaces_facts))
69
70        if commands:
71            if not self._module.check_mode:
72                self._connection.edit_config(commands)
73            result['changed'] = True
74        result['commands'] = commands
75
76        changed_interfaces_facts = self.get_interfaces_facts()
77
78        result['before'] = existing_interfaces_facts
79        if result['changed']:
80            result['after'] = changed_interfaces_facts
81        result['warnings'] = warnings
82
83        return result
84
85    def set_config(self, existing_interfaces_facts):
86        """ Collect the configuration from the args passed to the module,
87            collect the current configuration (as a dict from facts)
88
89        :rtype: A list
90        :returns: the commands necessary to migrate the current configuration
91                  to the deisred configuration
92        """
93        want = self._module.params['config']
94        have = existing_interfaces_facts
95        resp = self.set_state(want, have)
96        return to_list(resp)
97
98    def set_state(self, want, have):
99        """ Select the appropriate function based on the state provided
100
101        :param want: the desired configuration as a dictionary
102        :param have: the current configuration as a dictionary
103        :rtype: A list
104        :returns: the commands necessary to migrate the current configuration
105                  to the deisred configuration
106        """
107        commands = []
108
109        state = self._module.params['state']
110        if state in ('overridden', 'merged', 'replaced') and not want:
111            self._module.fail_json(msg='value of config parameter must not be empty for state {0}'.format(state))
112
113        if state == 'overridden':
114            commands = self._state_overridden(want, have)
115        elif state == 'deleted':
116            commands = self._state_deleted(want, have)
117        elif state == 'merged':
118            commands = self._state_merged(want, have)
119        elif state == 'replaced':
120            commands = self._state_replaced(want, have)
121
122        return commands
123
124    def _state_replaced(self, want, have):
125        """ The command generator when state is replaced
126
127        :param want: the desired configuration as a dictionary
128        :param have: the current configuration as a dictionary
129        :param interface_type: interface type
130        :rtype: A list
131        :returns: the commands necessary to migrate the current configuration
132                  to the deisred configuration
133        """
134        commands = []
135
136        for interface in want:
137            for each in have:
138                if each['name'] == interface['name']:
139                    break
140                elif interface['name'] in each['name']:
141                    break
142            else:
143                # configuring non-existing interface
144                commands.extend(self._set_config(interface, dict()))
145                continue
146            have_dict = filter_dict_having_none_value(interface, each)
147            commands.extend(self._clear_config(dict(), have_dict))
148            commands.extend(self._set_config(interface, each))
149        # Remove the duplicate interface call
150        commands = remove_duplicate_interface(commands)
151
152        return commands
153
154    def _state_overridden(self, want, have):
155        """ The command generator when state is overridden
156
157        :param want: the desired configuration as a dictionary
158        :param obj_in_have: the current configuration as a dictionary
159        :rtype: A list
160        :returns: the commands necessary to migrate the current configuration
161                  to the desired configuration
162        """
163        commands = []
164
165        for each in have:
166            for interface in want:
167                count = 0
168                if each['name'] == interface['name']:
169                    break
170                elif interface['name'] in each['name']:
171                    break
172                count += 1
173            else:
174                # We didn't find a matching desired state, which means we can
175                # pretend we recieved an empty desired state.
176                interface = dict(name=each['name'])
177                commands.extend(self._clear_config(interface, each))
178                continue
179            have_dict = filter_dict_having_none_value(interface, each)
180            commands.extend(self._clear_config(dict(), have_dict))
181            commands.extend(self._set_config(interface, each))
182            # as the pre-existing interface are now configured by
183            # above set_config call, deleting the respective
184            # interface entry from the want list
185            del want[count]
186
187        # Iterating through want list which now only have new interfaces to be
188        # configured
189        for each in want:
190            commands.extend(self._set_config(each, dict()))
191        # Remove the duplicate interface call
192        commands = remove_duplicate_interface(commands)
193
194        return commands
195
196    def _state_merged(self, want, have):
197        """ The command generator when state is merged
198
199        :param want: the additive configuration as a dictionary
200        :param obj_in_have: the current configuration as a dictionary
201        :rtype: A list
202        :returns: the commands necessary to merge the provided into
203                  the current configuration
204        """
205        commands = []
206
207        for interface in want:
208            for each in have:
209                if each['name'] == interface['name']:
210                    break
211            else:
212                # configuring non-existing interface
213                commands.extend(self._set_config(interface, dict()))
214                continue
215            commands.extend(self._set_config(interface, each))
216
217        return commands
218
219    def _state_deleted(self, want, have):
220        """ The command generator when state is deleted
221
222        :param want: the objects from which the configuration should be removed
223        :param obj_in_have: the current configuration as a dictionary
224        :param interface_type: interface type
225        :rtype: A list
226        :returns: the commands necessary to remove the current configuration
227                  of the provided objects
228        """
229        commands = []
230
231        if want:
232            for interface in want:
233                for each in have:
234                    if each['name'] == interface['name']:
235                        break
236                else:
237                    continue
238                interface = dict(name=interface['name'])
239                commands.extend(self._clear_config(interface, each))
240        else:
241            for each in have:
242                want = dict()
243                commands.extend(self._clear_config(want, each))
244
245        return commands
246
247    def _set_config(self, want, have):
248        # Set the interface config based on the want and have config
249        commands = []
250        interface = 'interface ' + want['name']
251
252        # Get the diff b/w want and have
253        want_dict = dict_to_set(want)
254        have_dict = dict_to_set(have)
255        diff = want_dict - have_dict
256
257        if diff:
258            diff = dict(diff)
259            for item in self.params:
260                if diff.get(item):
261                    cmd = item + ' ' + str(want.get(item))
262                    add_command_to_config_list(interface, cmd, commands)
263            if diff.get('enabled'):
264                add_command_to_config_list(interface, 'no shutdown', commands)
265            elif diff.get('enabled') is False:
266                add_command_to_config_list(interface, 'shutdown', commands)
267
268        return commands
269
270    def _clear_config(self, want, have):
271        # Delete the interface config based on the want and have config
272        commands = []
273
274        if want.get('name'):
275            interface_type = get_interface_type(want['name'])
276            interface = 'interface ' + want['name']
277        else:
278            interface_type = get_interface_type(have['name'])
279            interface = 'interface ' + have['name']
280
281        if have.get('description') and want.get('description') != have.get('description'):
282            remove_command_from_config_list(interface, 'description', commands)
283        if not have.get('enabled') and want.get('enabled') != have.get('enabled'):
284            # if enable is False set enable as True which is the default behavior
285            remove_command_from_config_list(interface, 'shutdown', commands)
286
287        if interface_type.lower() == 'gigabitethernet':
288            if have.get('speed') and have.get('speed') != 'auto' and want.get('speed') != have.get('speed'):
289                remove_command_from_config_list(interface, 'speed', commands)
290            if have.get('duplex') and have.get('duplex') != 'auto' and want.get('duplex') != have.get('duplex'):
291                remove_command_from_config_list(interface, 'duplex', commands)
292            if have.get('mtu') and want.get('mtu') != have.get('mtu'):
293                remove_command_from_config_list(interface, 'mtu', commands)
294
295        return commands
296