blob: a88740b90942f18be63ef60fca200839c1aec6dd [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
233 # If interface address is in destination subnet, we don't need this route
234 if netaddr.IPNetwork(address).ip in netaddr.IPNetwork(destination):
235 continue
236
237 for dest_addr in destination.split():
238 new_route = {
239 "to": dest_addr,
240 "via": str(netaddr.IPNetwork(reserved_ip["ip4"]).ip),
241 "metric": 100,
242 }
243
244 if new_route not in routes:
245 routes.append(new_route)
246
247 self.netplan_config["vlans"][intfName] = {
248 "id": vlan_object.vid,
249 "link": interface["instance"].label,
250 "addresses": interface["addresses"],
251 }
252
253 # Only the fabric virtual interface will need to route to other network segments
254 if routes and "fab" in intfName:
255 self.netplan_config["vlans"][intfName]["routes"] = routes
256
257 return self.netplan_config
258
259 def generate_nftables(self):
260
261 ret = dict()
262
263 internal_if = None
264 external_if = None
265
266 # Use isPrimary == True as the identifier to select external interface
267 for interface in self.interfaces.values():
268 if interface["isPrimary"] is True:
269 external_if = interface["instance"]
270
271 if external_if is None:
272 logger.error("The primary interface wasn't set for device %s", self.name)
273 sys.exit(1)
274
275 for interface in self.interfaces.values():
276 # If "isVirtual" set to False and "mgmtOnly" set to False
277 if (
278 not interface["isVirtual"]
279 and not interface["mgmtOnly"]
280 and interface["instance"] is not external_if
281 ):
282 internal_if = interface["instance"]
283 break
284
285 ret["external_if"] = external_if.name
286 ret["internal_if"] = internal_if.name
287
288 if self.services:
289 ret["services"] = list()
290
291 for service in self.services:
292 ret["services"].append(
293 {
294 "name": service.name,
295 "protocol": service.protocol.value,
296 "port": service.port,
297 }
298 )
299
300 # Only management server needs to be configured the whitelist netrange of internal interface
301 if self.data.device_role.name == "Router":
302
303 ret["interface_subnets"] = dict()
304 ret["ue_routing"] = dict()
305 ret["ue_routing"]["ue_subnets"] = self.data.config_context["ue_subnets"]
306
307 # Create the interface_subnets in the configuration
308 # It's using the interface as the key to list IP addresses
309 for intfName, interface in self.interfaces.items():
310 if interface["mgmtOnly"]:
311 continue
312
313 for address in interface["addresses"]:
314 for prefix in PrefixContainer().all():
315 intfAddr = netaddr.IPNetwork(address).ip
316
317 # If interface IP doesn't belong to this prefix, skip
318 if intfAddr not in netaddr.IPNetwork(prefix.subnet):
319 continue
320
321 # If prefix is a parent prefix (parent prefix won't config domain name)
322 # skip to add in interface_subnets
323 if not prefix.data.description:
324 continue
325
326 ret["interface_subnets"].setdefault(intfName, list())
327
328 if prefix.subnet not in ret["interface_subnets"][intfName]:
329 ret["interface_subnets"][intfName].append(prefix.subnet)
330 for neighbor in prefix.neighbor:
331 if neighbor.subnet not in ret["interface_subnets"][intfName]:
332 ret["interface_subnets"][intfName].append(neighbor.subnet)
333
334 for prefix in PrefixContainer().all():
335
336 if "fab" in prefix.data.description:
337 ret["ue_routing"].setdefault("src_subnets", [])
338 ret["ue_routing"]["src_subnets"].append(prefix.data.prefix)
339
340 if (
341 not ret["ue_routing"].get("snat_addr")
342 and "fab" in prefix.data.description
343 ):
344 for interface in self.interfaces.values():
345 for address in interface["addresses"]:
346 if address in netaddr.IPSet([prefix.subnet]):
347 ret["ue_routing"]["snat_addr"] = str(
348 netaddr.IPNetwork(address).ip
349 )
350 break
351
352 return ret
353
354 def generate_extra_config(self):
355 """
356 Generate the extra configs which need in management server configuration
357 This function should only be called when the device role is "Router"
358 """
359
360 if self.extra_config:
361 return self.extra_config
362
363 primary_ip = self.data.primary_ip.address if self.data.primary_ip else None
364
365 service_names = list(map(lambda x: x.name, self.services))
366
367 if "dns" in service_names:
368 unbound_listen_ips = []
369 unbound_allow_ips = []
370
371 for interface in self.interfaces.values():
372 if not interface["isPrimary"] and not interface["mgmtOnly"]:
373 for address in interface["addresses"]:
374 unbound_listen_ips.append(address)
375
376 for prefix in PrefixContainer().all():
377 if prefix.data.description:
378 unbound_allow_ips.append(prefix.data.prefix)
379
380 if unbound_listen_ips:
381 self.extra_config["unbound_listen_ips"] = unbound_listen_ips
382
383 if unbound_allow_ips:
384 self.extra_config["unbound_allow_ips"] = unbound_allow_ips
385
386 if "ntp" in service_names:
387 ntp_client_allow = []
388
389 for prefix in PrefixContainer().all():
390 if prefix.data.description:
391 ntp_client_allow.append(prefix.data.prefix)
392
393 if ntp_client_allow:
394 self.extra_config["ntp_client_allow"] = ntp_client_allow
395
396 return self.extra_config
397
398
399class Device(AssignedObject):
400 """
401 Wraps a single Netbox device
402 Also caches all known devices in a class variable (devs)
403 """
404
405 def __init__(self, data):
406
407 super().__init__(data)
408 DeviceContainer().add(self.id, self)
409
410 @property
411 def type(self):
412 return "Device"
413
414 def get_interfaces(self):
415 if not self.interfaces:
416 self.interfaces = self.nbapi.dcim.interfaces.filter(device_id=self.id)
417
418 return self.interfaces
419
420
421class VirtualMachine(AssignedObject):
422 """
423 VM equivalent of Device
424 """
425
426 def __init__(self, data):
427
428 super().__init__(data)
429 VirtualMachineContainer().add(self.id, self)
430
431 @property
432 def type(self):
433 return "VirtualMachine"
434
435 def get_interfaces(self):
436 if not self.interfaces:
437 self.interfaces = self.nbapi.virtualization.interfaces.filter(
438 virtual_machine_id=self.id
439 )
440
441 return self.interfaces