Wei-Yu Chen | bd495ba | 2021-08-31 19:46:35 +0800 | [diff] [blame] | 1 | import netaddr |
| 2 | |
| 3 | |
| 4 | class Singleton(type): |
| 5 | _instances = {} |
| 6 | |
| 7 | def __call__(cls, *args, **kwargs): |
| 8 | if cls not in cls._instances: |
| 9 | cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs) |
| 10 | return cls._instances[cls] |
| 11 | |
| 12 | |
| 13 | class Container(object): |
| 14 | def __init__(self): |
| 15 | self.instances = dict() |
| 16 | |
| 17 | def add(self, instance_id, instance): |
| 18 | if instance_id in self.instances: |
| 19 | raise Exception() |
| 20 | self.instances[instance_id] = instance |
| 21 | |
| 22 | def get(self, instance_id): |
| 23 | if instance_id not in self.instances: |
| 24 | return instance_id |
| 25 | return self.instances[instance_id] |
| 26 | |
| 27 | |
| 28 | class AssignedObjectContainer(Container): |
| 29 | # AssignedObjectContainer is the parent class |
| 30 | # which is with the shared function of Devices/VMs Container |
| 31 | |
| 32 | def all(self): |
| 33 | return self.instances.values() |
| 34 | |
| 35 | def getDNSServer(self): |
| 36 | for instance in self.instances.values(): |
| 37 | if "dns" in list(map(str, instance.services)): |
| 38 | return instance |
| 39 | |
| 40 | def getDHCPServer(self): |
| 41 | for instance in self.instances.values(): |
| 42 | if "tftp" in list(map(str, instance.services)): |
| 43 | return instance |
| 44 | |
| 45 | def getNTPServer(self): |
| 46 | for instance in self.instances.values(): |
| 47 | if "ntp" in list(map(str, instance.services)): |
| 48 | return instance |
| 49 | |
| 50 | def getRouters(self): |
| 51 | """ Get a list of Devices/VMs which type is Router """ |
| 52 | |
| 53 | ret = list() |
| 54 | for instance in self.instances.values(): |
| 55 | if instance.role == "router": |
| 56 | ret.append(instance) |
| 57 | |
| 58 | return ret |
| 59 | |
| 60 | def getRouterIPforPrefix(self, prefix): |
| 61 | """ Get the first found IP address of exist routers as string """ |
| 62 | |
| 63 | for router in self.getRouters(): |
| 64 | for interface in router.interfaces.values(): |
| 65 | |
| 66 | # The mgmt-only interface will not act as gateway |
| 67 | if interface["mgmtOnly"]: |
| 68 | continue |
| 69 | |
| 70 | for address in interface["addresses"]: |
| 71 | if netaddr.IPNetwork(address).ip in netaddr.IPNetwork( |
| 72 | prefix.subnet |
| 73 | ): |
| 74 | return str(netaddr.IPNetwork(address).ip) |
| 75 | |
| 76 | |
| 77 | class DeviceContainer(AssignedObjectContainer, metaclass=Singleton): |
| 78 | # DeviceContainer holds all devices fetch from Netbox, device_id as key |
| 79 | pass |
| 80 | |
| 81 | |
| 82 | class VirtualMachineContainer(AssignedObjectContainer, metaclass=Singleton): |
| 83 | # DeviceContainer holds all devices fetch from Netbox, vm_id as key |
| 84 | pass |
| 85 | |
| 86 | |
| 87 | class PrefixContainer(Container, metaclass=Singleton): |
| 88 | # PrefixContainer holds all prefixes fetch from Netbox, prefix(str) as key |
| 89 | |
| 90 | def get(self, instance_id, name_segments=1): |
| 91 | return super().get(instance_id) |
| 92 | |
| 93 | def all(self): |
| 94 | return self.instances.values() |
| 95 | |
| 96 | def all_reserved_ips(self, ip_addr=""): |
| 97 | ret = list() |
| 98 | |
| 99 | for prefix in self.instances.values(): |
| 100 | if ip_addr and netaddr.IPNetwork(ip_addr).ip in netaddr.IPNetwork( |
| 101 | prefix.subnet |
| 102 | ): |
| 103 | if prefix.reserved_ips: |
| 104 | return list(prefix.reserved_ips.values()) |
Wei-Yu Chen | fc59b68 | 2021-09-13 13:54:16 +0800 | [diff] [blame] | 105 | elif not ip_addr: |
Wei-Yu Chen | bd495ba | 2021-08-31 19:46:35 +0800 | [diff] [blame] | 106 | if prefix.reserved_ips: |
| 107 | ret.extend(list(prefix.reserved_ips.values())) |
| 108 | |
| 109 | return ret |
| 110 | |
| 111 | |
| 112 | class ServiceInfoContainer(Container, metaclass=Singleton): |
| 113 | # ServiceInfoContainer holds hosts DNS/DHCP information in Tenant networks |
| 114 | |
| 115 | def __init__(self): |
| 116 | super().__init__() |
| 117 | self.initialized = False |
| 118 | |
| 119 | def all(self): |
| 120 | return self.instances.items() |
| 121 | |
| 122 | def initialize(self): |
| 123 | if self.initialized: |
| 124 | return |
| 125 | |
| 126 | deviceContainer = DeviceContainer() |
| 127 | vmContainer = VirtualMachineContainer() |
| 128 | prefixContainer = PrefixContainer() |
| 129 | |
| 130 | for prefix in prefixContainer.all(): |
| 131 | |
| 132 | subnet = netaddr.IPNetwork(prefix.subnet) |
| 133 | domain = prefix.data.description or "" |
| 134 | |
| 135 | if not domain: |
| 136 | continue |
| 137 | |
| 138 | # If prefix has set the Router IP, use this value as defualt, |
| 139 | # otherwise find router in this subnet and get its IP address |
| 140 | routes = ( |
| 141 | prefix.routes |
| 142 | or deviceContainer.getRouterIPforPrefix(prefix) |
| 143 | or vmContainer.getRouterIPforPrefix(prefix) |
| 144 | or "" |
| 145 | ) |
| 146 | |
Wei-Yu Chen | c7d6831 | 2021-09-14 17:12:34 +0800 | [diff] [blame^] | 147 | # When prefix.routes is None, we will get the RouterIP from prefix instance |
| 148 | # And to output a normal format, we need to build as a list here. |
| 149 | if isinstance(routes, str): |
| 150 | routes = [{"ip": routes}] |
| 151 | |
Wei-Yu Chen | bd495ba | 2021-08-31 19:46:35 +0800 | [diff] [blame] | 152 | self.instances[prefix.data.description] = { |
| 153 | "domain": domain, |
| 154 | "subnet": prefix.subnet, |
| 155 | "router": routes, |
| 156 | "dhcprange": prefix.dhcp_range or "", |
| 157 | "hosts": dict(), |
| 158 | "dnsServer": None, |
| 159 | "ntpServer": None, |
| 160 | "dhcpServer": None, |
| 161 | } |
| 162 | |
| 163 | # Find the service IP address for this network |
| 164 | serviceMap = { |
| 165 | "dnsServer": deviceContainer.getDNSServer() |
| 166 | or vmContainer.getDNSServer(), |
| 167 | "ntpServer": deviceContainer.getNTPServer() |
| 168 | or vmContainer.getNTPServer(), |
| 169 | "dhcpServer": deviceContainer.getDHCPServer() |
| 170 | or vmContainer.getDHCPServer(), |
| 171 | } |
| 172 | |
| 173 | # Loop through the device's IP, and set IP to dataset |
| 174 | for service, device in serviceMap.items(): |
| 175 | for interface in device.interfaces.values(): |
| 176 | for address in interface["addresses"]: |
| 177 | address = netaddr.IPNetwork(address).ip |
| 178 | if address in subnet: |
| 179 | self.instances[domain][service] = { |
| 180 | "name": device.name, |
| 181 | "address": str(address), |
| 182 | } |
| 183 | else: |
| 184 | for neighbor in prefix.neighbor: |
| 185 | neighborSubnet = netaddr.IPNetwork(neighbor.subnet) |
| 186 | if address in neighborSubnet: |
| 187 | self.instances[domain][service] = { |
| 188 | "name": device.name, |
| 189 | "address": str(address), |
| 190 | } |
| 191 | break |
| 192 | |
| 193 | # A dict to check if the device exists in this domain (prefix) |
| 194 | deviceInDomain = dict() |
| 195 | |
| 196 | # Gather all Devices/VMs in this tenant, build IP/Hostname map |
| 197 | for device in list(deviceContainer.all()) + list(vmContainer.all()): |
| 198 | |
| 199 | # Iterate the interface owned by device |
| 200 | for intfName, interface in device.interfaces.items(): |
| 201 | |
| 202 | # Iterate the address with each interface |
| 203 | for address in interface["addresses"]: |
| 204 | |
| 205 | # Extract the IP address from interface's IP (w/o netmask) |
| 206 | address = netaddr.IPNetwork(address).ip |
| 207 | |
| 208 | # Only record the IP address in current subnet, |
| 209 | # Skip if the mac_address is blank |
| 210 | if address in subnet: |
| 211 | |
| 212 | # deviceInDomain store the interface information like: |
| 213 | # {"mgmtserver1": { |
| 214 | # "non-mgmt-counter": 1, |
| 215 | # "interface": { |
| 216 | # "bmc": { |
| 217 | # "mgmtOnly": True, |
| 218 | # "macaddr": "ca:fe:ba:be:00:00", |
| 219 | # "ipaddr": [IPAddress("10.32.4.1")] |
| 220 | # } |
| 221 | # "eno1": { |
| 222 | # "mgmtOnly": False, |
| 223 | # "macaddr": "ca:fe:ba:be:11:11", |
| 224 | # "ipaddr": [IPAddress("10.32.4.129"), IPAddress("10.32.4.130")] |
| 225 | # } |
| 226 | # } |
| 227 | # "mgmtswitch1": ... |
| 228 | # } |
| 229 | |
| 230 | deviceInDomain.setdefault(device.name, dict()) |
| 231 | deviceInDomain[device.name].setdefault( |
| 232 | "non-mgmt-counter", 0 |
| 233 | ) |
| 234 | deviceInDomain[device.name].setdefault("interfaces", dict()) |
| 235 | |
| 236 | # Set up a interface structure in deviceInDomain[device.name] |
| 237 | deviceInDomain[device.name]["interfaces"].setdefault( |
| 238 | intfName, dict() |
| 239 | ) |
| 240 | interfaceDict = deviceInDomain[device.name]["interfaces"][ |
| 241 | intfName |
| 242 | ] |
| 243 | interfaceDict.setdefault("mgmtOnly", False) |
| 244 | |
| 245 | # Use interface["mac_address"] as the default value, but if the mac_address |
| 246 | # is None, that means we are dealing with a virtual interfaces |
| 247 | # so we can get the linked interface's mac_address instead |
| 248 | |
| 249 | interfaceDict.setdefault( |
| 250 | "mac_address", interface["mac_address"] or |
| 251 | device.interfaces[interface["instance"].label]["mac_address"] |
| 252 | ) |
| 253 | interfaceDict.setdefault("ip_addresses", list()) |
| 254 | interfaceDict["ip_addresses"].append(address) |
| 255 | |
| 256 | # If the interface is mgmtOnly, set the attribute to True |
| 257 | # Otherwise, increase the non-mgmt-counter, the counter uses to |
| 258 | # find out how many interfaces of this device has IPs on subnet |
| 259 | if interface["mgmtOnly"]: |
| 260 | interfaceDict["mgmtOnly"] = True |
| 261 | else: |
| 262 | deviceInDomain[device.name]["non-mgmt-counter"] += 1 |
| 263 | |
| 264 | for deviceName, data in deviceInDomain.items(): |
| 265 | |
| 266 | nonMgmtCounter = data["non-mgmt-counter"] |
| 267 | for intfName, interface in data["interfaces"].items(): |
| 268 | |
| 269 | # If current interface doesn't have mac address set, skip |
| 270 | if not interface["mac_address"]: |
| 271 | continue |
| 272 | |
| 273 | # In the default situation, hostname is deviceName |
| 274 | hostname_list = [deviceName] |
| 275 | |
| 276 | # If the condition is - |
| 277 | # 1. multiple interfaces show on this subnet |
| 278 | # 2. the interface is a management-only interface |
| 279 | # then add interface name into hostname |
| 280 | if nonMgmtCounter > 1 or interface["mgmtOnly"]: |
| 281 | hostname_list.append(intfName) |
| 282 | |
| 283 | # Iterate the IP address owns by current interface, |
| 284 | # if the interface has multiple IP addresses, |
| 285 | # add last digit to hostname for identifiability |
| 286 | for address in interface["ip_addresses"]: |
| 287 | hostname = hostname_list.copy() |
| 288 | if len(interface["ip_addresses"]) > 1: |
| 289 | hostname.append(str(address.words[-1])) |
| 290 | |
| 291 | self.instances[domain]["hosts"][str(address)] = { |
| 292 | "hostname": "-".join(hostname), |
| 293 | "macaddr": interface["mac_address"].lower(), |
| 294 | } |
| 295 | |
| 296 | self.initialized = True |