blob: 2f3748af8f57eac24a4c2b83e85dcac283f48569 [file] [log] [blame]
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +08001import netaddr
2
3
4class 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
13class 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
28class 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
77class DeviceContainer(AssignedObjectContainer, metaclass=Singleton):
78 # DeviceContainer holds all devices fetch from Netbox, device_id as key
79 pass
80
81
82class VirtualMachineContainer(AssignedObjectContainer, metaclass=Singleton):
83 # DeviceContainer holds all devices fetch from Netbox, vm_id as key
84 pass
85
86
87class 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 Chenfc59b682021-09-13 13:54:16 +0800105 elif not ip_addr:
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800106 if prefix.reserved_ips:
107 ret.extend(list(prefix.reserved_ips.values()))
108
109 return ret
110
111
112class 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
147 self.instances[prefix.data.description] = {
148 "domain": domain,
149 "subnet": prefix.subnet,
150 "router": routes,
151 "dhcprange": prefix.dhcp_range or "",
152 "hosts": dict(),
153 "dnsServer": None,
154 "ntpServer": None,
155 "dhcpServer": None,
156 }
157
158 # Find the service IP address for this network
159 serviceMap = {
160 "dnsServer": deviceContainer.getDNSServer()
161 or vmContainer.getDNSServer(),
162 "ntpServer": deviceContainer.getNTPServer()
163 or vmContainer.getNTPServer(),
164 "dhcpServer": deviceContainer.getDHCPServer()
165 or vmContainer.getDHCPServer(),
166 }
167
168 # Loop through the device's IP, and set IP to dataset
169 for service, device in serviceMap.items():
170 for interface in device.interfaces.values():
171 for address in interface["addresses"]:
172 address = netaddr.IPNetwork(address).ip
173 if address in subnet:
174 self.instances[domain][service] = {
175 "name": device.name,
176 "address": str(address),
177 }
178 else:
179 for neighbor in prefix.neighbor:
180 neighborSubnet = netaddr.IPNetwork(neighbor.subnet)
181 if address in neighborSubnet:
182 self.instances[domain][service] = {
183 "name": device.name,
184 "address": str(address),
185 }
186 break
187
188 # A dict to check if the device exists in this domain (prefix)
189 deviceInDomain = dict()
190
191 # Gather all Devices/VMs in this tenant, build IP/Hostname map
192 for device in list(deviceContainer.all()) + list(vmContainer.all()):
193
194 # Iterate the interface owned by device
195 for intfName, interface in device.interfaces.items():
196
197 # Iterate the address with each interface
198 for address in interface["addresses"]:
199
200 # Extract the IP address from interface's IP (w/o netmask)
201 address = netaddr.IPNetwork(address).ip
202
203 # Only record the IP address in current subnet,
204 # Skip if the mac_address is blank
205 if address in subnet:
206
207 # deviceInDomain store the interface information like:
208 # {"mgmtserver1": {
209 # "non-mgmt-counter": 1,
210 # "interface": {
211 # "bmc": {
212 # "mgmtOnly": True,
213 # "macaddr": "ca:fe:ba:be:00:00",
214 # "ipaddr": [IPAddress("10.32.4.1")]
215 # }
216 # "eno1": {
217 # "mgmtOnly": False,
218 # "macaddr": "ca:fe:ba:be:11:11",
219 # "ipaddr": [IPAddress("10.32.4.129"), IPAddress("10.32.4.130")]
220 # }
221 # }
222 # "mgmtswitch1": ...
223 # }
224
225 deviceInDomain.setdefault(device.name, dict())
226 deviceInDomain[device.name].setdefault(
227 "non-mgmt-counter", 0
228 )
229 deviceInDomain[device.name].setdefault("interfaces", dict())
230
231 # Set up a interface structure in deviceInDomain[device.name]
232 deviceInDomain[device.name]["interfaces"].setdefault(
233 intfName, dict()
234 )
235 interfaceDict = deviceInDomain[device.name]["interfaces"][
236 intfName
237 ]
238 interfaceDict.setdefault("mgmtOnly", False)
239
240 # Use interface["mac_address"] as the default value, but if the mac_address
241 # is None, that means we are dealing with a virtual interfaces
242 # so we can get the linked interface's mac_address instead
243
244 interfaceDict.setdefault(
245 "mac_address", interface["mac_address"] or
246 device.interfaces[interface["instance"].label]["mac_address"]
247 )
248 interfaceDict.setdefault("ip_addresses", list())
249 interfaceDict["ip_addresses"].append(address)
250
251 # If the interface is mgmtOnly, set the attribute to True
252 # Otherwise, increase the non-mgmt-counter, the counter uses to
253 # find out how many interfaces of this device has IPs on subnet
254 if interface["mgmtOnly"]:
255 interfaceDict["mgmtOnly"] = True
256 else:
257 deviceInDomain[device.name]["non-mgmt-counter"] += 1
258
259 for deviceName, data in deviceInDomain.items():
260
261 nonMgmtCounter = data["non-mgmt-counter"]
262 for intfName, interface in data["interfaces"].items():
263
264 # If current interface doesn't have mac address set, skip
265 if not interface["mac_address"]:
266 continue
267
268 # In the default situation, hostname is deviceName
269 hostname_list = [deviceName]
270
271 # If the condition is -
272 # 1. multiple interfaces show on this subnet
273 # 2. the interface is a management-only interface
274 # then add interface name into hostname
275 if nonMgmtCounter > 1 or interface["mgmtOnly"]:
276 hostname_list.append(intfName)
277
278 # Iterate the IP address owns by current interface,
279 # if the interface has multiple IP addresses,
280 # add last digit to hostname for identifiability
281 for address in interface["ip_addresses"]:
282 hostname = hostname_list.copy()
283 if len(interface["ip_addresses"]) > 1:
284 hostname.append(str(address.words[-1]))
285
286 self.instances[domain]["hosts"][str(address)] = {
287 "hostname": "-".join(hostname),
288 "macaddr": interface["mac_address"].lower(),
289 }
290
291 self.initialized = True