blob: 94b03dfadccdef639e9d469e8db07e42a163fe1d [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
7#
8
9import netaddr
10
11from .utils import logger, clean_name_dns
12from .network import Prefix
13from .container import DeviceContainer, VirtualMachineContainer, PrefixContainer
14
15
16class AssignedObject:
17 """
18 Assigned Object is either a Device or Virtual Machine, which function
19 nearly identically in the NetBox data model.
20
21 This parent class holds common functions for those two child classes
22
23 An assignedObject (device or VM) should have following attributes:
24 - self.data: contains the original copy of data from NetBox
25 - self.id: Device ID or VM ID
26 - self.interfaces: A dictionary contains interfaces belong to this AO
27 the interface dictionary looks like:
28
29 {
30 "eno1": {
31 "address": ["192.168.0.1/24", "192.168.0.2/24"],
32 "instance": <interface_instance>,
33 "isPrimary": True,
34 "mgmtOnly": False,
35 "isVirtual": False
36 }
37 }
38 """
39
40 objects = dict()
41
42 def __init__(self, data):
43 from .utils import netboxapi, netbox_config
44
45 self.data = data
46 self.nbapi = netboxapi
47
48 # The AssignedObject attributes
49 self.id = self.data.id
50 self.tenant = None
51 self.primary_ip = None
52
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
111 if address.address == self.primary_ip.address:
112 interface["isPrimary"] = True
113
114 # mgmt_only = False is a hack for VirtualMachine type
115 if self.__class__ == VirtualMachine:
116 interface["instance"].mgmt_only = False
117
118 def __repr__(self):
119 return str(dict(self.data))
120
121 @property
122 def type(self):
123 return "AssignedObject"
124
125 @property
126 def internal_interfaces(self):
127 """
128 The function internal_interfaces
129 """
130
131 ret = dict()
132 for intfName, interface in self.interfaces.items():
133 if (
134 not interface["isPrimary"]
135 and not interface["mgmtOnly"]
136 and interface["addresses"]
137 ):
138 ret[intfName] = interface
139
140 return ret
141
142 def generate_netplan(self):
143 """
144 Get the interface config of specific server belongs to this tenant
145 """
146
147 if self.netplan_config:
148 return self.netplan_config
149
150 primary_if = None
151 for interface in self.interfaces.values():
152 if interface["isPrimary"] is True:
153 primary_if = interface["instance"]
154
155 if primary_if is None:
156 logger.error("The primary interface wasn't set for device %s", self.name)
157 return dict()
158
159 # Initialize the part of "ethernets" configuration
160 self.netplan_config["ethernets"] = dict()
161
162 # If the current selected device is a Router
163 if (isinstance(self, Device) and self.data.device_role.name == "Router") or (
164 isinstance(self, VirtualMachine) and self.data.role.name == "Router"
165 ):
166 for intfName, interface in self.interfaces.items():
167 if interface["mgmtOnly"] or interface["isVirtual"]:
168 continue
169
170 # Check if this address is public IP address (e.g. "8.8.8.8" on eth0)
171 isExternalAddress = True
172 for prefix in PrefixContainer().all():
173 for address in interface["addresses"]:
174 if address in netaddr.IPSet([prefix.subnet]):
175 isExternalAddress = False
176
177 # If this interface has the public IP address, netplan shouldn't include it
178 if isExternalAddress:
179 continue
180
181 self.netplan_config["ethernets"].setdefault(intfName, {})
182 self.netplan_config["ethernets"][intfName].setdefault(
183 "addresses", []
184 ).append(address)
185
186 # If the current selected device is a Server
187 elif isinstance(self, Device) and self.data.device_role.name == "Server":
188 if primary_if:
189 self.netplan_config["ethernets"][primary_if.name] = {
190 "dhcp4": "yes",
191 "dhcp4-overrides": {"route-metric": 100},
192 }
193
194 for intfName, interface in self.interfaces.items():
195 if (
196 not interface["isVirtual"]
197 and intfName != primary_if.name
198 and not interface["mgmtOnly"]
199 and interface["addresses"]
200 ):
201 self.netplan_config["ethernets"][intfName] = {
202 "dhcp4": "yes",
203 "dhcp4-overrides": {"route-metric": 200},
204 }
205
206 else:
207 # Exclude the device type which is not Router and Server
208 return None
209
210 # Get interfaces own by AssignedObject and is virtual (VLAN interface)
211 for intfName, interface in self.interfaces.items():
212
213 # If the interface is not a virtual interface or
214 # the interface doesn't have VLAN tagged, skip this interface
215 if not interface["isVirtual"] or not interface["instance"].tagged_vlans:
216 continue
217
218 if "vlans" not in self.netplan_config:
219 self.netplan_config["vlans"] = dict()
220
221 vlan_object_id = interface["instance"].tagged_vlans[0].id
222 vlan_object = self.nbapi.ipam.vlans.get(vlan_object_id)
223
224 routes = list()
225 for address in interface["addresses"]:
226
227 for reserved_ip in PrefixContainer().all_reserved_ips(address):
228
229 destination = reserved_ip["custom_fields"].get("rfc3442routes", "")
230 if not destination:
231 continue
232
Wei-Yu Chenc7d68312021-09-14 17:12:34 +0800233 for dest_addr in destination.split(","):
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800234
Wei-Yu Chenc7d68312021-09-14 17:12:34 +0800235 # If interface address is in destination subnet, we don't need this route
236 if netaddr.IPNetwork(address).ip in netaddr.IPNetwork(dest_addr):
237 continue
238
Wei-Yu Chenbd495ba2021-08-31 19:46:35 +0800239 new_route = {
240 "to": dest_addr,
241 "via": str(netaddr.IPNetwork(reserved_ip["ip4"]).ip),
242 "metric": 100,
243 }
244
245 if new_route not in routes:
246 routes.append(new_route)
247
248 self.netplan_config["vlans"][intfName] = {
249 "id": vlan_object.vid,
250 "link": interface["instance"].label,
251 "addresses": interface["addresses"],
252 }
253
254 # Only the fabric virtual interface will need to route to other network segments
255 if routes and "fab" in intfName:
256 self.netplan_config["vlans"][intfName]["routes"] = routes
257
258 return self.netplan_config
259
260 def generate_nftables(self):
261
262 ret = dict()
263
264 internal_if = None
265 external_if = None
266
267 # Use isPrimary == True as the identifier to select external interface
268 for interface in self.interfaces.values():
269 if interface["isPrimary"] is True:
270 external_if = interface["instance"]
271
272 if external_if is None:
273 logger.error("The primary interface wasn't set for device %s", self.name)
274 sys.exit(1)
275
276 for interface in self.interfaces.values():
277 # If "isVirtual" set to False and "mgmtOnly" set to False
278 if (
279 not interface["isVirtual"]
280 and not interface["mgmtOnly"]
281 and interface["instance"] is not external_if
282 ):
283 internal_if = interface["instance"]
284 break
285
286 ret["external_if"] = external_if.name
287 ret["internal_if"] = internal_if.name
288
289 if self.services:
290 ret["services"] = list()
291
292 for service in self.services:
293 ret["services"].append(
294 {
295 "name": service.name,
296 "protocol": service.protocol.value,
297 "port": service.port,
298 }
299 )
300
301 # Only management server needs to be configured the whitelist netrange of internal interface
302 if self.data.device_role.name == "Router":
303
304 ret["interface_subnets"] = dict()
305 ret["ue_routing"] = dict()
306 ret["ue_routing"]["ue_subnets"] = self.data.config_context["ue_subnets"]
307
308 # Create the interface_subnets in the configuration
309 # It's using the interface as the key to list IP addresses
310 for intfName, interface in self.interfaces.items():
311 if interface["mgmtOnly"]:
312 continue
313
314 for address in interface["addresses"]:
315 for prefix in PrefixContainer().all():
316 intfAddr = netaddr.IPNetwork(address).ip
317
318 # If interface IP doesn't belong to this prefix, skip
319 if intfAddr not in netaddr.IPNetwork(prefix.subnet):
320 continue
321
322 # If prefix is a parent prefix (parent prefix won't config domain name)
323 # skip to add in interface_subnets
324 if not prefix.data.description:
325 continue
326
327 ret["interface_subnets"].setdefault(intfName, list())
328
329 if prefix.subnet not in ret["interface_subnets"][intfName]:
330 ret["interface_subnets"][intfName].append(prefix.subnet)
331 for neighbor in prefix.neighbor:
332 if neighbor.subnet not in ret["interface_subnets"][intfName]:
333 ret["interface_subnets"][intfName].append(neighbor.subnet)
334
335 for prefix in PrefixContainer().all():
336
337 if "fab" in prefix.data.description:
338 ret["ue_routing"].setdefault("src_subnets", [])
339 ret["ue_routing"]["src_subnets"].append(prefix.data.prefix)
340
341 if (
342 not ret["ue_routing"].get("snat_addr")
343 and "fab" in prefix.data.description
344 ):
345 for interface in self.interfaces.values():
346 for address in interface["addresses"]:
347 if address in netaddr.IPSet([prefix.subnet]):
348 ret["ue_routing"]["snat_addr"] = str(
349 netaddr.IPNetwork(address).ip
350 )
351 break
352
353 return ret
354
355 def generate_extra_config(self):
356 """
357 Generate the extra configs which need in management server configuration
358 This function should only be called when the device role is "Router"
359 """
360
361 if self.extra_config:
362 return self.extra_config
363
364 primary_ip = self.data.primary_ip.address if self.data.primary_ip else None
365
366 service_names = list(map(lambda x: x.name, self.services))
367
368 if "dns" in service_names:
369 unbound_listen_ips = []
370 unbound_allow_ips = []
371
372 for interface in self.interfaces.values():
373 if not interface["isPrimary"] and not interface["mgmtOnly"]:
374 for address in interface["addresses"]:
375 unbound_listen_ips.append(address)
376
377 for prefix in PrefixContainer().all():
378 if prefix.data.description:
379 unbound_allow_ips.append(prefix.data.prefix)
380
381 if unbound_listen_ips:
382 self.extra_config["unbound_listen_ips"] = unbound_listen_ips
383
384 if unbound_allow_ips:
385 self.extra_config["unbound_allow_ips"] = unbound_allow_ips
386
387 if "ntp" in service_names:
388 ntp_client_allow = []
389
390 for prefix in PrefixContainer().all():
391 if prefix.data.description:
392 ntp_client_allow.append(prefix.data.prefix)
393
394 if ntp_client_allow:
395 self.extra_config["ntp_client_allow"] = ntp_client_allow
396
397 return self.extra_config
398
399
400class Device(AssignedObject):
401 """
402 Wraps a single Netbox device
403 Also caches all known devices in a class variable (devs)
404 """
405
406 def __init__(self, data):
407
408 super().__init__(data)
409 DeviceContainer().add(self.id, self)
410
411 @property
412 def type(self):
413 return "Device"
414
415 def get_interfaces(self):
416 if not self.interfaces:
417 self.interfaces = self.nbapi.dcim.interfaces.filter(device_id=self.id)
418
419 return self.interfaces
420
421
422class VirtualMachine(AssignedObject):
423 """
424 VM equivalent of Device
425 """
426
427 def __init__(self, data):
428
429 super().__init__(data)
430 VirtualMachineContainer().add(self.id, self)
431
432 @property
433 def type(self):
434 return "VirtualMachine"
435
436 def get_interfaces(self):
437 if not self.interfaces:
438 self.interfaces = self.nbapi.virtualization.interfaces.filter(
439 virtual_machine_id=self.id
440 )
441
442 return self.interfaces