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