blob: 0e728a1facabc89ff616511c299b3827294fc386 [file] [log] [blame]
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +08001import netaddr
Zack Williamsdac2be42021-08-19 16:14:31 -07002from .utils import logger
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +08003
4
5class 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 Williamsdac2be42021-08-19 16:14:31 -070014class Container:
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080015 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
29class 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 Williamsdac2be42021-08-19 16:14:31 -070040 return None
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080041
42 def getDHCPServer(self):
43 for instance in self.instances.values():
44 if "tftp" in list(map(str, instance.services)):
45 return instance
Zack Williamsdac2be42021-08-19 16:14:31 -070046 return None
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080047
48 def getNTPServer(self):
49 for instance in self.instances.values():
50 if "ntp" in list(map(str, instance.services)):
51 return instance
Zack Williamsdac2be42021-08-19 16:14:31 -070052 return None
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080053
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 Williamsdac2be42021-08-19 16:14:31 -070079 return None
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080080
81
82class DeviceContainer(AssignedObjectContainer, metaclass=Singleton):
83 # DeviceContainer holds all devices fetch from Netbox, device_id as key
84 pass
85
86
87class VirtualMachineContainer(AssignedObjectContainer, metaclass=Singleton):
88 # DeviceContainer holds all devices fetch from Netbox, vm_id as key
89 pass
90
91
92class PrefixContainer(Container, metaclass=Singleton):
93 # PrefixContainer holds all prefixes fetch from Netbox, prefix(str) as key
94
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080095 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 Chenfc59b682021-09-13 13:54:16 +0800107 elif not ip_addr:
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800108 if prefix.reserved_ips:
109 ret.extend(list(prefix.reserved_ips.values()))
110
111 return ret
112
113
114class 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 Chenc7d68312021-09-14 17:12:34 +0800149 # 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 Chenbd495ba2021-08-31 19:46:35 +0800154 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 Williamsdac2be42021-08-19 16:14:31 -0700226 # "ipaddr": [IPAddress("10.32.4.129"),
227 # IPAddress("10.32.4.130")]
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800228 # }
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 Williamsdac2be42021-08-19 16:14:31 -0700248 # 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 Chenbd495ba2021-08-31 19:46:35 +0800251
Zack Williamsdac2be42021-08-19 16:14:31 -0700252 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 Chenbd495ba2021-08-31 19:46:35 +0800266 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