#!/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 .
#
from __future__ import (absolute_import, division, print_function)
__metaclass__ = type
ANSIBLE_METADATA = {'metadata_version': '1.1',
'status': ['preview'],
'supported_by': 'community'}
DOCUMENTATION = '''
---
module: ce_eth_trunk
short_description: Manages Eth-Trunk interfaces on HUAWEI CloudEngine switches.
description:
- Manages Eth-Trunk specific configuration parameters on HUAWEI CloudEngine switches.
author: QijunPan (@QijunPan)
notes:
- C(state=absent) removes the Eth-Trunk config and interface if it
already exists. If members to be removed are not explicitly
passed, all existing members (if any), are removed,
and Eth-Trunk removed.
- Members must be a list.
- This module requires the netconf system service be enabled on the remote device being managed.
- Recommended connection is C(netconf).
- This module also works with C(local) connections for legacy playbooks.
options:
trunk_id:
description:
- Eth-Trunk interface number.
The value is an integer.
The value range depends on the assign forward eth-trunk mode command.
When 256 is specified, the value ranges from 0 to 255.
When 512 is specified, the value ranges from 0 to 511.
When 1024 is specified, the value ranges from 0 to 1023.
required: true
mode:
description:
- Specifies the working mode of an Eth-Trunk interface.
choices: ['manual','lacp-dynamic','lacp-static']
min_links:
description:
- Specifies the minimum number of Eth-Trunk member links in the Up state.
The value is an integer ranging from 1 to the maximum number of interfaces
that can be added to a Eth-Trunk interface.
hash_type:
description:
- Hash algorithm used for load balancing among Eth-Trunk member interfaces.
choices: ['src-dst-ip', 'src-dst-mac', 'enhanced', 'dst-ip', 'dst-mac', 'src-ip', 'src-mac']
members:
description:
- List of interfaces that will be managed in a given Eth-Trunk.
The interface name must be full name.
force:
description:
- When true it forces Eth-Trunk members to match what is
declared in the members param. This can be used to remove
members.
type: bool
default: 'no'
state:
description:
- Manage the state of the resource.
default: present
choices: ['present','absent']
'''
EXAMPLES = '''
- name: eth_trunk module test
hosts: cloudengine
connection: local
gather_facts: no
vars:
cli:
host: "{{ inventory_hostname }}"
port: "{{ ansible_ssh_port }}"
username: "{{ username }}"
password: "{{ password }}"
transport: cli
tasks:
- name: Ensure Eth-Trunk100 is created, add two members, and set to mode lacp-static
ce_eth_trunk:
trunk_id: 100
members: ['10GE1/0/24','10GE1/0/25']
mode: 'lacp-static'
state: present
provider: '{{ cli }}'
'''
RETURN = '''
proposed:
description: k/v pairs of parameters passed into module
returned: always
type: dict
sample: {"trunk_id": "100", "members": ['10GE1/0/24','10GE1/0/25'], "mode": "lacp-static"}
existing:
description: k/v pairs of existing Eth-Trunk
returned: always
type: dict
sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [
{"memberIfName": "10GE1/0/25", "memberIfState": "Down"}],
"min_links": "1", "mode": "manual"}
end_state:
description: k/v pairs of Eth-Trunk info after module execution
returned: always
type: dict
sample: {"trunk_id": "100", "hash_type": "mac", "members_detail": [
{"memberIfName": "10GE1/0/24", "memberIfState": "Down"},
{"memberIfName": "10GE1/0/25", "memberIfState": "Down"}],
"min_links": "1", "mode": "lacp-static"}
updates:
description: command sent to the device
returned: always
type: list
sample: ["interface Eth-Trunk 100",
"mode lacp-static",
"interface 10GE1/0/25",
"eth-trunk 100"]
changed:
description: check to see if a change was made on the device
returned: always
type: bool
sample: true
'''
import re
from ansible.module_utils.basic import AnsibleModule
from ansible_collections.community.general.plugins.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec
CE_NC_GET_TRUNK = """
Eth-Trunk%s
"""
CE_NC_XML_BUILD_TRUNK_CFG = """
%s
"""
CE_NC_XML_DELETE_TRUNK = """
Eth-Trunk%s
"""
CE_NC_XML_CREATE_TRUNK = """
Eth-Trunk%s
"""
CE_NC_XML_MERGE_MINUPNUM = """
Eth-Trunk%s
%s
"""
CE_NC_XML_MERGE_HASHTYPE = """
Eth-Trunk%s
%s
"""
CE_NC_XML_MERGE_WORKMODE = """
Eth-Trunk%s
%s
"""
CE_NC_XML_BUILD_MEMBER_CFG = """
Eth-Trunk%s
%s
"""
CE_NC_XML_MERGE_MEMBER = """
%s
"""
CE_NC_XML_DELETE_MEMBER = """
%s
"""
MODE_XML2CLI = {"Manual": "manual", "Dynamic": "lacp-dynamic", "Static": "lacp-static"}
MODE_CLI2XML = {"manual": "Manual", "lacp-dynamic": "Dynamic", "lacp-static": "Static"}
HASH_XML2CLI = {"IP": "src-dst-ip", "MAC": "src-dst-mac", "Enhanced": "enhanced",
"Desip": "dst-ip", "Desmac": "dst-mac", "Sourceip": "src-ip", "Sourcemac": "src-mac"}
HASH_CLI2XML = {"src-dst-ip": "IP", "src-dst-mac": "MAC", "enhanced": "Enhanced",
"dst-ip": "Desip", "dst-mac": "Desmac", "src-ip": "Sourceip", "src-mac": "Sourcemac"}
def get_interface_type(interface):
"""Gets the type of interface, such as 10GE, ETH-TRUNK, VLANIF..."""
if interface is None:
return None
iftype = None
if interface.upper().startswith('GE'):
iftype = 'ge'
elif interface.upper().startswith('10GE'):
iftype = '10ge'
elif interface.upper().startswith('25GE'):
iftype = '25ge'
elif interface.upper().startswith('4X10GE'):
iftype = '4x10ge'
elif interface.upper().startswith('40GE'):
iftype = '40ge'
elif interface.upper().startswith('100GE'):
iftype = '100ge'
elif interface.upper().startswith('VLANIF'):
iftype = 'vlanif'
elif interface.upper().startswith('LOOPBACK'):
iftype = 'loopback'
elif interface.upper().startswith('METH'):
iftype = 'meth'
elif interface.upper().startswith('ETH-TRUNK'):
iftype = 'eth-trunk'
elif interface.upper().startswith('VBDIF'):
iftype = 'vbdif'
elif interface.upper().startswith('NVE'):
iftype = 'nve'
elif interface.upper().startswith('TUNNEL'):
iftype = 'tunnel'
elif interface.upper().startswith('ETHERNET'):
iftype = 'ethernet'
elif interface.upper().startswith('FCOE-PORT'):
iftype = 'fcoe-port'
elif interface.upper().startswith('FABRIC-PORT'):
iftype = 'fabric-port'
elif interface.upper().startswith('STACK-PORT'):
iftype = 'stack-port'
elif interface.upper().startswith('NULL'):
iftype = 'null'
else:
return None
return iftype.lower()
def mode_xml_to_cli_str(mode):
"""convert mode to cli format string"""
if not mode:
return ""
return MODE_XML2CLI.get(mode)
def hash_type_xml_to_cli_str(hash_type):
"""convert trunk hash type netconf xml to cli format string"""
if not hash_type:
return ""
return HASH_XML2CLI.get(hash_type)
class EthTrunk(object):
"""
Manages Eth-Trunk interfaces.
"""
def __init__(self, argument_spec):
self.spec = argument_spec
self.module = None
self.__init_module__()
# module input info
self.trunk_id = self.module.params['trunk_id']
self.mode = self.module.params['mode']
self.min_links = self.module.params['min_links']
self.hash_type = self.module.params['hash_type']
self.members = self.module.params['members']
self.state = self.module.params['state']
self.force = self.module.params['force']
# state
self.changed = False
self.updates_cmd = list()
self.results = dict()
self.proposed = dict()
self.existing = dict()
self.end_state = dict()
# interface info
self.trunk_info = dict()
def __init_module__(self):
""" init module """
self.module = AnsibleModule(
argument_spec=self.spec, supports_check_mode=True)
def netconf_set_config(self, xml_str, xml_name):
""" netconf set config """
recv_xml = set_nc_config(self.module, xml_str)
if "" not in recv_xml:
self.module.fail_json(msg='Error: %s failed.' % xml_name)
def get_trunk_dict(self, trunk_id):
""" get one interface attributes dict."""
trunk_info = dict()
conf_str = CE_NC_GET_TRUNK % trunk_id
recv_xml = get_nc_config(self.module, conf_str)
if "" in recv_xml:
return trunk_info
# get trunk base info
base = re.findall(
r'.*(.*).*\s*'
r'(.*).*\s*'
r'(.*).*\s*'
r'(.*).*\s*'
r'(.*).*\s*'
r'(.*).*\s*'
r'(.*).*\s*'
r'(.*).*', recv_xml)
if base:
trunk_info = dict(ifName=base[0][0],
trunkId=base[0][0].lower().replace("eth-trunk", "").replace(" ", ""),
minUpNum=base[0][1],
maxUpNum=base[0][2],
trunkType=base[0][3],
hashType=base[0][4],
workMode=base[0][5],
upMemberIfNum=base[0][6],
memberIfNum=base[0][7])
# get trunk member interface info
member = re.findall(
r'.*(.*).*\s*'
r'(.*).*', recv_xml)
trunk_info["TrunkMemberIfs"] = list()
for mem in member:
trunk_info["TrunkMemberIfs"].append(
dict(memberIfName=mem[0], memberIfState=mem[1]))
return trunk_info
def is_member_exist(self, ifname):
"""is trunk member exist"""
if not self.trunk_info["TrunkMemberIfs"]:
return False
for mem in self.trunk_info["TrunkMemberIfs"]:
if ifname.replace(" ", "").upper() == mem["memberIfName"].replace(" ", "").upper():
return True
return False
def get_mode_xml_str(self):
"""trunk mode netconf xml format string"""
return MODE_CLI2XML.get(self.mode)
def get_hash_type_xml_str(self):
"""trunk hash type netconf xml format string"""
return HASH_CLI2XML.get(self.hash_type)
def create_eth_trunk(self):
"""Create Eth-Trunk interface"""
xml_str = CE_NC_XML_CREATE_TRUNK % self.trunk_id
self.updates_cmd.append("interface Eth-Trunk %s" % self.trunk_id)
if self.hash_type:
self.updates_cmd.append("load-balance %s" % self.hash_type)
xml_str += CE_NC_XML_MERGE_HASHTYPE % (self.trunk_id, self.get_hash_type_xml_str())
if self.mode:
self.updates_cmd.append("mode %s" % self.mode)
xml_str += CE_NC_XML_MERGE_WORKMODE % (self.trunk_id, self.get_mode_xml_str())
if self.min_links:
self.updates_cmd.append("least active-linknumber %s" % self.min_links)
xml_str += CE_NC_XML_MERGE_MINUPNUM % (self.trunk_id, self.min_links)
if self.members:
mem_xml = ""
for mem in self.members:
mem_xml += CE_NC_XML_MERGE_MEMBER % mem.upper()
self.updates_cmd.append("interface %s" % mem)
self.updates_cmd.append("eth-trunk %s" % self.trunk_id)
xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml)
cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
self.netconf_set_config(cfg_xml, "CREATE_TRUNK")
self.changed = True
def delete_eth_trunk(self):
"""Delete Eth-Trunk interface and remove all member"""
if not self.trunk_info:
return
xml_str = ""
mem_str = ""
if self.trunk_info["TrunkMemberIfs"]:
for mem in self.trunk_info["TrunkMemberIfs"]:
mem_str += CE_NC_XML_DELETE_MEMBER % mem["memberIfName"]
self.updates_cmd.append("interface %s" % mem["memberIfName"])
self.updates_cmd.append("undo eth-trunk")
if mem_str:
xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_str)
xml_str += CE_NC_XML_DELETE_TRUNK % self.trunk_id
self.updates_cmd.append("undo interface Eth-Trunk %s" % self.trunk_id)
cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
self.netconf_set_config(cfg_xml, "DELETE_TRUNK")
self.changed = True
def remove_member(self):
"""delete trunk member"""
if not self.members:
return
change = False
mem_xml = ""
xml_str = ""
for mem in self.members:
if self.is_member_exist(mem):
mem_xml += CE_NC_XML_DELETE_MEMBER % mem.upper()
self.updates_cmd.append("interface %s" % mem)
self.updates_cmd.append("undo eth-trunk")
if mem_xml:
xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml)
change = True
if not change:
return
cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
self.netconf_set_config(cfg_xml, "REMOVE_TRUNK_MEMBER")
self.changed = True
def merge_eth_trunk(self):
"""Create or merge Eth-Trunk"""
change = False
xml_str = ""
self.updates_cmd.append("interface Eth-Trunk %s" % self.trunk_id)
if self.hash_type and self.get_hash_type_xml_str() != self.trunk_info["hashType"]:
self.updates_cmd.append("load-balance %s" %
self.hash_type)
xml_str += CE_NC_XML_MERGE_HASHTYPE % (
self.trunk_id, self.get_hash_type_xml_str())
change = True
if self.min_links and self.min_links != self.trunk_info["minUpNum"]:
self.updates_cmd.append(
"least active-linknumber %s" % self.min_links)
xml_str += CE_NC_XML_MERGE_MINUPNUM % (
self.trunk_id, self.min_links)
change = True
if self.mode and self.get_mode_xml_str() != self.trunk_info["workMode"]:
self.updates_cmd.append("mode %s" % self.mode)
xml_str += CE_NC_XML_MERGE_WORKMODE % (
self.trunk_id, self.get_mode_xml_str())
change = True
if not change:
self.updates_cmd.pop() # remove 'interface Eth-Trunk' command
# deal force:
# When true it forces Eth-Trunk members to match
# what is declared in the members param.
if self.force and self.trunk_info["TrunkMemberIfs"]:
mem_xml = ""
for mem in self.trunk_info["TrunkMemberIfs"]:
if not self.members or mem["memberIfName"].replace(" ", "").upper() not in self.members:
mem_xml += CE_NC_XML_DELETE_MEMBER % mem["memberIfName"]
self.updates_cmd.append("interface %s" % mem["memberIfName"])
self.updates_cmd.append("undo eth-trunk")
if mem_xml:
xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (self.trunk_id, mem_xml)
change = True
if self.members:
mem_xml = ""
for mem in self.members:
if not self.is_member_exist(mem):
mem_xml += CE_NC_XML_MERGE_MEMBER % mem.upper()
self.updates_cmd.append("interface %s" % mem)
self.updates_cmd.append("eth-trunk %s" % self.trunk_id)
if mem_xml:
xml_str += CE_NC_XML_BUILD_MEMBER_CFG % (
self.trunk_id, mem_xml)
change = True
if not change:
return
cfg_xml = CE_NC_XML_BUILD_TRUNK_CFG % xml_str
self.netconf_set_config(cfg_xml, "MERGE_TRUNK")
self.changed = True
def check_params(self):
"""Check all input params"""
# trunk_id check
if not self.trunk_id.isdigit():
self.module.fail_json(msg='The parameter of trunk_id is invalid.')
# min_links check
if self.min_links and not self.min_links.isdigit():
self.module.fail_json(msg='The parameter of min_links is invalid.')
# members check and convert members to upper
if self.members:
for mem in self.members:
if not get_interface_type(mem.replace(" ", "")):
self.module.fail_json(
msg='The parameter of members is invalid.')
for mem_id in range(len(self.members)):
self.members[mem_id] = self.members[mem_id].replace(" ", "").upper()
def get_proposed(self):
"""get proposed info"""
self.proposed["trunk_id"] = self.trunk_id
self.proposed["mode"] = self.mode
if self.min_links:
self.proposed["min_links"] = self.min_links
self.proposed["hash_type"] = self.hash_type
if self.members:
self.proposed["members"] = self.members
self.proposed["state"] = self.state
self.proposed["force"] = self.force
def get_existing(self):
"""get existing info"""
if not self.trunk_info:
return
self.existing["trunk_id"] = self.trunk_info["trunkId"]
self.existing["min_links"] = self.trunk_info["minUpNum"]
self.existing["hash_type"] = hash_type_xml_to_cli_str(self.trunk_info["hashType"])
self.existing["mode"] = mode_xml_to_cli_str(self.trunk_info["workMode"])
self.existing["members_detail"] = self.trunk_info["TrunkMemberIfs"]
def get_end_state(self):
"""get end state info"""
trunk_info = self.get_trunk_dict(self.trunk_id)
if not trunk_info:
return
self.end_state["trunk_id"] = trunk_info["trunkId"]
self.end_state["min_links"] = trunk_info["minUpNum"]
self.end_state["hash_type"] = hash_type_xml_to_cli_str(trunk_info["hashType"])
self.end_state["mode"] = mode_xml_to_cli_str(trunk_info["workMode"])
self.end_state["members_detail"] = trunk_info["TrunkMemberIfs"]
def work(self):
"""worker"""
self.check_params()
self.trunk_info = self.get_trunk_dict(self.trunk_id)
self.get_existing()
self.get_proposed()
# deal present or absent
if self.state == "present":
if not self.trunk_info:
# create
self.create_eth_trunk()
else:
# merge trunk
self.merge_eth_trunk()
else:
if self.trunk_info:
if not self.members:
# remove all members and delete trunk
self.delete_eth_trunk()
else:
# remove some trunk members
self.remove_member()
else:
self.module.fail_json(msg='Error: Eth-Trunk does not exist.')
self.get_end_state()
self.results['changed'] = self.changed
self.results['proposed'] = self.proposed
self.results['existing'] = self.existing
self.results['end_state'] = self.end_state
if self.changed:
self.results['updates'] = self.updates_cmd
else:
self.results['updates'] = list()
self.module.exit_json(**self.results)
def main():
"""Module main"""
argument_spec = dict(
trunk_id=dict(required=True),
mode=dict(required=False,
choices=['manual', 'lacp-dynamic', 'lacp-static'],
type='str'),
min_links=dict(required=False, type='str'),
hash_type=dict(required=False,
choices=['src-dst-ip', 'src-dst-mac', 'enhanced',
'dst-ip', 'dst-mac', 'src-ip', 'src-mac'],
type='str'),
members=dict(required=False, default=None, type='list'),
force=dict(required=False, default=False, type='bool'),
state=dict(required=False, default='present',
choices=['present', 'absent'])
)
argument_spec.update(ce_argument_spec)
module = EthTrunk(argument_spec)
module.work()
if __name__ == '__main__':
main()