blob: 397ee32e83d9fb35646e9c77a13fb8f3c0214b89 [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
Wei-Yu Chenc7d68312021-09-14 17:12:34 +0800147 # 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 Chenbd495ba2021-08-31 19:46:35 +0800152 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