|
| 1 | +#!/usr/bin/env python |
| 2 | +import calendar |
| 3 | +import os |
| 4 | +import sys |
| 5 | +import socket |
| 6 | +import struct |
| 7 | +import json |
| 8 | +import copy |
| 9 | +import ipaddr as ipaddress |
| 10 | +from collections import defaultdict |
| 11 | + |
| 12 | +from lxml import etree as ET |
| 13 | +from lxml.etree import QName |
| 14 | + |
| 15 | +DOCUMENTATION = ''' |
| 16 | +--- |
| 17 | +module: minigraph_facts |
| 18 | +version_added: "1.9" |
| 19 | +author: Guohan Lu ([email protected]) |
| 20 | +short_description: Retrive minigraph facts for a device. |
| 21 | +description: |
| 22 | + - Retrieve minigraph facts for a device, the facts will be |
| 23 | + inserted to the ansible_facts key. |
| 24 | +options: |
| 25 | + host: |
| 26 | + description: |
| 27 | + - Set to target snmp server (normally {{inventory_hostname}}) |
| 28 | + required: true |
| 29 | +''' |
| 30 | + |
| 31 | +EXAMPLES = ''' |
| 32 | +# Gather minigraph facts |
| 33 | +- name: Gathering minigraph facts about the device |
| 34 | + minigraph_facts: host={{ hostname }} |
| 35 | +''' |
| 36 | + |
| 37 | +ns = "Microsoft.Search.Autopilot.Evolution" |
| 38 | +ns1 = "http://schemas.datacontract.org/2004/07/Microsoft.Search.Autopilot.Evolution" |
| 39 | +ns2 = "Microsoft.Search.Autopilot.NetMux" |
| 40 | +ns3 = "http://www.w3.org/2001/XMLSchema-instance" |
| 41 | + |
| 42 | +class minigraph_encoder(json.JSONEncoder): |
| 43 | + def default(self, obj): |
| 44 | + if isinstance(obj, (ipaddress.IPv4Network, ipaddress.IPv6Network, ipaddress.IPv4Address, ipaddress.IPv6Address)): |
| 45 | + return str(obj) |
| 46 | + return json.JSONEncoder.default(self, obj) |
| 47 | + |
| 48 | +def parse_png(png, hname): |
| 49 | + neighbors = {} |
| 50 | + devices = {} |
| 51 | + console_dev = '' |
| 52 | + console_port = '' |
| 53 | + mgmt_dev = '' |
| 54 | + mgmt_port = '' |
| 55 | + for child in png: |
| 56 | + if child.tag == str(QName(ns, "DeviceInterfaceLinks")): |
| 57 | + for link in child.findall(str(QName(ns, "DeviceLinkBase"))): |
| 58 | + linktype = link.find(str(QName(ns, "ElementType"))).text |
| 59 | + if linktype != "DeviceInterfaceLink" and linktype != "UnderlayInterfaceLink": |
| 60 | + continue |
| 61 | + |
| 62 | + enddevice = link.find(str(QName(ns, "EndDevice"))).text |
| 63 | + endport = link.find(str(QName(ns, "EndPort"))).text |
| 64 | + startdevice = link.find(str(QName(ns, "StartDevice"))).text |
| 65 | + startport = link.find(str(QName(ns, "StartPort"))).text |
| 66 | + |
| 67 | + if enddevice == hname: |
| 68 | + neighbors[endport] = {'name': startdevice, 'port': startport} |
| 69 | + else: |
| 70 | + neighbors[startport] = {'name': enddevice, 'port': endport} |
| 71 | + if child.tag == str(QName(ns, "Devices")): |
| 72 | + for device in child.findall(str(QName(ns, "Device"))): |
| 73 | + lo_addr = None |
| 74 | + # don't shadow type() |
| 75 | + d_type = None |
| 76 | + mgmt_addr = None |
| 77 | + hwsku = None |
| 78 | + if str(QName(ns3, "type")) in device.attrib: |
| 79 | + d_type = device.attrib[str(QName(ns3, "type"))] |
| 80 | + |
| 81 | + for node in device: |
| 82 | + if node.tag == str(QName(ns, "Address")): |
| 83 | + lo_addr = node.find(str(QName(ns2, "IPPrefix"))).text.split('/')[0] |
| 84 | + elif node.tag == str(QName(ns, "ManagementAddress")): |
| 85 | + mgmt_addr = node.find(str(QName(ns2, "IPPrefix"))).text.split('/')[0] |
| 86 | + elif node.tag == str(QName(ns, "Hostname")): |
| 87 | + name = node.text |
| 88 | + elif node.tag == str(QName(ns, "HwSku")): |
| 89 | + hwsku = node.text |
| 90 | + |
| 91 | + devices[name] = {'lo_addr': lo_addr, 'type': d_type, 'mgmt_addr': mgmt_addr, 'hwsku': hwsku} |
| 92 | + |
| 93 | + if child.tag == str(QName(ns, "DeviceInterfaceLinks")): |
| 94 | + for if_link in child.findall(str(QName(ns, 'DeviceLinkBase'))): |
| 95 | + if str(QName(ns3, "type")) in if_link.attrib: |
| 96 | + link_type = if_link.attrib[str(QName(ns3, "type"))] |
| 97 | + if link_type == 'DeviceSerialLink': |
| 98 | + for node in if_link: |
| 99 | + if node.tag == str(QName(ns, "EndPort")): |
| 100 | + console_port = node.text.split()[-1] |
| 101 | + elif node.tag == str(QName(ns, "EndDevice")): |
| 102 | + console_dev = node.text |
| 103 | + elif link_type == 'DeviceMgmtLink': |
| 104 | + for node in if_link: |
| 105 | + if node.tag == str(QName(ns, "EndPort")): |
| 106 | + mgmt_port = node.text.split()[-1] |
| 107 | + elif node.tag == str(QName(ns, "EndDevice")): |
| 108 | + mgmt_dev = node.text |
| 109 | + |
| 110 | + |
| 111 | + return (neighbors, devices, console_dev, console_port, mgmt_dev, mgmt_port) |
| 112 | + |
| 113 | + |
| 114 | +def parse_dpg(dpg, hname): |
| 115 | + for child in dpg: |
| 116 | + hostname = child.find(str(QName(ns, "Hostname"))) |
| 117 | + if hostname.text != hname: |
| 118 | + continue |
| 119 | + |
| 120 | + ipintfs = child.find(str(QName(ns, "IPInterfaces"))) |
| 121 | + intfs = [] |
| 122 | + vlan_map = {} |
| 123 | + for ipintf in ipintfs.findall(str(QName(ns, "IPInterface"))): |
| 124 | + intfname = ipintf.find(str(QName(ns, "AttachTo"))).text |
| 125 | + ipprefix = ipintf.find(str(QName(ns, "Prefix"))).text |
| 126 | + ipn = ipaddress.IPNetwork(ipprefix) |
| 127 | + ipaddr = ipn.ip |
| 128 | + prefix_len = ipn.prefixlen |
| 129 | + addr_bits = ipn.max_prefixlen |
| 130 | + subnet = ipaddress.IPNetwork(str(ipn.network) + '/' + str(prefix_len)) |
| 131 | + ipmask = ipn.netmask |
| 132 | + |
| 133 | + intf = {'addr': ipaddr, 'subnet': subnet} |
| 134 | + if isinstance(ipn, ipaddress.IPv4Network): |
| 135 | + intf['mask'] = ipmask |
| 136 | + else: |
| 137 | + intf['mask'] = str(prefix_len) |
| 138 | + |
| 139 | + if intfname[0:4] == "Vlan": |
| 140 | + if intfname in vlan_map: |
| 141 | + vlan_map[intfname].append(intf) |
| 142 | + |
| 143 | + else: |
| 144 | + vlan_map[intfname] = [intf] |
| 145 | + else: |
| 146 | + intf.update({'name': intfname, 'prefixlen': int(prefix_len)}) |
| 147 | + |
| 148 | + if port_alias_map.has_key(intfname): |
| 149 | + intf['alias'] = port_alias_map[intfname] |
| 150 | + else: |
| 151 | + intf['alias'] = intfname |
| 152 | + |
| 153 | + # TODO: remove peer_addr after dependency removed |
| 154 | + ipaddr_val = int(ipn.ip) |
| 155 | + peer_addr_val = None |
| 156 | + if int(prefix_len) == addr_bits - 2: |
| 157 | + if ipaddr_val & 0x3 == 1: |
| 158 | + peer_addr_val = ipaddr_val + 1 |
| 159 | + else: |
| 160 | + peer_addr_val = ipaddr_val - 1 |
| 161 | + elif int(prefix_len) == addr_bits - 1: |
| 162 | + if ipaddr_val & 0x1 == 0: |
| 163 | + peer_addr_val = ipaddr_val + 1 |
| 164 | + else: |
| 165 | + peer_addr_val = ipaddr_val - 1 |
| 166 | + |
| 167 | + if peer_addr_val is not None: |
| 168 | + intf['peer_addr'] = ipaddress.IPAddress(peer_addr_val) |
| 169 | + intfs.append(intf) |
| 170 | + |
| 171 | + pcintfs = child.find(str(QName(ns, "PortChannelInterfaces"))) |
| 172 | + pc_intfs = [] |
| 173 | + for pcintf in pcintfs.findall(str(QName(ns, "PortChannel"))): |
| 174 | + pcintfname = pcintf.find(str(QName(ns, "Name"))).text |
| 175 | + pcintfmbr = pcintf.find(str(QName(ns, "AttachTo"))).text |
| 176 | + pcmbr_list = pcintfmbr.split(';', 1) |
| 177 | + pc_intfs.append({'name': pcintfname, 'members': pcmbr_list}) |
| 178 | + |
| 179 | + lointfs = child.find(str(QName(ns, "LoopbackIPInterfaces"))) |
| 180 | + lo_intfs = [] |
| 181 | + for lointf in lointfs.findall(str(QName(ns1, "LoopbackIPInterface"))): |
| 182 | + intfname = lointf.find(str(QName(ns, "AttachTo"))).text |
| 183 | + ipprefix = lointf.find(str(QName(ns1, "PrefixStr"))).text |
| 184 | + ipn = ipaddress.IPNetwork(ipprefix) |
| 185 | + ipaddr = ipn.ip |
| 186 | + prefix_len = ipn.prefixlen |
| 187 | + ipmask = ipn.netmask |
| 188 | + lo_intf = {'name': intfname, 'addr': ipaddr, 'prefixlen': prefix_len} |
| 189 | + if isinstance(ipn, ipaddress.IPv4Network): |
| 190 | + lo_intf['mask'] = ipmask |
| 191 | + else: |
| 192 | + lo_intf['mask'] = str(prefix_len) |
| 193 | + lo_intfs.append(lo_intf) |
| 194 | + |
| 195 | + mgmtintfs = child.find(str(QName(ns, "ManagementIPInterfaces"))) |
| 196 | + mgmt_intf = None |
| 197 | + for mgmtintf in mgmtintfs.findall(str(QName(ns1, "ManagementIPInterface"))): |
| 198 | + ipprefix = mgmtintf.find(str(QName(ns1, "PrefixStr"))).text |
| 199 | + mgmtipn = ipaddress.IPNetwork(ipprefix) |
| 200 | + ipaddr = mgmtipn.ip |
| 201 | + prefix_len = str(mgmtipn.prefixlen) |
| 202 | + ipmask = mgmtipn.netmask |
| 203 | + gwaddr = ipaddress.IPAddress(int(mgmtipn.network) + 1) |
| 204 | + mgmt_intf = {'addr': ipaddr, 'prefixlen': prefix_len, 'mask': ipmask, 'gwaddr': gwaddr} |
| 205 | + |
| 206 | + vlanintfs = child.find(str(QName(ns, "VlanInterfaces"))) |
| 207 | + vlan_intfs = [] |
| 208 | + for vintf in vlanintfs.findall(str(QName(ns, "VlanInterface"))): |
| 209 | + vintfname = vintf.find(str(QName(ns, "Name"))).text |
| 210 | + vlanid = vintf.find(str(QName(ns, "VlanID"))).text |
| 211 | + vintfmbr = vintf.find(str(QName(ns, "AttachTo"))).text |
| 212 | + vmbr_list = vintfmbr.split(';')) |
| 213 | + vlan_attributes = {'name': vintfname, 'members': vmbr_list, 'vlanid': vlanid} |
| 214 | + for addrtuple in vlan_map.get(vintfname, []): |
| 215 | + vlan_attributes.update(addrtuple) |
| 216 | + vlan_intfs.append(copy.deepcopy(vlan_attributes)) |
| 217 | + |
| 218 | + return intfs, lo_intfs, mgmt_intf, vlan_intfs, pc_intfs |
| 219 | + return None, None, None, None, None |
| 220 | + |
| 221 | +def parse_cpg(cpg, hname): |
| 222 | + bgp_sessions = [] |
| 223 | + myasn = None |
| 224 | + for child in cpg: |
| 225 | + tag = child.tag |
| 226 | + if tag == str(QName(ns, "PeeringSessions")): |
| 227 | + for session in child.findall(str(QName(ns, "BGPSession"))): |
| 228 | + start_router = session.find(str(QName(ns, "StartRouter"))).text |
| 229 | + start_peer = session.find(str(QName(ns, "StartPeer"))).text |
| 230 | + end_router = session.find(str(QName(ns, "EndRouter"))).text |
| 231 | + end_peer = session.find(str(QName(ns, "EndPeer"))).text |
| 232 | + if end_router == hname: |
| 233 | + bgp_sessions.append({ |
| 234 | + 'name': start_router, |
| 235 | + 'addr': start_peer, |
| 236 | + 'peer_addr': end_peer |
| 237 | + }) |
| 238 | + else: |
| 239 | + bgp_sessions.append({ |
| 240 | + 'name': end_router, |
| 241 | + 'addr': end_peer, |
| 242 | + 'peer_addr': start_peer |
| 243 | + }) |
| 244 | + elif child.tag == str(QName(ns, "Routers")): |
| 245 | + for router in child.findall(str(QName(ns1, "BGPRouterDeclaration"))): |
| 246 | + asn = router.find(str(QName(ns1, "ASN"))).text |
| 247 | + hostname = router.find(str(QName(ns1, "Hostname"))).text |
| 248 | + if hostname == hname: |
| 249 | + myasn = int(asn) |
| 250 | + else: |
| 251 | + for bgp_session in bgp_sessions: |
| 252 | + if hostname == bgp_session['name']: |
| 253 | + bgp_session['asn'] = int(asn) |
| 254 | + |
| 255 | + return bgp_sessions, myasn |
| 256 | + |
| 257 | + |
| 258 | +def get_console_info(devices, dev, port): |
| 259 | + for k, v in devices.items(): |
| 260 | + if k == dev: |
| 261 | + break |
| 262 | + else: |
| 263 | + return {} |
| 264 | + |
| 265 | + ret_val = v |
| 266 | + ret_val.update({ |
| 267 | + 'ts_port': port, |
| 268 | + 'ts_dev': dev |
| 269 | + }) |
| 270 | + |
| 271 | + return ret_val |
| 272 | + |
| 273 | +def get_mgmt_info(devices, dev, port): |
| 274 | + for k, v in devices.items(): |
| 275 | + if k == dev: |
| 276 | + break |
| 277 | + else: |
| 278 | + return {} |
| 279 | + |
| 280 | + ret_val = v |
| 281 | + ret_val.update({ |
| 282 | + 'mgmt_port': port, |
| 283 | + 'mgmt_dev': dev |
| 284 | + }) |
| 285 | + |
| 286 | + return ret_val |
| 287 | + |
| 288 | +def parse_xml(filename): |
| 289 | + root = ET.parse(filename).getroot() |
| 290 | + mini_graph_path = filename |
| 291 | + |
| 292 | + u_neighbors = None |
| 293 | + u_devices = None |
| 294 | + hwsku = None |
| 295 | + bgp_sessions = None |
| 296 | + bgp_asn = None |
| 297 | + intfs = None |
| 298 | + vlan_intfs = None |
| 299 | + pc_intfs = None |
| 300 | + mgmt_intf = None |
| 301 | + lo_intf = None |
| 302 | + neighbors = None |
| 303 | + devices = None |
| 304 | + hostname = None |
| 305 | + |
| 306 | + hwsku_qn = QName(ns, "HwSku") |
| 307 | + hostname_qn = QName(ns, "Hostname") |
| 308 | + for child in root: |
| 309 | + if child.tag == str(hwsku_qn): |
| 310 | + hwsku = child.text |
| 311 | + if child.tag == str(hostname_qn): |
| 312 | + hostname = child.text |
| 313 | + |
| 314 | + # port_alias_map maps ngs port name to sonic port name |
| 315 | + if hwsku == "Force10-S6000": |
| 316 | + for i in range(0, 128, 4): |
| 317 | + port_alias_map["fortyGigE0/%d" % i] = "Ethernet%d" % i |
| 318 | + elif hwsku == "Arista-7050-QX32": |
| 319 | + for i in range(1, 25): |
| 320 | + port_alias_map["Ethernet%d/1" % i] = "Ethernet%d" % ((i - 1) * 4) |
| 321 | + for i in range(25, 33): |
| 322 | + port_alias_map["Ethernet%d" % i] = "Ethernet%d" % ((i - 1) * 4) |
| 323 | + |
| 324 | + for child in root: |
| 325 | + if child.tag == str(QName(ns, "DpgDec")): |
| 326 | + (intfs, lo_intfs, mgmt_intf, vlan_intfs, pc_intfs) = parse_dpg(child, hostname) |
| 327 | + elif child.tag == str(QName(ns, "CpgDec")): |
| 328 | + (bgp_sessions, bgp_asn) = parse_cpg(child, hostname) |
| 329 | + elif child.tag == str(QName(ns, "PngDec")): |
| 330 | + (neighbors, devices, console_dev, console_port, mgmt_dev, mgmt_port) = parse_png(child, hostname) |
| 331 | + elif child.tag == str(QName(ns, "UngDec")): |
| 332 | + (u_neighbors, u_devices, _, _, _, _) = parse_png(child, hostname) |
| 333 | + |
| 334 | + # Replace port with alias in Vlan interfaces members |
| 335 | + for vlan in vlan_intfs: |
| 336 | + for i,member in enumerate(vlan['members']): |
| 337 | + vlan['members'][i] = port_alias_map[member] |
| 338 | + |
| 339 | + # Convert vlan members into a space-delimited string |
| 340 | + vlan['members'] = " ".join(vlan['members']) |
| 341 | + |
| 342 | + # Replace port with alias in port channel interfaces members |
| 343 | + for pc in pc_intfs: |
| 344 | + for i,member in enumerate(pc['members']): |
| 345 | + pc['members'][i] = port_alias_map[member] |
| 346 | + |
| 347 | + Tree = lambda: defaultdict(Tree) |
| 348 | + |
| 349 | + results = Tree() |
| 350 | + results['minigraph_hwsku'] = hwsku |
| 351 | + # sorting by lambdas are not easily done without custom filters. |
| 352 | + # TODO: add jinja2 filter to accept a lambda to sort a list of dictionaries by attribute. |
| 353 | + # TODO: alternatively (preferred), implement class containers for multiple-attribute entries, enabling sort by attr |
| 354 | + results['minigraph_bgp'] = sorted(bgp_sessions, key=lambda x: x['addr']) |
| 355 | + results['minigraph_bgp_asn'] = bgp_asn |
| 356 | + # TODO: sort does not work properly on all interfaces of varying lengths. Need to sort by integer group(s). |
| 357 | + results['minigraph_interfaces'] = sorted(intfs, key=lambda x: x['name']) |
| 358 | + results['minigraph_vlan_interfaces'] = vlan_intfs |
| 359 | + results['minigraph_portchannel_interfaces'] = pc_intfs |
| 360 | + results['minigraph_mgmt_interface'] = mgmt_intf |
| 361 | + results['minigraph_lo_interfaces'] = lo_intfs |
| 362 | + results['minigraph_neighbors'] = neighbors |
| 363 | + results['minigraph_devices'] = devices |
| 364 | + results['minigraph_underlay_neighbors'] = u_neighbors |
| 365 | + results['minigraph_underlay_devices'] = u_devices |
| 366 | + results['minigraph_as_xml'] = mini_graph_path |
| 367 | + results['minigraph_console'] = get_console_info(devices, console_dev, console_port) |
| 368 | + results['minigraph_mgmt'] = get_mgmt_info(devices, mgmt_dev, mgmt_port) |
| 369 | + results['inventory_hostname'] = hostname |
| 370 | + |
| 371 | + return results |
| 372 | + |
| 373 | + |
| 374 | +port_alias_map = {} |
| 375 | + |
| 376 | + |
| 377 | +def print_parse_xml(filename): |
| 378 | + results = parse_xml(filename) |
| 379 | + print(json.dumps(results, indent=3, cls=minigraph_encoder)) |
| 380 | + |
| 381 | + |
0 commit comments