blob: 32a2075852b3a5d531936f112ecd19c7504d8092 [file] [log] [blame]
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +08001#!/usr/bin/env python3
2
3# SPDX-FileCopyrightText: © 2021 Open Networking Foundation <support@opennetworking.org>
4# SPDX-License-Identifier: Apache-2.0
5
6# device.py
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +08007
Zack Williamsdac2be42021-08-19 16:14:31 -07008import sys
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +08009import netaddr
10
Zack Williamsdac2be42021-08-19 16:14:31 -070011from .utils import logger
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080012from .container import DeviceContainer, VirtualMachineContainer, PrefixContainer
13
14
15class AssignedObject:
16 """
17 Assigned Object is either a Device or Virtual Machine, which function
18 nearly identically in the NetBox data model.
19
20 This parent class holds common functions for those two child classes
21
22 An assignedObject (device or VM) should have following attributes:
23 - self.data: contains the original copy of data from NetBox
24 - self.id: Device ID or VM ID
25 - self.interfaces: A dictionary contains interfaces belong to this AO
26 the interface dictionary looks like:
27
28 {
29 "eno1": {
30 "address": ["192.168.0.1/24", "192.168.0.2/24"],
31 "instance": <interface_instance>,
32 "isPrimary": True,
33 "mgmtOnly": False,
34 "isVirtual": False
35 }
36 }
37 """
38
39 objects = dict()
40
41 def __init__(self, data):
Zack Williamsdac2be42021-08-19 16:14:31 -070042 from .utils import netboxapi
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080043
44 self.data = data
45 self.nbapi = netboxapi
46
47 # The AssignedObject attributes
48 self.id = self.data.id
49 self.tenant = None
50 self.primary_ip = None
Zack Williamsdac2be42021-08-19 16:14:31 -070051 self.primary_iface = None
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +080052
53 # In Netbox, we use FQDN as the Device name, but in the script,
54 # we use the first segment to be the name of device.
55 # For example, if the device named "mgmtserver1.stage1.menlo" on Netbox,
56 # then we will have "mgmtserver1" as name.
57 self.fullname = self.data.name
58 self.name = self.fullname.split(".")[0]
59
60 # The device role which can be ["server", "router", "switch", ...]
61 self.role = None
62
63 # The NetBox objects related with this AssignedObject
64 self.interfaces = dict()
65 self.services = None
66
67 # Generated configuration for ansible playbooks
68 self.netplan_config = dict()
69 self.extra_config = dict()
70
71 if self.__class__ == Device:
72 self.role = self.data.device_role.slug
73 self.services = self.nbapi.ipam.services.filter(device_id=self.id)
74 interfaces = self.nbapi.dcim.interfaces.filter(device_id=self.id)
75 ip_addresses = self.nbapi.ipam.ip_addresses.filter(device_id=self.id)
76 elif self.__class__ == VirtualMachine:
77 self.role = self.data.role.slug
78 self.services = self.nbapi.ipam.services.filter(virtual_machine_id=self.id)
79 interfaces = self.nbapi.virtualization.interfaces.filter(
80 virtual_machine_id=self.id
81 )
82 ip_addresses = self.nbapi.ipam.ip_addresses.filter(
83 virtual_machine_id=self.id
84 )
85
86 self.primary_ip = self.data.primary_ip
87
88 for interface in interfaces:
89 # The Device's interface structure is different from VM's interface
90 # VM interface doesn't have mgmt_only and type, Therefore,
91 # the default value of mgmtOnly is False, isVirtual is True
92
93 self.interfaces[interface.name] = {
94 "addresses": list(),
95 "mac_address": interface.mac_address,
96 "instance": interface,
97 "isPrimary": False,
98 "mgmtOnly": getattr(interface, "mgmt_only", False),
99 "isVirtual": interface.type.value == "virtual"
100 if hasattr(interface, "type")
101 else True,
102 }
103
104 for address in ip_addresses:
105 interface = self.interfaces[address.assigned_object.name]
106 interface["addresses"].append(address.address)
107
108 # ipam.ip_addresses doesn't have primary tag,
109 # the primary tag is only available is only in the Device.
110 # So we need to compare address to check which one is primary ip
Zack Williamsdac2be42021-08-19 16:14:31 -0700111 try:
112 if address.address == self.primary_ip.address:
113 interface["isPrimary"] = True
114 self.primary_iface = interface
115 except AttributeError:
116 logger.error("Error with primary address for device %s", self.fullname)
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800117
118 # mgmt_only = False is a hack for VirtualMachine type
119 if self.__class__ == VirtualMachine:
120 interface["instance"].mgmt_only = False
121
122 def __repr__(self):
123 return str(dict(self.data))
124
125 @property
126 def type(self):
127 return "AssignedObject"
128
129 @property
130 def internal_interfaces(self):
131 """
132 The function internal_interfaces
133 """
134
135 ret = dict()
136 for intfName, interface in self.interfaces.items():
137 if (
138 not interface["isPrimary"]
139 and not interface["mgmtOnly"]
140 and interface["addresses"]
141 ):
142 ret[intfName] = interface
143
144 return ret
145
146 def generate_netplan(self):
147 """
148 Get the interface config of specific server belongs to this tenant
149 """
150
151 if self.netplan_config:
152 return self.netplan_config
153
154 primary_if = None
155 for interface in self.interfaces.values():
156 if interface["isPrimary"] is True:
157 primary_if = interface["instance"]
158
159 if primary_if is None:
160 logger.error("The primary interface wasn't set for device %s", self.name)
161 return dict()
162
163 # Initialize the part of "ethernets" configuration
164 self.netplan_config["ethernets"] = dict()
165
166 # If the current selected device is a Router
167 if (isinstance(self, Device) and self.data.device_role.name == "Router") or (
168 isinstance(self, VirtualMachine) and self.data.role.name == "Router"
169 ):
170 for intfName, interface in self.interfaces.items():
171 if interface["mgmtOnly"] or interface["isVirtual"]:
172 continue
173
174 # Check if this address is public IP address (e.g. "8.8.8.8" on eth0)
175 isExternalAddress = True
176 for prefix in PrefixContainer().all():
177 for address in interface["addresses"]:
178 if address in netaddr.IPSet([prefix.subnet]):
179 isExternalAddress = False
180
181 # If this interface has the public IP address, netplan shouldn't include it
182 if isExternalAddress:
183 continue
184
185 self.netplan_config["ethernets"].setdefault(intfName, {})
186 self.netplan_config["ethernets"][intfName].setdefault(
187 "addresses", []
Zack Williamsdac2be42021-08-19 16:14:31 -0700188 ).extend(interface["addresses"])
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800189
190 # If the current selected device is a Server
191 elif isinstance(self, Device) and self.data.device_role.name == "Server":
192 if primary_if:
193 self.netplan_config["ethernets"][primary_if.name] = {
194 "dhcp4": "yes",
195 "dhcp4-overrides": {"route-metric": 100},
196 }
197
198 for intfName, interface in self.interfaces.items():
199 if (
200 not interface["isVirtual"]
201 and intfName != primary_if.name
202 and not interface["mgmtOnly"]
203 and interface["addresses"]
204 ):
205 self.netplan_config["ethernets"][intfName] = {
206 "dhcp4": "yes",
207 "dhcp4-overrides": {"route-metric": 200},
208 }
209
210 else:
211 # Exclude the device type which is not Router and Server
212 return None
213
214 # Get interfaces own by AssignedObject and is virtual (VLAN interface)
215 for intfName, interface in self.interfaces.items():
216
217 # If the interface is not a virtual interface or
218 # the interface doesn't have VLAN tagged, skip this interface
219 if not interface["isVirtual"] or not interface["instance"].tagged_vlans:
220 continue
221
222 if "vlans" not in self.netplan_config:
223 self.netplan_config["vlans"] = dict()
224
225 vlan_object_id = interface["instance"].tagged_vlans[0].id
226 vlan_object = self.nbapi.ipam.vlans.get(vlan_object_id)
227
228 routes = list()
229 for address in interface["addresses"]:
230
231 for reserved_ip in PrefixContainer().all_reserved_ips(address):
232
233 destination = reserved_ip["custom_fields"].get("rfc3442routes", "")
234 if not destination:
235 continue
236
Wei-Yu Chenc7d68312021-09-14 17:12:34 +0800237 for dest_addr in destination.split(","):
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800238
Wei-Yu Chenc7d68312021-09-14 17:12:34 +0800239 # If interface address is in destination subnet, we don't need this route
Zack Williamsdac2be42021-08-19 16:14:31 -0700240 if netaddr.IPNetwork(address).ip in netaddr.IPNetwork(
241 dest_addr
242 ):
Wei-Yu Chenc7d68312021-09-14 17:12:34 +0800243 continue
244
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800245 new_route = {
246 "to": dest_addr,
247 "via": str(netaddr.IPNetwork(reserved_ip["ip4"]).ip),
248 "metric": 100,
249 }
250
251 if new_route not in routes:
252 routes.append(new_route)
253
254 self.netplan_config["vlans"][intfName] = {
255 "id": vlan_object.vid,
256 "link": interface["instance"].label,
257 "addresses": interface["addresses"],
258 }
259
260 # Only the fabric virtual interface will need to route to other network segments
261 if routes and "fab" in intfName:
262 self.netplan_config["vlans"][intfName]["routes"] = routes
263
264 return self.netplan_config
265
266 def generate_nftables(self):
267
268 ret = dict()
269
270 internal_if = None
271 external_if = None
272
273 # Use isPrimary == True as the identifier to select external interface
274 for interface in self.interfaces.values():
275 if interface["isPrimary"] is True:
276 external_if = interface["instance"]
277
278 if external_if is None:
279 logger.error("The primary interface wasn't set for device %s", self.name)
280 sys.exit(1)
281
282 for interface in self.interfaces.values():
283 # If "isVirtual" set to False and "mgmtOnly" set to False
284 if (
285 not interface["isVirtual"]
286 and not interface["mgmtOnly"]
287 and interface["instance"] is not external_if
288 ):
289 internal_if = interface["instance"]
290 break
291
292 ret["external_if"] = external_if.name
293 ret["internal_if"] = internal_if.name
294
295 if self.services:
296 ret["services"] = list()
297
298 for service in self.services:
299 ret["services"].append(
300 {
301 "name": service.name,
302 "protocol": service.protocol.value,
303 "port": service.port,
304 }
305 )
306
Zack Williamsdac2be42021-08-19 16:14:31 -0700307 # Only management server needs to be configured the whitelist netrange of
308 # internal interface
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800309 if self.data.device_role.name == "Router":
310
311 ret["interface_subnets"] = dict()
312 ret["ue_routing"] = dict()
313 ret["ue_routing"]["ue_subnets"] = self.data.config_context["ue_subnets"]
314
315 # Create the interface_subnets in the configuration
316 # It's using the interface as the key to list IP addresses
317 for intfName, interface in self.interfaces.items():
318 if interface["mgmtOnly"]:
319 continue
320
321 for address in interface["addresses"]:
322 for prefix in PrefixContainer().all():
323 intfAddr = netaddr.IPNetwork(address).ip
324
325 # If interface IP doesn't belong to this prefix, skip
326 if intfAddr not in netaddr.IPNetwork(prefix.subnet):
327 continue
328
329 # If prefix is a parent prefix (parent prefix won't config domain name)
330 # skip to add in interface_subnets
331 if not prefix.data.description:
332 continue
333
334 ret["interface_subnets"].setdefault(intfName, list())
335
336 if prefix.subnet not in ret["interface_subnets"][intfName]:
337 ret["interface_subnets"][intfName].append(prefix.subnet)
338 for neighbor in prefix.neighbor:
Zack Williamsdac2be42021-08-19 16:14:31 -0700339 if (
340 neighbor.subnet
341 not in ret["interface_subnets"][intfName]
342 ):
343 ret["interface_subnets"][intfName].append(
344 neighbor.subnet
345 )
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800346
347 for prefix in PrefixContainer().all():
348
349 if "fab" in prefix.data.description:
350 ret["ue_routing"].setdefault("src_subnets", [])
351 ret["ue_routing"]["src_subnets"].append(prefix.data.prefix)
352
353 if (
354 not ret["ue_routing"].get("snat_addr")
355 and "fab" in prefix.data.description
356 ):
357 for interface in self.interfaces.values():
358 for address in interface["addresses"]:
359 if address in netaddr.IPSet([prefix.subnet]):
360 ret["ue_routing"]["snat_addr"] = str(
361 netaddr.IPNetwork(address).ip
362 )
363 break
364
365 return ret
366
367 def generate_extra_config(self):
368 """
369 Generate the extra configs which need in management server configuration
370 This function should only be called when the device role is "Router"
371 """
372
373 if self.extra_config:
374 return self.extra_config
375
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800376 service_names = list(map(lambda x: x.name, self.services))
377
378 if "dns" in service_names:
379 unbound_listen_ips = []
380 unbound_allow_ips = []
381
382 for interface in self.interfaces.values():
383 if not interface["isPrimary"] and not interface["mgmtOnly"]:
384 for address in interface["addresses"]:
385 unbound_listen_ips.append(address)
386
387 for prefix in PrefixContainer().all():
388 if prefix.data.description:
389 unbound_allow_ips.append(prefix.data.prefix)
390
391 if unbound_listen_ips:
392 self.extra_config["unbound_listen_ips"] = unbound_listen_ips
393
394 if unbound_allow_ips:
395 self.extra_config["unbound_allow_ips"] = unbound_allow_ips
396
397 if "ntp" in service_names:
398 ntp_client_allow = []
399
400 for prefix in PrefixContainer().all():
401 if prefix.data.description:
402 ntp_client_allow.append(prefix.data.prefix)
403
404 if ntp_client_allow:
405 self.extra_config["ntp_client_allow"] = ntp_client_allow
406
407 return self.extra_config
408
409
410class Device(AssignedObject):
411 """
412 Wraps a single Netbox device
413 Also caches all known devices in a class variable (devs)
414 """
415
416 def __init__(self, data):
417
418 super().__init__(data)
419 DeviceContainer().add(self.id, self)
420
421 @property
422 def type(self):
423 return "Device"
424
425 def get_interfaces(self):
426 if not self.interfaces:
427 self.interfaces = self.nbapi.dcim.interfaces.filter(device_id=self.id)
428
429 return self.interfaces
430
431
432class VirtualMachine(AssignedObject):
433 """
434 VM equivalent of Device
435 """
436
437 def __init__(self, data):
438
439 super().__init__(data)
440 VirtualMachineContainer().add(self.id, self)
441
442 @property
443 def type(self):
444 return "VirtualMachine"
445
446 def get_interfaces(self):
447 if not self.interfaces:
448 self.interfaces = self.nbapi.virtualization.interfaces.filter(
449 virtual_machine_id=self.id
450 )
451
452 return self.interfaces