Repository URL to install this package:
|
Version:
6.0.0 ▾
|
#!/usr/bin/python
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# Ansible is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
#
from __future__ import absolute_import, division, print_function
__metaclass__ = type
DOCUMENTATION = """
module: ios_l2_interface
extends_documentation_fragment:
- cisco.ios.ios
short_description: (deprecated, removed after 2022-06-01) Manage Layer-2 interface
on Cisco IOS devices.
description:
- This module provides declarative management of Layer-2 interfaces on Cisco IOS devices.
version_added: 1.0.0
deprecated:
alternative: ios_l2_interfaces
why: Newer and updated modules released with more functionality in Ansible 2.9
removed_at_date: '2022-06-01'
author:
- Nathaniel Case (@Qalthos)
options:
name:
description:
- Full name of the interface excluding any logical unit number, i.e. GigabitEthernet0/1.
aliases:
- interface
type: str
mode:
description:
- Mode in which interface needs to be configured.
choices:
- access
- trunk
type: str
access_vlan:
description:
- Configure given VLAN in access port. If C(mode=access), used as the access VLAN
ID.
type: str
trunk_vlans:
description:
- List of VLANs to be configured in trunk port. If C(mode=trunk), used as the
VLAN range to ADD or REMOVE from the trunk.
type: str
native_vlan:
description:
- Native VLAN to be configured in trunk port. If C(mode=trunk), used as the trunk
native VLAN ID.
type: str
trunk_allowed_vlans:
description:
- List of allowed VLANs in a given trunk port. If C(mode=trunk), these are the
only VLANs that will be configured on the trunk, i.e. "2-10,15".
type: str
aggregate:
description:
- List of Layer-2 interface definitions.
type: list
elements: dict
suboptions:
name:
description:
- Full name of the interface excluding any logical unit number, i.e. GigabitEthernet0/1.
aliases:
- interface
type: str
mode:
description:
- Mode in which interface needs to be configured.
choices:
- access
- trunk
type: str
access_vlan:
description:
- Configure given VLAN in access port. If C(mode=access), used as the access VLAN
ID.
type: str
trunk_vlans:
description:
- List of VLANs to be configured in trunk port. If C(mode=trunk), used as the
VLAN range to ADD or REMOVE from the trunk.
type: str
native_vlan:
description:
- Native VLAN to be configured in trunk port. If C(mode=trunk), used as the trunk
native VLAN ID.
type: str
trunk_allowed_vlans:
description:
- List of allowed VLANs in a given trunk port. If C(mode=trunk), these are the
only VLANs that will be configured on the trunk, i.e. "2-10,15".
type: str
state:
description:
- Manage the state of the Layer-2 Interface configuration.
choices:
- present
- absent
- unconfigured
type: str
state:
description:
- Manage the state of the Layer-2 Interface configuration.
default: present
choices:
- present
- absent
- unconfigured
type: str
"""
EXAMPLES = """
- name: Ensure GigabitEthernet0/5 is in its default l2 interface state
ios.ios_l2_interface:
name: GigabitEthernet0/5
state: unconfigured
- name: Ensure GigabitEthernet0/5 is configured for access vlan 20
ios.ios_l2_interface:
name: GigabitEthernet0/5
mode: access
access_vlan: 20
- name: Ensure GigabitEthernet0/5 only has vlans 5-10 as trunk vlans
ios.ios_l2_interface:
name: GigabitEthernet0/5
mode: trunk
native_vlan: 10
trunk_allowed_vlans: 5-10
- name: Ensure GigabitEthernet0/5 is a trunk port and ensure 2-50 are being tagged
(doesn't mean others aren't also being tagged)
ios.ios_l2_interface:
name: GigabitEthernet0/5
mode: trunk
native_vlan: 10
trunk_vlans: 2-50
- name: Ensure these VLANs are not being tagged on the trunk
ios.ios_l2_interface:
name: GigabitEthernet0/5
mode: trunk
trunk_vlans: 51-4094
state: absent
"""
RETURN = """
commands:
description: The list of configuration mode commands to send to the device
returned: always, except for the platforms that use Netconf transport to manage the device.
type: list
sample:
- interface GigabitEthernet0/5
- switchport access vlan 20
"""
import re
from copy import deepcopy
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.ansible.netcommon.plugins.module_utils.network.common.utils import (
remove_default_spec,
)
from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
load_config,
run_commands,
)
from ansible_collections.cisco.ios.plugins.module_utils.network.ios.ios import (
ios_argument_spec,
)
def get_interface_type(interface):
intf_type = "unknown"
if interface.upper()[:2] in (
"ET",
"GI",
"FA",
"TE",
"FO",
"FI",
"HU",
"TWE",
"TW",
):
intf_type = "ethernet"
elif interface.upper().startswith("VL"):
intf_type = "svi"
elif interface.upper().startswith("LO"):
intf_type = "loopback"
elif interface.upper()[:2] in ("MG", "MA"):
intf_type = "management"
elif interface.upper().startswith("PO"):
intf_type = "portchannel"
elif interface.upper().startswith("NV"):
intf_type = "nve"
return intf_type
def is_switchport(name, module):
intf_type = get_interface_type(name)
if intf_type in ("ethernet", "portchannel"):
config = run_commands(
module, ["show interface {0} switchport".format(name)]
)[0]
match = re.search("Switchport: Enabled", config)
return bool(match)
return False
def interface_is_portchannel(name, module):
if get_interface_type(name) == "ethernet":
config = run_commands(module, ["show run interface {0}".format(name)])[
0
]
if any(c in config for c in ["channel group", "channel-group"]):
return True
return False
def get_switchport(name, module):
config = run_commands(
module, ["show interface {0} switchport".format(name)]
)[0]
mode = re.search("Administrative Mode: (?:.* )?(\\w+)$", config, re.M)
access = re.search("Access Mode VLAN: (\\d+)", config)
native = re.search("Trunking Native Mode VLAN: (\\d+)", config)
trunk = re.search("Trunking VLANs Enabled: (.+)$", config, re.M)
if mode:
mode = mode.group(1)
if access:
access = access.group(1)
if native:
native = native.group(1)
if trunk:
trunk = trunk.group(1)
if trunk == "ALL":
trunk = "1-4094"
switchport_config = {
"interface": name,
"mode": mode,
"access_vlan": access,
"native_vlan": native,
"trunk_vlans": trunk,
}
return switchport_config
def remove_switchport_config_commands(name, existing, proposed, module):
mode = proposed.get("mode")
commands = []
command = None
if mode == "access":
av_check = existing.get("access_vlan") == proposed.get("access_vlan")
if av_check:
command = "no switchport access vlan {0}".format(
existing.get("access_vlan")
)
commands.append(command)
elif mode == "trunk":
# Supported Remove Scenarios for trunk_vlans_list
# 1) Existing: 1,2,3 Proposed: 1,2,3 - Remove all
# 2) Existing: 1,2,3 Proposed: 1,2 - Remove 1,2 Leave 3
# 3) Existing: 1,2,3 Proposed: 2,3 - Remove 2,3 Leave 1
# 4) Existing: 1,2,3 Proposed: 4,5,6 - None removed.
# 5) Existing: None Proposed: 1,2,3 - None removed.
existing_vlans = existing.get("trunk_vlans_list")
proposed_vlans = proposed.get("trunk_vlans_list")
vlans_to_remove = set(proposed_vlans).intersection(existing_vlans)
if vlans_to_remove:
proposed_allowed_vlans = proposed.get("trunk_allowed_vlans")
remove_trunk_allowed_vlans = proposed.get(
"trunk_vlans", proposed_allowed_vlans
)
command = "switchport trunk allowed vlan remove {0}".format(
remove_trunk_allowed_vlans
)
commands.append(command)
native_check = existing.get("native_vlan") == proposed.get(
"native_vlan"
)
if native_check and proposed.get("native_vlan"):
command = "no switchport trunk native vlan {0}".format(
existing.get("native_vlan")
)
commands.append(command)
if commands:
commands.insert(0, "interface " + name)
return commands
def get_switchport_config_commands(name, existing, proposed, module):
"""Gets commands required to config a given switchport interface"""
proposed_mode = proposed.get("mode")
existing_mode = existing.get("mode")
commands = []
command = None
if proposed_mode != existing_mode:
if proposed_mode == "trunk":
command = "switchport mode trunk"
elif proposed_mode == "access":
command = "switchport mode access"
if command:
commands.append(command)
if proposed_mode == "access":
av_check = str(existing.get("access_vlan")) == str(
proposed.get("access_vlan")
)
if not av_check:
command = "switchport access vlan {0}".format(
proposed.get("access_vlan")
)
commands.append(command)
elif proposed_mode == "trunk":
tv_check = existing.get("trunk_vlans_list") == proposed.get(
"trunk_vlans_list"
)
if not tv_check:
if proposed.get("allowed"):
command = "switchport trunk allowed vlan {0}".format(
proposed.get("trunk_allowed_vlans")
)
commands.append(command)
else:
existing_vlans = existing.get("trunk_vlans_list")
proposed_vlans = proposed.get("trunk_vlans_list")
vlans_to_add = set(proposed_vlans).difference(existing_vlans)
if vlans_to_add:
command = "switchport trunk allowed vlan add {0}".format(
proposed.get("trunk_vlans")
)
commands.append(command)
native_check = str(existing.get("native_vlan")) == str(
proposed.get("native_vlan")
)
if not native_check and proposed.get("native_vlan"):
command = "switchport trunk native vlan {0}".format(
proposed.get("native_vlan")
)
commands.append(command)
if commands:
commands.insert(0, "interface " + name)
return commands
def is_switchport_default(existing):
"""Determines if switchport has a default config based on mode
Args:
existing (dict): existing switchport configuration from Ansible mod
Returns:
boolean: True if switchport has OOB Layer 2 config, i.e.
vlan 1 and trunk all and mode is access
"""
c1 = str(existing["access_vlan"]) == "1"
c2 = str(existing["native_vlan"]) == "1"
c3 = existing["trunk_vlans"] == "1-4094"
c4 = existing["mode"] == "access"
default = c1 and c2 and c3 and c4
return default
def default_switchport_config(name):
commands = []
commands.append("interface " + name)
commands.append("switchport mode access")
commands.append("switch access vlan 1")
commands.append("switchport trunk native vlan 1")
commands.append("switchport trunk allowed vlan all")
return commands
def vlan_range_to_list(vlans):
result = []
if vlans:
for part in vlans.split(","):
if part.lower() == "none":
break
if part:
if "-" in part:
start, stop = (int(i) for i in part.split("-"))
result.extend(range(start, stop + 1))
else:
result.append(int(part))
return sorted(result)
def get_list_of_vlans(module):
config = run_commands(module, ["show vlan"])[0]
vlans = set()
lines = config.strip().splitlines()
for line in lines:
line_parts = line.split()
if line_parts:
try:
int(line_parts[0])
except ValueError:
continue
vlans.add(line_parts[0])
return list(vlans)
def flatten_list(commands):
flat_list = []
for command in commands:
if isinstance(command, list):
flat_list.extend(command)
else:
flat_list.append(command)
return flat_list
def map_params_to_obj(module):
obj = []
aggregate = module.params.get("aggregate")
if aggregate:
for item in aggregate:
for key in item:
if item.get(key) is None:
item[key] = module.params[key]
obj.append(item.copy())
else:
obj.append(
{
"name": module.params["name"],
"mode": module.params["mode"],
"access_vlan": module.params["access_vlan"],
"native_vlan": module.params["native_vlan"],
"trunk_vlans": module.params["trunk_vlans"],
"trunk_allowed_vlans": module.params["trunk_allowed_vlans"],
"state": module.params["state"],
}
)
return obj
def main():
"""main entry point for module execution"""
element_spec = dict(
name=dict(type="str", aliases=["interface"]),
mode=dict(choices=["access", "trunk"]),
access_vlan=dict(type="str"),
native_vlan=dict(type="str"),
trunk_vlans=dict(type="str"),
trunk_allowed_vlans=dict(type="str"),
state=dict(
choices=["absent", "present", "unconfigured"], default="present"
),
)
aggregate_spec = deepcopy(element_spec)
# remove default in aggregate spec, to handle common arguments
remove_default_spec(aggregate_spec)
argument_spec = dict(
aggregate=dict(type="list", elements="dict", options=aggregate_spec)
)
argument_spec.update(element_spec)
argument_spec.update(ios_argument_spec)
module = AnsibleModule(
argument_spec=argument_spec,
mutually_exclusive=[
["access_vlan", "trunk_vlans"],
["access_vlan", "native_vlan"],
["access_vlan", "trunk_allowed_vlans"],
],
supports_check_mode=True,
)
warnings = list()
commands = []
result = {"changed": False, "warnings": warnings}
want = map_params_to_obj(module)
for w in want:
name = w["name"]
mode = w["mode"]
access_vlan = w["access_vlan"]
state = w["state"]
trunk_vlans = w["trunk_vlans"]
native_vlan = w["native_vlan"]
trunk_allowed_vlans = w["trunk_allowed_vlans"]
args = dict(
name=name,
mode=mode,
access_vlan=access_vlan,
native_vlan=native_vlan,
trunk_vlans=trunk_vlans,
trunk_allowed_vlans=trunk_allowed_vlans,
)
proposed = dict((k, v) for k, v in args.items() if v is not None)
name = name.lower()
if mode == "access" and state == "present" and not access_vlan:
module.fail_json(
msg="access_vlan param is required when mode=access && state=present"
)
if mode == "trunk" and access_vlan:
module.fail_json(
msg="access_vlan param not supported when using mode=trunk"
)
if not is_switchport(name, module):
module.fail_json(
msg="""Ensure interface is configured to be a L2
port first before using this module. You can use
the ios_interface module for this."""
)
if interface_is_portchannel(name, module):
module.fail_json(
msg="""Cannot change L2 config on physical
port because it is in a portchannel.
You should update the portchannel config."""
)
# existing will never be null for Eth intfs as there is always a default
existing = get_switchport(name, module)
# Safeguard check
# If there isn't an existing, something is wrong per previous comment
if not existing:
module.fail_json(
msg="Make sure you are using the FULL interface name"
)
if trunk_vlans or trunk_allowed_vlans:
if trunk_vlans:
trunk_vlans_list = vlan_range_to_list(trunk_vlans)
elif trunk_allowed_vlans:
trunk_vlans_list = vlan_range_to_list(trunk_allowed_vlans)
proposed["allowed"] = True
existing_trunks_list = vlan_range_to_list(existing["trunk_vlans"])
existing["trunk_vlans_list"] = existing_trunks_list
proposed["trunk_vlans_list"] = trunk_vlans_list
current_vlans = get_list_of_vlans(module)
if state == "present":
if access_vlan and access_vlan not in current_vlans:
module.fail_json(
msg="""You are trying to configure a VLAN on an interface that
does not exist on the switch yet!""",
vlan=access_vlan,
)
elif native_vlan and native_vlan not in current_vlans:
module.fail_json(
msg="""You are trying to configure a VLAN on an interface that
does not exist on the switch yet!""",
vlan=native_vlan,
)
else:
command = get_switchport_config_commands(
name, existing, proposed, module
)
commands.append(command)
elif state == "unconfigured":
is_default = is_switchport_default(existing)
if not is_default:
command = default_switchport_config(name)
commands.append(command)
elif state == "absent":
command = remove_switchport_config_commands(
name, existing, proposed, module
)
commands.append(command)
if trunk_vlans or trunk_allowed_vlans:
existing.pop("trunk_vlans_list")
proposed.pop("trunk_vlans_list")
cmds = flatten_list(commands)
if cmds:
if module.check_mode:
module.exit_json(changed=True, commands=cmds)
else:
result["changed"] = True
load_config(module, cmds)
if "configure" in cmds:
cmds.pop(0)
result["commands"] = cmds
module.exit_json(**result)
if __name__ == "__main__":
main()