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 exos_l2_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"""
13from __future__ import absolute_import, division, print_function
14__metaclass__ = type
15
16import json
17from copy import deepcopy
18from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ConfigBase
19from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import to_list, dict_diff
20from ansible_collections.community.network.plugins.module_utils.network.exos.facts.facts import Facts
21from ansible_collections.community.network.plugins.module_utils.network.exos.exos import send_requests
22
23
24class L2_interfaces(ConfigBase):
25    """
26    The exos_l2_interfaces class
27    """
28
29    gather_subset = [
30        '!all',
31        '!min',
32    ]
33
34    gather_network_resources = [
35        'l2_interfaces',
36    ]
37
38    L2_INTERFACE_NATIVE = {
39        "data": {
40            "openconfig-vlan:config": {
41                "interface-mode": "TRUNK",
42                "native-vlan": None,
43                "trunk-vlans": []
44            }
45        },
46        "method": "PATCH",
47        "path": None
48    }
49
50    L2_INTERFACE_TRUNK = {
51        "data": {
52            "openconfig-vlan:config": {
53                "interface-mode": "TRUNK",
54                "trunk-vlans": []
55            }
56        },
57        "method": "PATCH",
58        "path": None
59    }
60
61    L2_INTERFACE_ACCESS = {
62        "data": {
63            "openconfig-vlan:config": {
64                "interface-mode": "ACCESS",
65                "access-vlan": None
66            }
67        },
68        "method": "PATCH",
69        "path": None
70    }
71
72    L2_PATH = "/rest/restconf/data/openconfig-interfaces:interfaces/interface="
73
74    def __init__(self, module):
75        super(L2_interfaces, self).__init__(module)
76
77    def get_l2_interfaces_facts(self):
78        """ Get the 'facts' (the current configuration)
79
80        :rtype: A dictionary
81        :returns: The current configuration as a dictionary
82        """
83        facts, _warnings = Facts(self._module).get_facts(
84            self.gather_subset, self.gather_network_resources)
85        l2_interfaces_facts = facts['ansible_network_resources'].get(
86            'l2_interfaces')
87        if not l2_interfaces_facts:
88            return []
89        return l2_interfaces_facts
90
91    def execute_module(self):
92        """ Execute the module
93
94        :rtype: A dictionary
95        :returns: The result from module execution
96        """
97        result = {'changed': False}
98        warnings = list()
99        requests = list()
100
101        existing_l2_interfaces_facts = self.get_l2_interfaces_facts()
102        requests.extend(self.set_config(existing_l2_interfaces_facts))
103        if requests:
104            if not self._module.check_mode:
105                send_requests(self._module, requests=requests)
106            result['changed'] = True
107        result['requests'] = requests
108
109        changed_l2_interfaces_facts = self.get_l2_interfaces_facts()
110
111        result['before'] = existing_l2_interfaces_facts
112        if result['changed']:
113            result['after'] = changed_l2_interfaces_facts
114
115        result['warnings'] = warnings
116        return result
117
118    def set_config(self, existing_l2_interfaces_facts):
119        """ Collect the configuration from the args passed to the module,
120            collect the current configuration (as a dict from facts)
121
122        :rtype: A list
123        :returns: the requests necessary to migrate the current configuration
124                  to the desired configuration
125        """
126        want = self._module.params['config']
127        have = existing_l2_interfaces_facts
128        resp = self.set_state(want, have)
129        return to_list(resp)
130
131    def set_state(self, want, have):
132        """ Select the appropriate function based on the state provided
133
134        :param want: the desired configuration as a dictionary
135        :param have: the current configuration as a dictionary
136        :rtype: A list
137        :returns: the requests necessary to migrate the current configuration
138                  to the desired configuration
139        """
140        state = self._module.params['state']
141        if state == 'overridden':
142            requests = self._state_overridden(want, have)
143        elif state == 'deleted':
144            requests = self._state_deleted(want, have)
145        elif state == 'merged':
146            requests = self._state_merged(want, have)
147        elif state == 'replaced':
148            requests = self._state_replaced(want, have)
149        return requests
150
151    def _state_replaced(self, want, have):
152        """ The request generator when state is replaced
153
154        :rtype: A list
155        :returns: the requests necessary to migrate the current configuration
156                  to the desired configuration
157        """
158        requests = []
159        for w in want:
160            for h in have:
161                if w["name"] == h["name"]:
162                    if dict_diff(w, h):
163                        l2_request = self._update_patch_request(w, h)
164                        l2_request["data"] = json.dumps(l2_request["data"])
165                        requests.append(l2_request)
166                    break
167
168        return requests
169
170    def _state_overridden(self, want, have):
171        """ The request generator when state is overridden
172
173        :rtype: A list
174        :returns: the requests necessary to migrate the current configuration
175                  to the desired configuration
176        """
177        requests = []
178        have_copy = []
179        for w in want:
180            for h in have:
181                if w["name"] == h["name"]:
182                    if dict_diff(w, h):
183                        l2_request = self._update_patch_request(w, h)
184                        l2_request["data"] = json.dumps(l2_request["data"])
185                        requests.append(l2_request)
186                    have_copy.append(h)
187                    break
188
189        for h in have:
190            if h not in have_copy:
191                l2_delete = self._update_delete_request(h)
192                if l2_delete["path"]:
193                    l2_delete["data"] = json.dumps(l2_delete["data"])
194                    requests.append(l2_delete)
195
196        return requests
197
198    def _state_merged(self, want, have):
199        """ The request generator when state is merged
200
201        :rtype: A list
202        :returns: the requests necessary to merge the provided into
203                  the current configuration
204        """
205        requests = []
206        for w in want:
207            for h in have:
208                if w["name"] == h["name"]:
209                    if dict_diff(h, w):
210                        l2_request = self._update_patch_request(w, h)
211                        l2_request["data"] = json.dumps(l2_request["data"])
212                        requests.append(l2_request)
213                    break
214
215        return requests
216
217    def _state_deleted(self, want, have):
218        """ The request generator when state is deleted
219
220        :rtype: A list
221        :returns: the requests necessary to remove the current configuration
222                  of the provided objects
223        """
224        requests = []
225        if want:
226            for w in want:
227                for h in have:
228                    if w["name"] == h["name"]:
229                        l2_delete = self._update_delete_request(h)
230                        if l2_delete["path"]:
231                            l2_delete["data"] = json.dumps(l2_delete["data"])
232                            requests.append(l2_delete)
233                        break
234
235        else:
236            for h in have:
237                l2_delete = self._update_delete_request(h)
238                if l2_delete["path"]:
239                    l2_delete["data"] = json.dumps(l2_delete["data"])
240                    requests.append(l2_delete)
241
242        return requests
243
244    def _update_patch_request(self, want, have):
245
246        facts, _warnings = Facts(self._module).get_facts(
247            self.gather_subset, ['vlans', ])
248        vlans_facts = facts['ansible_network_resources'].get('vlans')
249
250        vlan_id = []
251
252        for vlan in vlans_facts:
253            vlan_id.append(vlan['vlan_id'])
254
255        if want.get("access"):
256            if want["access"]["vlan"] in vlan_id:
257                l2_request = deepcopy(self.L2_INTERFACE_ACCESS)
258                l2_request["data"]["openconfig-vlan:config"]["access-vlan"] = want["access"]["vlan"]
259                l2_request["path"] = self.L2_PATH + str(want["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
260            else:
261                self._module.fail_json(msg="VLAN %s does not exist" % (want["access"]["vlan"]))
262
263        elif want.get("trunk"):
264            if want["trunk"]["native_vlan"]:
265                if want["trunk"]["native_vlan"] in vlan_id:
266                    l2_request = deepcopy(self.L2_INTERFACE_NATIVE)
267                    l2_request["data"]["openconfig-vlan:config"]["native-vlan"] = want["trunk"]["native_vlan"]
268                    l2_request["path"] = self.L2_PATH + str(want["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
269                    for vlan in want["trunk"]["trunk_allowed_vlans"]:
270                        if int(vlan) in vlan_id:
271                            l2_request["data"]["openconfig-vlan:config"]["trunk-vlans"].append(int(vlan))
272                        else:
273                            self._module.fail_json(msg="VLAN %s does not exist" % (vlan))
274                else:
275                    self._module.fail_json(msg="VLAN %s does not exist" % (want["trunk"]["native_vlan"]))
276            else:
277                l2_request = deepcopy(self.L2_INTERFACE_TRUNK)
278                l2_request["path"] = self.L2_PATH + str(want["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
279                for vlan in want["trunk"]["trunk_allowed_vlans"]:
280                    if int(vlan) in vlan_id:
281                        l2_request["data"]["openconfig-vlan:config"]["trunk-vlans"].append(int(vlan))
282                    else:
283                        self._module.fail_json(msg="VLAN %s does not exist" % (vlan))
284        return l2_request
285
286    def _update_delete_request(self, have):
287
288        l2_request = deepcopy(self.L2_INTERFACE_ACCESS)
289
290        if have["access"] and have["access"]["vlan"] != 1 or have["trunk"] or not have["access"]:
291            l2_request["data"]["openconfig-vlan:config"]["access-vlan"] = 1
292            l2_request["path"] = self.L2_PATH + str(have["name"]) + "/openconfig-if-ethernet:ethernet/openconfig-vlan:switched-vlan/config"
293
294        return l2_request
295