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