blob: 05f7ee0747e8f191d9737c53feb55132063dcba2 [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()
Wei-Yu Chen9b55d362021-09-22 11:04:31 +0800313 ret["ue_routing"]["ue_subnets"] = self.data.config_context.pop("ue_subnets")
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800314
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"
Wei-Yu Chen9b55d362021-09-22 11:04:31 +0800371
372 Extra config includes: service configuring parameters, additional config context
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800373 """
374
375 if self.extra_config:
376 return self.extra_config
377
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800378 service_names = list(map(lambda x: x.name, self.services))
379
380 if "dns" in service_names:
381 unbound_listen_ips = []
382 unbound_allow_ips = []
383
384 for interface in self.interfaces.values():
385 if not interface["isPrimary"] and not interface["mgmtOnly"]:
386 for address in interface["addresses"]:
387 unbound_listen_ips.append(address)
388
389 for prefix in PrefixContainer().all():
390 if prefix.data.description:
391 unbound_allow_ips.append(prefix.data.prefix)
392
393 if unbound_listen_ips:
394 self.extra_config["unbound_listen_ips"] = unbound_listen_ips
395
396 if unbound_allow_ips:
397 self.extra_config["unbound_allow_ips"] = unbound_allow_ips
398
399 if "ntp" in service_names:
400 ntp_client_allow = []
401
402 for prefix in PrefixContainer().all():
403 if prefix.data.description:
404 ntp_client_allow.append(prefix.data.prefix)
405
406 if ntp_client_allow:
407 self.extra_config["ntp_client_allow"] = ntp_client_allow
408
Wei-Yu Chen9b55d362021-09-22 11:04:31 +0800409 # If the key exists in generated config, warning with the key name
410 for key in self.data.config_context.keys():
411 if key in self.extra_config:
412 logger.warning("Extra config Key %s was overwritten", key)
413
414 self.extra_config.update(self.data.config_context)
415
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800416 return self.extra_config
417
418
419class Device(AssignedObject):
420 """
421 Wraps a single Netbox device
422 Also caches all known devices in a class variable (devs)
423 """
424
425 def __init__(self, data):
426
427 super().__init__(data)
428 DeviceContainer().add(self.id, self)
429
430 @property
431 def type(self):
432 return "Device"
433
434 def get_interfaces(self):
435 if not self.interfaces:
436 self.interfaces = self.nbapi.dcim.interfaces.filter(device_id=self.id)
437
438 return self.interfaces
439
440
441class VirtualMachine(AssignedObject):
442 """
443 VM equivalent of Device
444 """
445
446 def __init__(self, data):
447
448 super().__init__(data)
449 VirtualMachineContainer().add(self.id, self)
450
451 @property
452 def type(self):
453 return "VirtualMachine"
454
455 def get_interfaces(self):
456 if not self.interfaces:
457 self.interfaces = self.nbapi.virtualization.interfaces.filter(
458 virtual_machine_id=self.id
459 )
460
461 return self.interfaces