1# -*- coding: utf-8 -*- 2# Copyright 2019 Red Hat 3# GNU General Public License v3.0+ 4# (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) 5""" 6The eos_l2_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 15__metaclass__ = type 16 17from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.cfg.base import ( 18 ConfigBase, 19) 20from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import ( 21 to_list, 22 param_list_to_dict, 23) 24from ansible_collections.arista.eos.plugins.module_utils.network.eos.facts.facts import ( 25 Facts, 26) 27from ansible_collections.arista.eos.plugins.module_utils.network.eos.utils.utils import ( 28 normalize_interface, 29 vlan_range_to_list, 30) 31 32 33class L2_interfaces(ConfigBase): 34 """ 35 The eos_l2_interfaces class 36 """ 37 38 gather_subset = ["!all", "!min"] 39 40 gather_network_resources = ["l2_interfaces"] 41 42 def get_l2_interfaces_facts(self, data=None): 43 """Get the 'facts' (the current configuration) 44 45 :rtype: A dictionary 46 :returns: The current configuration as a dictionary 47 """ 48 facts, _warnings = Facts(self._module).get_facts( 49 self.gather_subset, self.gather_network_resources, data=data 50 ) 51 l2_interfaces_facts = facts["ansible_network_resources"].get( 52 "l2_interfaces" 53 ) 54 if not l2_interfaces_facts: 55 return [] 56 return l2_interfaces_facts 57 58 def execute_module(self): 59 """Execute the module 60 61 :rtype: A dictionary 62 :returns: The result from module execution 63 """ 64 result = {"changed": False} 65 commands = list() 66 warnings = list() 67 68 if self.state in self.ACTION_STATES: 69 existing_l2_interfaces_facts = self.get_l2_interfaces_facts() 70 else: 71 existing_l2_interfaces_facts = [] 72 73 if self.state in self.ACTION_STATES or self.state == "rendered": 74 commands.extend(self.set_config(existing_l2_interfaces_facts)) 75 76 if commands and self.state in self.ACTION_STATES: 77 if not self._module.check_mode: 78 self._connection.edit_config(commands) 79 result["changed"] = True 80 81 if self.state in self.ACTION_STATES: 82 result["commands"] = commands 83 84 if self.state in self.ACTION_STATES or self.state == "gathered": 85 changed_l2_interfaces_facts = self.get_l2_interfaces_facts() 86 87 elif self.state == "rendered": 88 result["rendered"] = commands 89 90 elif self.state == "parsed": 91 running_config = self._module.params["running_config"] 92 if not running_config: 93 self._module.fail_json( 94 msg="value of running_config parameter must not be empty for state parsed" 95 ) 96 result["parsed"] = self.get_l2_interfaces_facts( 97 data=running_config 98 ) 99 100 if self.state in self.ACTION_STATES: 101 result["before"] = existing_l2_interfaces_facts 102 if result["changed"]: 103 result["after"] = changed_l2_interfaces_facts 104 105 elif self.state == "gathered": 106 result["gathered"] = changed_l2_interfaces_facts 107 108 result["warnings"] = warnings 109 return result 110 111 def set_config(self, existing_l2_interfaces_facts): 112 """Collect the configuration from the args passed to the module, 113 collect the current configuration (as a dict from facts) 114 115 :rtype: A list 116 :returns: the commands necessary to migrate the current configuration 117 to the desired configuration 118 """ 119 want = self._module.params["config"] 120 have = existing_l2_interfaces_facts 121 resp = self.set_state(want, have) 122 return to_list(resp) 123 124 def set_state(self, want, have): 125 """Select the appropriate function based on the state provided 126 127 :param want: the desired configuration as a dictionary 128 :param have: the current configuration as a dictionary 129 :rtype: A list 130 :returns: the commands necessary to migrate the current configuration 131 to the desired configuration 132 """ 133 state = self._module.params["state"] 134 if ( 135 state in ("merged", "replaced", "overridden", "rendered") 136 and not want 137 ): 138 self._module.fail_json( 139 msg="value of config parameter must not be empty for state {0}".format( 140 state 141 ) 142 ) 143 want = param_list_to_dict(want) 144 have = param_list_to_dict(have) 145 commands = [] 146 if state == "overridden": 147 commands = self._state_overridden(want, have) 148 elif state == "deleted": 149 commands = self._state_deleted(want, have) 150 elif state == "merged" or state == "rendered": 151 commands = self._state_merged(want, have) 152 elif state == "replaced": 153 commands = self._state_replaced(want, have) 154 return commands 155 156 @staticmethod 157 def _state_replaced(want, have): 158 """The command generator when state is replaced 159 160 :rtype: A list 161 :returns: the commands necessary to migrate the current configuration 162 to the desired configuration 163 """ 164 commands = [] 165 for key, desired in want.items(): 166 interface_name = normalize_interface(key) 167 if interface_name in have: 168 extant = have[interface_name] 169 else: 170 extant = dict() 171 172 intf_commands = set_interface(desired, extant) 173 intf_commands.extend(clear_interface(desired, extant)) 174 175 if intf_commands: 176 commands.append("interface {0}".format(interface_name)) 177 commands.extend(intf_commands) 178 179 return commands 180 181 @staticmethod 182 def _state_overridden(want, have): 183 """The command generator when state is overridden 184 185 :rtype: A list 186 :returns: the commands necessary to migrate the current configuration 187 to the desired configuration 188 """ 189 commands = [] 190 for key, extant in have.items(): 191 if key in want: 192 desired = want[key] 193 else: 194 desired = dict() 195 196 intf_commands = set_interface(desired, extant) 197 intf_commands.extend(clear_interface(desired, extant)) 198 199 if intf_commands: 200 commands.append("interface {0}".format(key)) 201 commands.extend(intf_commands) 202 203 return commands 204 205 @staticmethod 206 def _state_merged(want, have): 207 """The command generator when state is merged 208 209 :rtype: A list 210 :returns: the commands necessary to merge the provided into 211 the current configuration 212 """ 213 commands = [] 214 for key, desired in want.items(): 215 interface_name = normalize_interface(key) 216 if interface_name in have: 217 extant = have[interface_name] 218 else: 219 extant = dict() 220 221 intf_commands = set_interface(desired, extant) 222 223 if intf_commands: 224 commands.append("interface {0}".format(interface_name)) 225 commands.extend(intf_commands) 226 227 return commands 228 229 @staticmethod 230 def _state_deleted(want, have): 231 """The command generator when state is deleted 232 233 :rtype: A list 234 :returns: the commands necessary to remove the current configuration 235 of the provided objects 236 """ 237 commands = [] 238 for key in want: 239 desired = dict() 240 if key in have: 241 extant = have[key] 242 else: 243 continue 244 intf_commands = clear_interface(desired, extant) 245 246 if intf_commands: 247 commands.append("interface {0}".format(key)) 248 commands.extend(intf_commands) 249 250 return commands 251 252 253def set_interface(want, have): 254 commands = [] 255 256 want_mode = want.get("mode") 257 if want_mode and want_mode != have.get("mode"): 258 commands.append("switchport mode {0}".format(want_mode)) 259 260 wants_access = want.get("access") 261 if wants_access: 262 access_vlan = wants_access.get("vlan") 263 if access_vlan and access_vlan != have.get("access", {}).get("vlan"): 264 commands.append("switchport access vlan {0}".format(access_vlan)) 265 266 wants_trunk = want.get("trunk") 267 if wants_trunk: 268 allowed_vlans = [] 269 has_allowed_vlans = {} 270 want_allowed_vlans = {} 271 has_trunk = have.get("trunk", {}) 272 native_vlan = wants_trunk.get("native_vlan") 273 if native_vlan and native_vlan != has_trunk.get("native_vlan"): 274 commands.append( 275 "switchport trunk native vlan {0}".format(native_vlan) 276 ) 277 for con in [want, have]: 278 expand_trunk_allowed_vlans(con) 279 want_allowed_vlans = want["trunk"].get("trunk_allowed_vlans") 280 if has_trunk: 281 has_allowed_vlans = has_trunk.get("trunk_allowed_vlans") 282 283 if want_allowed_vlans and has_allowed_vlans: 284 allowed_vlans = list( 285 set(want_allowed_vlans.split(",")) 286 - set(has_allowed_vlans.split(",")) 287 ) 288 elif want_allowed_vlans: 289 allowed_vlans = want_allowed_vlans.split(",") 290 if allowed_vlans: 291 allowed_vlans.sort() 292 allowed_vlans = ",".join( 293 ["{0}".format(vlan) for vlan in allowed_vlans] 294 ) 295 if has_allowed_vlans: 296 commands.append( 297 "switchport trunk allowed vlan add {0}".format( 298 allowed_vlans 299 ) 300 ) 301 else: 302 commands.append( 303 "switchport trunk allowed vlan {0}".format(allowed_vlans) 304 ) 305 return commands 306 307 308def expand_trunk_allowed_vlans(want): 309 if not want: 310 return None 311 if want.get("trunk"): 312 if "trunk_allowed_vlans" in want["trunk"]: 313 allowed_vlans = vlan_range_to_list( 314 want["trunk"]["trunk_allowed_vlans"] 315 ) 316 vlans_list = [str(num) for num in sorted(allowed_vlans)] 317 want["trunk"]["trunk_allowed_vlans"] = ",".join(vlans_list) 318 319 320def clear_interface(want, have): 321 commands = [] 322 323 if "mode" in have and want.get("mode") is None: 324 commands.append("no switchport mode") 325 326 if "access" in have and not want.get("access"): 327 commands.append("no switchport access vlan") 328 329 has_trunk = have.get("trunk") or {} 330 wants_trunk = want.get("trunk") or {} 331 if ( 332 "trunk_allowed_vlans" in has_trunk 333 and "trunk_allowed_vlans" not in wants_trunk 334 ): 335 commands.append("no switchport trunk allowed vlan") 336 if ( 337 "trunk_allowed_vlans" in has_trunk 338 and "trunk_allowed_vlans" in wants_trunk 339 ): 340 for con in [want, have]: 341 expand_trunk_allowed_vlans(con) 342 want_allowed_vlans = want["trunk"].get("trunk_allowed_vlans") 343 has_allowed_vlans = has_trunk.get("trunk_allowed_vlans") 344 allowed_vlans = list( 345 set(has_allowed_vlans.split(",")) 346 - set(want_allowed_vlans.split(",")) 347 ) 348 if allowed_vlans: 349 allowed_vlans = ",".join( 350 ["{0}".format(vlan) for vlan in allowed_vlans] 351 ) 352 353 commands.append( 354 "switchport trunk allowed vlan remove {0}".format( 355 allowed_vlans 356 ) 357 ) 358 359 if "native_vlan" in has_trunk and "native_vlan" not in wants_trunk: 360 commands.append("no switchport trunk native vlan") 361 return commands 362