#!/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 . # ANSIBLE_METADATA = {'metadata_version': '1.1', 'status': ['preview'], 'supported_by': 'community'} DOCUMENTATION = ''' --- module: ce_ospf version_added: "2.4" short_description: Manages configuration of an OSPF instance on HUAWEI CloudEngine switches. description: - Manages configuration of an OSPF instance on HUAWEI CloudEngine switches. author: QijunPan (@CloudEngine-Ansible) options: process_id: description: - Specifies a process ID. The value is an integer ranging from 1 to 4294967295. required: true area: description: - Specifies the area ID. The area with the area-id being 0 is a backbone area. Valid values are a string, formatted as an IP address (i.e. "0.0.0.0") or as an integer between 1 and 4294967295. addr: description: - Specifies the address of the network segment where the interface resides. The value is in dotted decimal notation. mask: description: - IP network wildcard bits in decimal format between 0 and 32. auth_mode: description: - Specifies the authentication type. choices: ['none', 'hmac-sha256', 'md5', 'hmac-md5', 'simple'] auth_text_simple: description: - Specifies a password for simple authentication. The value is a string of 1 to 8 characters. auth_key_id: description: - Authentication key id when C(auth_mode) is 'hmac-sha256', 'md5' or 'hmac-md5. Valid value is an integer is in the range from 1 to 255. auth_text_md5: description: - Specifies a password for MD5, HMAC-MD5, or HMAC-SHA256 authentication. The value is a string of 1 to 255 case-sensitive characters, spaces not supported. nexthop_addr: description: - IPv4 address for configure next-hop address's weight. Valid values are a string, formatted as an IP address. nexthop_weight: description: - Indicates the weight of the next hop. The smaller the value is, the higher the preference of the route is. It is an integer that ranges from 1 to 254. max_load_balance: description: - The maximum number of paths for forward packets over multiple paths. Valid value is an integer in the range from 1 to 64. state: description: - Determines whether the config should be present or not on the device. default: present choices: ['present','absent'] ''' EXAMPLES = ''' - name: ospf 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: Configure ospf ce_ospf: process_id: 1 area: 100 state: present provider: "{{ cli }}" ''' RETURN = ''' proposed: description: k/v pairs of parameters passed into module returned: verbose mode type: dict sample: {"process_id": "1", "area": "100"} existing: description: k/v pairs of existing configuration returned: verbose mode type: dict sample: {"process_id": "1", "areas": [], "nexthops":[], "max_load_balance": "32"} end_state: description: k/v pairs of configuration after module execution returned: verbose mode type: dict sample: {"process_id": "1", "areas": [{"areaId": "0.0.0.100", "areaType": "Normal"}], "nexthops":[], "max_load_balance": "32"} updates: description: commands sent to the device returned: always type: list sample: ["ospf 1", "area 0.0.0.100"] changed: description: check to see if a change was made on the device returned: always type: boolean sample: true ''' from xml.etree import ElementTree from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.network.cloudengine.ce import get_nc_config, set_nc_config, ce_argument_spec CE_NC_GET_OSPF = """ %s """ CE_NC_CREATE_PROCESS = """ %s """ CE_NC_DELETE_PROCESS = """ %s """ CE_NC_XML_BUILD_MERGE_PROCESS = """ %s %s """ CE_NC_XML_BUILD_PROCESS = """ %s %s """ CE_NC_XML_BUILD_MERGE_AREA = """ %s %s """ CE_NC_XML_BUILD_DELETE_AREA = """ %s %s """ CE_NC_XML_BUILD_AREA = """ %s %s """ CE_NC_XML_SET_AUTH_MODE = """ %s """ CE_NC_XML_SET_AUTH_TEXT_SIMPLE = """ %s """ CE_NC_XML_SET_AUTH_MD5 = """ %s %s """ CE_NC_XML_MERGE_NETWORKS = """ %s %s """ CE_NC_XML_DELETE_NETWORKS = """ %s %s """ CE_NC_XML_SET_LB = """ %s """ CE_NC_XML_BUILD_MERGE_TOPO = """ base %s """ CE_NC_XML_BUILD_TOPO = """ base %s """ CE_NC_XML_MERGE_NEXTHOP = """ %s %s """ CE_NC_XML_DELETE_NEXTHOP = """ %s """ class OSPF(object): """ Manages configuration of an ospf instance. """ def __init__(self, argument_spec): self.spec = argument_spec self.module = None self.init_module() # module input info self.process_id = self.module.params['process_id'] self.area = self.module.params['area'] self.addr = self.module.params['addr'] self.mask = self.module.params['mask'] self.auth_mode = self.module.params['auth_mode'] self.auth_text_simple = self.module.params['auth_text_simple'] self.auth_key_id = self.module.params['auth_key_id'] self.auth_text_md5 = self.module.params['auth_text_md5'] self.nexthop_addr = self.module.params['nexthop_addr'] self.nexthop_weight = self.module.params['nexthop_weight'] self.max_load_balance = self.module.params['max_load_balance'] self.state = self.module.params['state'] # ospf info self.ospf_info = dict() # state self.changed = False self.updates_cmd = list() self.results = dict() self.proposed = dict() self.existing = dict() self.end_state = dict() def init_module(self): """ init module """ required_together = [ ("addr", "mask"), ("auth_key_id", "auth_text_md5"), ("nexthop_addr", "nexthop_weight") ] self.module = AnsibleModule( argument_spec=self.spec, required_together=required_together, supports_check_mode=True) def check_response(self, xml_str, xml_name): """Check if response message is already succeed.""" if "" not in xml_str: self.module.fail_json(msg='Error: %s failed.' % xml_name) def get_wildcard_mask(self): """convert mask length to ip address wildcard mask, i.e. 24 to 0.0.0.255""" mask_int = ["255"] * 4 length = int(self.mask) if length > 32: self.module.fail_json(msg='IPv4 ipaddress mask length is invalid') if length < 8: mask_int[0] = str(int(~(0xFF << (8 - length % 8)) & 0xFF)) if length >= 8: mask_int[0] = '0' mask_int[1] = str(int(~(0xFF << (16 - (length % 16))) & 0xFF)) if length >= 16: mask_int[1] = '0' mask_int[2] = str(int(~(0xFF << (24 - (length % 24))) & 0xFF)) if length >= 24: mask_int[2] = '0' mask_int[3] = str(int(~(0xFF << (32 - (length % 32))) & 0xFF)) if length == 32: mask_int[3] = '0' return '.'.join(mask_int) def get_area_ip(self): """convert integer to ip address""" if not self.area.isdigit(): return self.area addr_int = ['0'] * 4 addr_int[0] = str(((int(self.area) & 0xFF000000) >> 24) & 0xFF) addr_int[1] = str(((int(self.area) & 0x00FF0000) >> 16) & 0xFF) addr_int[2] = str(((int(self.area) & 0x0000FF00) >> 8) & 0XFF) addr_int[3] = str(int(self.area) & 0xFF) return '.'.join(addr_int) def get_ospf_dict(self, process_id): """ get one ospf attributes dict.""" ospf_info = dict() conf_str = CE_NC_GET_OSPF % process_id xml_str = get_nc_config(self.module, conf_str) if "" in xml_str: return ospf_info xml_str = xml_str.replace('\r', '').replace('\n', '').\ replace('xmlns="urn:ietf:params:xml:ns:netconf:base:1.0"', "").\ replace('xmlns="http://www.huawei.com/netconf/vrp"', "") # get process base info root = ElementTree.fromstring(xml_str) ospfsite = root.find("data/ospfv2/ospfv2comm/ospfSites/ospfSite") if ospfsite: for site in ospfsite: if site.tag in ["processId", "routerId", "vrfName"]: ospf_info[site.tag] = site.text # get Topology info topo = root.find( "data/ospfv2/ospfv2comm/ospfSites/ospfSite/ProcessTopologys/ProcessTopology") if topo: for eles in topo: if eles.tag in ["maxLoadBalancing"]: ospf_info[eles.tag] = eles.text # get nexthop info ospf_info["nexthops"] = list() nexthops = root.findall( "data/ospfv2/ospfv2comm/ospfSites/ospfSite/ProcessTopologys/ProcessTopology/nexthopMTs/nexthopMT") if nexthops: for nexthop in nexthops: nh_dict = dict() for ele in nexthop: if ele.tag in ["ipAddress", "weight"]: nh_dict[ele.tag] = ele.text ospf_info["nexthops"].append(nh_dict) # get areas info ospf_info["areas"] = list() areas = root.findall( "data/ospfv2/ospfv2comm/ospfSites/ospfSite/areas/area") if areas: for area in areas: area_dict = dict() for ele in area: if ele.tag in ["areaId", "authTextSimple", "areaType", "authenticationMode", "keyId", "authTextMd5"]: area_dict[ele.tag] = ele.text if ele.tag == "networks": # get networks info area_dict["networks"] = list() for net in ele: net_dict = dict() for net_ele in net: if net_ele.tag in ["ipAddress", "wildcardMask"]: net_dict[net_ele.tag] = net_ele.text area_dict["networks"].append(net_dict) ospf_info["areas"].append(area_dict) return ospf_info def is_area_exist(self): """is ospf area exist""" if not self.ospf_info: return False for area in self.ospf_info["areas"]: if area["areaId"] == self.get_area_ip(): return True return False def is_network_exist(self): """is ospf area network exist""" if not self.ospf_info: return False for area in self.ospf_info["areas"]: if area["areaId"] == self.get_area_ip(): if not area.get("networks"): return False for network in area.get("networks"): if network["ipAddress"] == self.addr and network["wildcardMask"] == self.get_wildcard_mask(): return True else: break return False def is_nexthop_exist(self): """is ospf nexthop exist""" if not self.ospf_info: return False for nexthop in self.ospf_info["nexthops"]: if nexthop["ipAddress"] == self.nexthop_addr: return True return False def is_nexthop_change(self): """is ospf nexthop change""" if not self.ospf_info: return True for nexthop in self.ospf_info["nexthops"]: if nexthop["ipAddress"] == self.nexthop_addr: if nexthop["weight"] == self.nexthop_weight: return False else: return True return True def create_process(self): """Create ospf process""" xml_area = "" self.updates_cmd.append("ospf %s" % self.process_id) # nexthop weight xml_nh = "" if self.nexthop_addr: xml_nh = CE_NC_XML_MERGE_NEXTHOP % ( self.nexthop_addr, self.nexthop_weight) self.updates_cmd.append("nexthop %s weight %s" % ( self.nexthop_addr, self.nexthop_weight)) # max load balance xml_lb = "" if self.max_load_balance: xml_lb = CE_NC_XML_SET_LB % self.max_load_balance self.updates_cmd.append( "maximum load-balancing %s" % self.max_load_balance) xml_topo = "" if xml_lb or xml_nh: xml_topo = CE_NC_XML_BUILD_TOPO % (xml_nh + xml_lb) if self.area: self.updates_cmd.append("area %s" % self.get_area_ip()) xml_auth = "" xml_network = "" # networks if self.addr and self.mask: xml_network = CE_NC_XML_MERGE_NETWORKS % ( self.addr, self.get_wildcard_mask()) self.updates_cmd.append("network %s %s" % ( self.addr, self.get_wildcard_mask())) # authentication mode if self.auth_mode: xml_auth += CE_NC_XML_SET_AUTH_MODE % self.auth_mode if self.auth_mode == "none": self.updates_cmd.append("undo authentication-mode") else: self.updates_cmd.append( "authentication-mode %s" % self.auth_mode) if self.auth_mode == "simple" and self.auth_text_simple: xml_auth += CE_NC_XML_SET_AUTH_TEXT_SIMPLE % self.auth_text_simple self.updates_cmd.pop() self.updates_cmd.append( "authentication-mode %s %s" % (self.auth_mode, self.auth_text_simple)) if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: if self.auth_key_id and self.auth_text_md5: xml_auth += CE_NC_XML_SET_AUTH_MD5 % ( self.auth_key_id, self.auth_text_md5) self.updates_cmd.pop() self.updates_cmd.append( "authentication-mode %s %s %s" % (self.auth_mode, self.auth_key_id, self.auth_text_md5)) if xml_network or xml_auth: xml_area += CE_NC_XML_BUILD_MERGE_AREA % ( self.get_area_ip(), xml_network + xml_auth) xml_str = CE_NC_XML_BUILD_MERGE_PROCESS % ( self.process_id, xml_topo + xml_area) recv_xml = set_nc_config(self.module, xml_str) self.check_response(recv_xml, "CREATE_PROCESS") self.changed = True def delete_process(self): """Delete ospf process""" xml_str = CE_NC_DELETE_PROCESS % self.process_id recv_xml = set_nc_config(self.module, xml_str) self.check_response(recv_xml, "DELETE_PROCESS") self.updates_cmd.append("undo ospf %s" % self.process_id) self.changed = True def merge_process(self): """merge ospf process""" xml_area = "" xml_str = "" self.updates_cmd.append("ospf %s" % self.process_id) # nexthop weight xml_nh = "" if self.nexthop_addr and self.is_nexthop_change(): xml_nh = CE_NC_XML_MERGE_NEXTHOP % ( self.nexthop_addr, self.nexthop_weight) self.updates_cmd.append("nexthop %s weight %s" % ( self.nexthop_addr, self.nexthop_weight)) # max load balance xml_lb = "" if self.max_load_balance and self.ospf_info.get("maxLoadBalancing") != self.max_load_balance: xml_lb = CE_NC_XML_SET_LB % self.max_load_balance self.updates_cmd.append( "maximum load-balancing %s" % self.max_load_balance) xml_topo = "" if xml_lb or xml_nh: xml_topo = CE_NC_XML_BUILD_MERGE_TOPO % (xml_nh + xml_lb) if self.area: self.updates_cmd.append("area %s" % self.get_area_ip()) xml_network = "" xml_auth = "" if self.addr and self.mask: if not self.is_network_exist(): xml_network += CE_NC_XML_MERGE_NETWORKS % ( self.addr, self.get_wildcard_mask()) self.updates_cmd.append("network %s %s" % ( self.addr, self.get_wildcard_mask())) # NOTE: for security, authentication config will always be update if self.auth_mode: xml_auth += CE_NC_XML_SET_AUTH_MODE % self.auth_mode if self.auth_mode == "none": self.updates_cmd.append("undo authentication-mode") else: self.updates_cmd.append( "authentication-mode %s" % self.auth_mode) if self.auth_mode == "simple" and self.auth_text_simple: xml_auth += CE_NC_XML_SET_AUTH_TEXT_SIMPLE % self.auth_text_simple self.updates_cmd.pop() self.updates_cmd.append( "authentication-mode %s %s" % (self.auth_mode, self.auth_text_simple)) if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: if self.auth_key_id and self.auth_text_md5: xml_auth += CE_NC_XML_SET_AUTH_MD5 % ( self.auth_key_id, self.auth_text_md5) self.updates_cmd.pop() self.updates_cmd.append( "authentication-mode %s %s %s" % (self.auth_mode, self.auth_key_id, self.auth_text_md5)) if xml_network or xml_auth or not self.is_area_exist(): xml_area += CE_NC_XML_BUILD_MERGE_AREA % ( self.get_area_ip(), xml_network + xml_auth) elif self.is_area_exist(): self.updates_cmd.pop() # remove command: area else: pass if xml_area or xml_topo: xml_str = CE_NC_XML_BUILD_MERGE_PROCESS % ( self.process_id, xml_topo + xml_area) recv_xml = set_nc_config(self.module, xml_str) self.check_response(recv_xml, "MERGE_PROCESS") self.changed = True def remove_area_network(self): """remvoe ospf area network""" if not self.is_network_exist(): return xml_network = CE_NC_XML_DELETE_NETWORKS % ( self.addr, self.get_wildcard_mask()) xml_area = CE_NC_XML_BUILD_AREA % (self.get_area_ip(), xml_network) xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, xml_area) recv_xml = set_nc_config(self.module, xml_str) self.check_response(recv_xml, "DELETE_AREA_NETWORK") self.updates_cmd.append("ospf %s" % self.process_id) self.updates_cmd.append("area %s" % self.get_area_ip()) self.updates_cmd.append("undo network %s %s" % (self.addr, self.get_wildcard_mask())) self.changed = True def remove_area(self): """remove ospf area""" if not self.is_area_exist(): return xml_area = CE_NC_XML_BUILD_DELETE_AREA % (self.get_area_ip(), "") xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, xml_area) recv_xml = set_nc_config(self.module, xml_str) self.check_response(recv_xml, "DELETE_AREA") self.updates_cmd.append("ospf %s" % self.process_id) self.updates_cmd.append("undo area %s" % self.get_area_ip()) self.changed = True def remove_nexthop(self): """remove ospf nexthop weight""" if not self.is_nexthop_exist(): return xml_nh = CE_NC_XML_DELETE_NEXTHOP % self.nexthop_addr xml_topo = CE_NC_XML_BUILD_TOPO % xml_nh xml_str = CE_NC_XML_BUILD_PROCESS % (self.process_id, xml_topo) recv_xml = set_nc_config(self.module, xml_str) self.check_response(recv_xml, "DELETE_NEXTHOP_WEIGHT") self.updates_cmd.append("ospf %s" % self.process_id) self.updates_cmd.append("undo nexthop %s" % self.nexthop_addr) self.changed = True def is_valid_v4addr(self, addr): """check is ipv4 addr is valid""" if addr.find('.') != -1: addr_list = addr.split('.') if len(addr_list) != 4: return False for each_num in addr_list: if not each_num.isdigit(): return False if int(each_num) > 255: return False return True return False def convert_ip_to_network(self): """convert ip to subnet address""" ip_list = self.addr.split('.') mask_list = self.get_wildcard_mask().split('.') for i in range(len(ip_list)): ip_list[i] = str((int(ip_list[i]) & (~int(mask_list[i]))) & 0xff) self.addr = '.'.join(ip_list) def check_params(self): """Check all input params""" # process_id check if not self.process_id.isdigit(): self.module.fail_json(msg="Error: process_id is not digit.") if int(self.process_id) < 1 or int(self.process_id) > 4294967295: self.module.fail_json( msg="Error: process_id must be an integer between 1 and 4294967295.") if self.area: # area check if self.area.isdigit(): if int(self.area) < 0 or int(self.area) > 4294967295: self.module.fail_json( msg="Error: area id (Integer) must be between 0 and 4294967295.") else: if not self.is_valid_v4addr(self.area): self.module.fail_json(msg="Error: area id is invalid.") # area network check if self.addr: if not self.is_valid_v4addr(self.addr): self.module.fail_json( msg="Error: network addr is invalid.") if not self.mask.isdigit(): self.module.fail_json( msg="Error: network mask is not digit.") if int(self.mask) < 0 or int(self.mask) > 32: self.module.fail_json( msg="Error: network mask is invalid.") # area authentication check if self.state == "present" and self.auth_mode: if self.auth_mode == "simple": if self.auth_text_simple and len(self.auth_text_simple) > 8: self.module.fail_json( msg="Error: auth_text_simple is not in the range from 1 to 8.") if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: if self.auth_key_id: if not self.auth_key_id.isdigit(): self.module.fail_json( msg="Error: auth_key_id is not digit.") if int(self.auth_key_id) < 1 or int(self.auth_key_id) > 255: self.module.fail_json( msg="Error: auth_key_id is not in the range from 1 to 255.") if self.auth_text_md5 and len(self.auth_text_md5) > 255: self.module.fail_json( msg="Error: auth_text_md5 is not in the range from 1 to 255.") # process max load balance check if self.state == "present" and self.max_load_balance: if not self.max_load_balance.isdigit(): self.module.fail_json( msg="Error: max_load_balance is not digit.") if int(self.max_load_balance) < 1 or int(self.max_load_balance) > 64: self.module.fail_json( msg="Error: max_load_balance is not in the range from 1 to 64.") # process nexthop weight check if self.nexthop_addr: if not self.is_valid_v4addr(self.nexthop_addr): self.module.fail_json(msg="Error: nexthop_addr is invalid.") if not self.nexthop_weight.isdigit(): self.module.fail_json( msg="Error: nexthop_weight is not digit.") if int(self.nexthop_weight) < 1 or int(self.nexthop_weight) > 254: self.module.fail_json( msg="Error: nexthop_weight is not in the range from 1 to 254.") if self.addr: self.convert_ip_to_network() def get_proposed(self): """get proposed info""" self.proposed["process_id"] = self.process_id self.proposed["area"] = self.area if self.area: self.proposed["addr"] = self.addr self.proposed["mask"] = self.mask if self.auth_mode: self.proposed["auth_mode"] = self.auth_mode if self.auth_mode == "simple": self.proposed["auth_text_simple"] = self.auth_text_simple if self.auth_mode in ["hmac-sha256", "hmac-sha256", "md5"]: self.proposed["auth_key_id"] = self.auth_key_id self.proposed["auth_text_md5"] = self.auth_text_md5 if self.nexthop_addr: self.proposed["nexthop_addr"] = self.nexthop_addr self.proposed["nexthop_weight"] = self.nexthop_weight self.proposed["max_load_balance"] = self.max_load_balance self.proposed["state"] = self.state def get_existing(self): """get existing info""" if not self.ospf_info: return self.existing["process_id"] = self.process_id self.existing["areas"] = self.ospf_info["areas"] self.existing["nexthops"] = self.ospf_info["nexthops"] self.existing["max_load_balance"] = self.ospf_info.get( "maxLoadBalancing") def get_end_state(self): """get end state info""" ospf_info = self.get_ospf_dict(self.process_id) if not ospf_info: return self.end_state["process_id"] = self.process_id self.end_state["areas"] = ospf_info["areas"] self.end_state["nexthops"] = ospf_info["nexthops"] self.end_state["max_load_balance"] = ospf_info.get("maxLoadBalancing") def work(self): """worker""" self.check_params() self.ospf_info = self.get_ospf_dict(self.process_id) self.get_existing() self.get_proposed() # deal present or absent if self.state == "present": if not self.ospf_info: # create ospf process self.create_process() else: # merge ospf self.merge_process() else: if self.ospf_info: if self.area: if self.addr: # remove ospf area network self.remove_area_network() else: # remove ospf area self.remove_area() if self.nexthop_addr: # remove ospf nexthop weight self.remove_nexthop() if not self.area and not self.nexthop_addr: # remove ospf process self.delete_process() else: self.module.fail_json(msg='Error: ospf process 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( process_id=dict(required=True, type='str'), area=dict(required=False, type='str'), addr=dict(required=False, type='str'), mask=dict(required=False, type='str'), auth_mode=dict(required=False, choices=['none', 'hmac-sha256', 'md5', 'hmac-md5', 'simple'], type='str'), auth_text_simple=dict(required=False, type='str', no_log=True), auth_key_id=dict(required=False, type='str'), auth_text_md5=dict(required=False, type='str', no_log=True), nexthop_addr=dict(required=False, type='str'), nexthop_weight=dict(required=False, type='str'), max_load_balance=dict(required=False, type='str'), state=dict(required=False, default='present', choices=['present', 'absent']) ) argument_spec.update(ce_argument_spec) module = OSPF(argument_spec) module.work() if __name__ == '__main__': main()