blob: d91108fe16f0e8957f166645a0501561305050d3 [file] [log] [blame]
Zack Williams2aeb3ef2021-06-11 17:10:36 -07001#!/usr/bin/env python3
2
3# SPDX-FileCopyrightText: © 2021 Open Networking Foundation <support@opennetworking.org>
4# SPDX-License-Identifier: Apache-2.0
5
6# nbhelper.py
7# Helper functions for building YAML output from Netbox API calls
8
9from __future__ import absolute_import
10
11import re
12import sys
13import argparse
14import logging
15import netaddr
16import pynetbox
17import requests
18
19from ruamel import yaml
20
21# create shared logger
22logging.basicConfig()
23logger = logging.getLogger("nbh")
24
25# to dump YAML properly, using internal representers
26# see also:
27# https://stackoverflow.com/questions/54378220/declare-data-type-to-ruamel-yaml-so-that-it-can-represen-serialize-it
28# https://sourceforge.net/p/ruamel-yaml/code/ci/default/tree/representer.py
29
30ydump = yaml.YAML(typ="safe")
31ydump.representer.add_representer(
32 pynetbox.models.dcim.Devices, yaml.SafeRepresenter.represent_dict
33)
34ydump.representer.add_representer(
35 pynetbox.models.dcim.Interfaces, yaml.SafeRepresenter.represent_dict
36)
37ydump.representer.add_representer(
38 pynetbox.models.ipam.Prefixes, yaml.SafeRepresenter.represent_dict
39)
40ydump.representer.add_representer(
41 pynetbox.core.response.Record, yaml.SafeRepresenter.represent_dict
42)
43ydump.representer.add_representer(
44 pynetbox.models.ipam.IpAddresses, yaml.SafeRepresenter.represent_dict
45)
46ydump.representer.add_representer(
47 pynetbox.core.api.Api, yaml.SafeRepresenter.represent_none
48)
49
Wei-Yu Chen55a86822021-07-08 14:34:59 +080050netboxapi = None
51netbox_config = None
52netbox_version = None
53
54
55def initialize(extra_args):
56 global netboxapi, netbox_config, netbox_version
57
58 args = parse_cli_args(extra_args)
59 netbox_config = yaml.safe_load(args.settings.read())
60
61 for require_args in ["api_endpoint", "token", "tenant_name"]:
62 if not netbox_config.get(require_args):
63 logger.error("The require argument: %s was not set. Stop." % require_args)
64 sys.exit(1)
65
66 netboxapi = pynetbox.api(
67 netbox_config["api_endpoint"], token=netbox_config["token"], threading=True,
68 )
69
70 if not netbox_config.get("validate_certs", True):
71 session = requests.Session()
72 session.verify = False
73 netboxapi.http_session = session
74
75 netbox_version = netboxapi.version
76
77 return args
78
Zack Williams2aeb3ef2021-06-11 17:10:36 -070079
80def parse_cli_args(extra_args={}):
81 """
82 parse CLI arguments. Can add extra arguments with a option:kwargs dict
83 """
84
85 parser = argparse.ArgumentParser(description="Netbox")
86
87 # Positional args
88 parser.add_argument(
89 "settings",
90 type=argparse.FileType("r"),
91 help="YAML Ansible inventory file w/NetBox API token",
92 )
93
94 parser.add_argument(
95 "--debug", action="store_true", help="Print additional debugging information"
96 )
97
Wei-Yu Chen55a86822021-07-08 14:34:59 +080098 for ename, ekwargs in extra_args.items():
99 parser.add_argument(ename, **ekwargs)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700100
101 args = parser.parse_args()
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800102 log_level = logging.DEBUG if args.debug else logging.INFO
103 logger.setLevel(log_level)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700104
105 return args
106
107
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800108def check_name_dns(name):
109
110 badchars = re.search("[^a-z0-9.-]", name.lower(), re.ASCII)
111
112 if badchars:
113 logger.error(
114 "DNS name '%s' has one or more invalid characters: '%s'",
115 name,
116 badchars.group(0),
117 )
118 sys.exit(1)
119
120 return name.lower()
121
122
123def clean_name_dns(name):
124 return re.sub("[^a-z0-9.-]", "-", name.lower(), 0, re.ASCII)
125
126
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700127class AttrDict(dict):
128 def __init__(self, *args, **kwargs):
129 super(AttrDict, self).__init__(*args, **kwargs)
130 self.__dict__ = self
131
132
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800133class NBTenant:
134 def __init__(self):
135 self.name = netbox_config["tenant_name"]
136 self.name_segments = netbox_config.get("prefix_segments", 1)
137 self.tenant = netboxapi.tenancy.tenants.get(name=self.name)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700138
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800139 # NBTenant only keep the resources which owns by it
140 self.devices = list()
141 self.vms = list()
142 self.prefixes = dict()
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700143
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800144 # Get the Device and Virtual Machines from Netbox API
145 for device_data in netboxapi.dcim.devices.filter(tenant=self.tenant.slug):
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800146 device = NBDevice(device_data)
147 device.tenant = self
148 self.devices.append(device)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700149
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800150 for vm_data in netboxapi.virtualization.virtual_machines.filter(
151 tenant=self.tenant.slug
152 ):
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800153 vm = NBVirtualMachine(vm_data)
154 vm.tenant = self
155 self.vms.append(vm)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700156
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800157 def get_prefixes(self):
158 """Get the IP Prefixes owns by current tenant"""
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700159
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800160 if self.prefixes:
161 return self.prefixes
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700162
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800163 vrf = netboxapi.ipam.vrfs.get(tenant=self.tenant.slug)
164 for prefix_data in netboxapi.ipam.prefixes.filter(vrf_id=vrf.id):
165 if prefix_data.description:
166 self.prefixes[prefix_data.description] = NBPrefix(
167 prefix_data, self.name_segments
168 )
169
170 return self.prefixes
171
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800172 def get_device_by_name(self, name):
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700173 """
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800174 Find the device or VM which belongs to this Tenant,
175 If the name wasn't specified, return the management server
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700176 """
177
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800178 for machine in self.devices + self.vms:
179 if name and machine.name == name:
180 return machine
181 elif machine.data["device_role"]["name"] == "Router":
182 return machine
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700183
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800184 ret_msg = (
185 "The name '%s' wasn't found in this tenant, "
186 + "or can't found any Router in this tenant"
187 )
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700188
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800189 logger.error(ret_msg, name)
190 sys.exit(1)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700191
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800192 def get_devices(self, device_types=["server", "router"]):
Zack Williams5d66d182021-07-27 23:08:28 -0700193 """
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800194 Get all devices (Router + Server) belong to this Tenant
Zack Williams5d66d182021-07-27 23:08:28 -0700195 """
196
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800197 if not device_types:
198 return self.devices + self.vms
Zack Williams5d66d182021-07-27 23:08:28 -0700199
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800200 ret = []
Zack Williams5d66d182021-07-27 23:08:28 -0700201
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800202 for machine in self.devices:
203 if machine.data.device_role.slug in device_types:
204 ret.append(machine)
205
206 for vm in self.vms:
207 if vm.data.role.slug in device_types:
208 ret.append(vm)
209
210 return ret
Zack Williams5d66d182021-07-27 23:08:28 -0700211
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700212
213@yaml.yaml_object(ydump)
214class NBPrefix:
215
216 prefixes = {}
217
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800218 def __init__(self, data, name_segments):
219 self.data = data
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700220 self.name_segments = name_segments
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800221 self.domain_extension = check_name_dns(self.data.description)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700222
223 logger.debug(
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800224 "Preix %s: domain_extension %s, data: %s",
225 self.data.prefix,
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700226 self.domain_extension,
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800227 dict(self.data),
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700228 )
229
230 # ip centric info
231 self.dhcp_range = None
232 self.reserved_ips = {}
233 self.aos = {}
234
235 # build item lists
236 self.build_prefix()
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800237 self.prefixes[self.data.prefix] = self
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700238
239 @classmethod
240 def all_prefixes(cls):
241 return cls.prefixes
242
243 @classmethod
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800244 def get_prefix(cls, prefix, name_segments=1):
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700245 if prefix in cls.prefixes:
246 return cls.prefixes[prefix]
247
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800248 data = netboxapi.ipam.prefixes.get(prefix=prefix)
249 if data:
250 return NBPrefix(data, name_segments)
251 else:
252 raise Exception("The prefix %s wasn't found in Netbox" % prefix)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700253
254 def __repr__(self):
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800255 return str(self.data.prefix)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700256
257 @classmethod
258 def to_yaml(cls, representer, node):
259 return representer.represent_dict(
260 {
261 "dhcp_range": node.dhcp_range,
262 "reserved_ips": node.reserved_ips,
263 "aos": node.aos,
264 "prefix_data": dict(node.prefix_data),
265 }
266 )
267
Wei-Yu Chen6ca7bc12021-07-19 19:53:43 +0800268 @classmethod
269 def all_reserved_by_ip(cls, ip_addr=""):
270 """
271 all_reserved_by_ip will return all reserved IP found in prefixes
272
273 We have the IP address marked as type 'Reserved' in Prefix,
274 This type of IP address is using to define a DHCP range
275 """
276
277 ret = list()
278
279 for prefix in cls.prefixes.values():
280 if ip_addr and ip_addr in prefix.aos.keys():
281 if prefix.reserved_ips:
282 return list(prefix.reserved_ips.values())
283 else:
284 if prefix.reserved_ips:
285 ret.extend(list(prefix.reserved_ips.values()))
286
287 return ret
288
289 def get_reserved_ips(self):
290 """
291 Get the reserved IP range (DHCP) in prefix
292
293 We have the IP address marked as type 'Reserved' in Prefix,
294 This type of IP address is using to define a DHCP range
295 """
296 if prefix.reserved_ips:
297 return list(prefix.reserved_ips.values())
298
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800299 def check_ip_belonging(self, ip):
300 """
301 Check if an IP address is belonging to this prefix
302 """
303 return ip in netaddr.IPSet([self.data.prefix])
304
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700305 def parent(self):
306 """
307 Get the parent prefix to this prefix
308
309 FIXME: Doesn't handle multiple layers of prefixes, returns first found
310 """
311
312 # get all parents of this prefix (include self)
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800313 possible_parents = netboxapi.ipam.prefixes.filter(contains=self.data.prefix)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700314
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800315 logger.debug(
316 "Prefix %s: possible parents %s", self.data.prefix, possible_parents
317 )
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700318
319 # filter out self, return first found
320 for pparent in possible_parents:
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800321 if pparent.prefix != self.data.prefix:
322 return NBPrefix.get_prefix(pparent.prefix, self.name_segments)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700323
324 return None
325
326 def build_prefix(self):
327 """
328 find ip information for items (devices/vms, reserved_ips, dhcp_range) in prefix
329 """
330
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800331 ips = netboxapi.ipam.ip_addresses.filter(parent=self.data.prefix)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700332
333 for ip in sorted(ips, key=lambda k: k["address"]):
334
335 logger.debug("prefix_item ip: %s, data: %s", ip, dict(ip))
336
337 # if it's a DHCP range, add that range to the dev list as prefix_dhcp
338 if ip.status.value == "dhcp":
339 self.dhcp_range = str(ip.address)
340 continue
341
342 # reserved IPs
343 if ip.status.value == "reserved":
344
345 res = {}
346 res["name"] = ip.description.lower().split(" ")[0]
347 res["description"] = ip.description
348 res["ip4"] = str(netaddr.IPNetwork(ip.address))
349 res["custom_fields"] = ip.custom_fields
350
351 self.reserved_ips[str(ip)] = res
352 continue
353
354 # devices and VMs
355 if ip.assigned_object: # can be null if not assigned to a device/vm
356 aotype = ip.assigned_object_type
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700357 if aotype == "dcim.interface":
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800358 self.aos[str(ip)] = NBDevice.get_by_id(
359 ip.assigned_object.device.id,
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700360 )
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700361 elif aotype == "virtualization.vminterface":
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800362 self.aos[str(ip)] = NBVirtualMachine.get_by_id(
363 ip.assigned_object.virtual_machine.id,
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700364 )
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700365 else:
366 logger.error("IP %s has unknown device type: %s", ip, aotype)
367 sys.exit(1)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700368 else:
369 logger.warning("Unknown IP type %s, with attributes: %s", ip, dict(ip))
370
371
372@yaml.yaml_object(ydump)
373class NBAssignedObject:
374 """
375 Assigned Object is either a Device or Virtual Machine, which function
376 nearly identically in the NetBox data model.
377
378 This parent class holds common functions for those two child classes
379 """
380
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800381 objects = dict()
382
383 def __init__(self, data):
384 self.data = data
385
386 # The AssignedObject attributes
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800387 self.tenant = None
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800388 self.id = data.id
389 self.name = data.name
390 self.ips = dict()
391
392 # The NetBox objects related with this AssignedObject
393 self.services = None
394 self.interfaces = list()
395 self.mgmt_interfaces = list()
396 self.interfaces_by_ip = dict()
397
398 if self.__class__ == NBDevice:
399 self.interfaces = netboxapi.dcim.interfaces.filter(device_id=self.id)
400 self.services = netboxapi.ipam.services.filter(device_id=self.id)
401 ip_addresses = netboxapi.ipam.ip_addresses.filter(device_id=self.id)
402 elif self.__class__ == NBVirtualMachine:
403 self.interfaces = netboxapi.virtualization.interfaces.filter(
404 virtual_machine_id=self.id
405 )
406 self.services = netboxapi.ipam.services.filter(virtual_machine_id=self.id)
407 ip_addresses = netboxapi.ipam.ip_addresses.filter(
408 virtual_machine_id=self.id
409 )
410
411 for ip in ip_addresses:
412 self.ips[ip.address] = ip
413 if ip.assigned_object and self.__class__ == NBDevice:
414 self.interfaces_by_ip[ip.address] = netboxapi.dcim.interfaces.get(
415 ip.assigned_object_id
416 )
417 elif ip.assigned_object and self.__class__ == NBVirtualMachine:
418 self.interfaces_by_ip[
419 ip.address
420 ] = netboxapi.virtualization.interfaces.get(ip.assigned_object_id)
421 self.interfaces_by_ip[ip.address].mgmt_only = False
422
423 logger.debug(
424 "%s id: %d, data: %s, ips: %s"
425 % (self.type, self.id, dict(self.data), self.ips)
426 )
427
428 self.netplan_config = dict()
Zack Williams5d66d182021-07-27 23:08:28 -0700429 self.extra_config = dict()
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800430
431 def __repr__(self):
432 return str(dict(self.data))
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700433
434 def dns_name(self, ip, prefix):
435 """
436 Returns the DNS name for the device at this IP in the prefix
437 """
438
439 def first_segment_suffix(split_name, suffixes, segments):
440 first_seg = "-".join([split_name[0], *suffixes])
441
442 if segments > 1:
443 name = ".".join([first_seg, *split_name[1:segments]])
444 else:
445 name = first_seg
446
447 return name
448
449 # clean/split the device name
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800450 name_split = clean_name_dns(self.data.name).split(".")
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700451
452 # always add interface suffix to mgmt interfaces
453 if self.interfaces_by_ip[ip].mgmt_only:
454 return first_segment_suffix(
455 name_split, [self.interfaces_by_ip[ip].name], prefix.name_segments
456 )
457
458 # find all IP's for this device in the prefix that aren't mgmt interfaces
459 prefix_ips = []
460 for s_ip in self.ips:
461 if s_ip in prefix.aos and not self.interfaces_by_ip[s_ip].mgmt_only:
462 prefix_ips.append(s_ip)
463
464 # name to use when only one IP address for device in a prefix
465 simple_name = ".".join(name_split[0 : prefix.name_segments])
466
467 # if more than one non-mgmt IP in prefix
468 if len(prefix_ips) > 1:
469
470 # use bare name if primary IP address
471 try: # skip if no primary_ip.address
472 if ip == self.data.primary_ip.address:
473 return simple_name
474 except AttributeError:
475 pass
476
477 # else, suffix with the interface name, and the last octet of IP address
478 return first_segment_suffix(
479 name_split,
480 [
481 self.interfaces_by_ip[ip].name,
482 str(netaddr.IPNetwork(ip).ip.words[3]),
483 ],
484 prefix.name_segments,
485 )
486
487 # simplest case - only one IP in prefix, return simple_name
488 return simple_name
489
490 def dns_cnames(self, ip):
491 """
492 returns a list of cnames for this object, based on IP matches
493 """
494
495 cnames = []
496
497 for service in self.services:
498
499 # if not assigned to any IP's, service is on all IPs
500 if not service.ipaddresses:
501 cnames.append(service.name)
502 continue
503
504 # If assigned to an IP, only create a CNAME on that IP
505 for service_ip in service.ipaddresses:
506 if ip == service_ip.address:
507 cnames.append(service.name)
508
509 return cnames
510
511 def has_service(self, cidr_ip, port, protocol):
512 """
513 Return True if this AO has a service using specific port and protocol combination
514 """
515
516 if (
517 cidr_ip in self.interfaces_by_ip
518 and not self.interfaces_by_ip[cidr_ip].mgmt_only
519 ):
520 for service in self.services:
521 if service.port == port and service.protocol.value == protocol:
522 return True
523
524 return False
525
526 def primary_iface(self):
527 """
528 Returns the interface data for the device that has the primary_ip
529 """
530
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800531 if self.data.primary_ip:
532 return self.interfaces_by_ip[self.data.primary_ip.address]
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700533
534 return None
535
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800536 @property
537 def type(self):
538 return "AssignedObject"
539
540 @classmethod
541 def get_by_id(cls, obj_id):
542 raise Exception("not implemented")
543
544 @classmethod
545 def all_objects(cls):
546 return cls.objects
547
548 @classmethod
549 def to_yaml(cls, representer, node):
550 return representer.represent_dict(
551 {
552 "data": node.data,
553 "services": node.services,
554 "ips": node.ips,
555 "interfaces_by_ip": node.interfaces_by_ip,
556 }
557 )
558
559 def generate_netplan(self):
560 """
561 Get the interface config of specific server belongs to this tenant
562 """
563
564 if self.netplan_config:
565 return self.netplan_config
566
567 if not self.data:
568 logger.error(
569 "{type} {name} doesn't have data yet.".format(
570 type=self.type, name=self.name
571 )
572 )
573 sys.exit(1)
574
575 primary_ip = self.data.primary_ip.address if self.data.primary_ip else None
576 primary_if = self.interfaces_by_ip[primary_ip] if primary_ip else None
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800577
578 self.netplan_config["ethernets"] = dict()
579
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800580 if (isinstance(self, NBDevice) and self.data.device_role.name == "Router") or (
581 isinstance(self, NBVirtualMachine) and self.data.role.name == "Router"
582 ):
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800583 for address, interface in self.interfaces_by_ip.items():
Wei-Yu Chenb43fc322021-07-21 14:50:16 +0800584 if interface.mgmt_only is True or str(interface.type) == "Virtual":
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800585 continue
586
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800587 if not any(
588 [
589 p.check_ip_belonging(address)
590 for p in self.tenant.prefixes.values()
591 ]
592 ):
593 continue
594
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800595 self.netplan_config["ethernets"].setdefault(interface.name, {})
596 self.netplan_config["ethernets"][interface.name].setdefault(
597 "addresses", []
598 ).append(address)
599
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800600 elif isinstance(self, NBDevice) and self.data.device_role.name == "Server":
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800601 if primary_if:
602 self.netplan_config["ethernets"][primary_if.name] = {
603 "dhcp4": "yes",
604 "dhcp4-overrides": {"route-metric": 100},
605 }
606
Wei-Yu Chenb43fc322021-07-21 14:50:16 +0800607 for physical_if in filter(
608 lambda i: str(i.type) != "Virtual"
609 and i != primary_if
610 and i.mgmt_only is False,
611 self.interfaces,
612 ):
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800613 self.netplan_config["ethernets"][physical_if.name] = {
614 "dhcp4": "yes",
615 "dhcp4-overrides": {"route-metric": 200},
616 }
617 else:
618 # Exclude the device type which is not Router and Server
619 return None
620
621 # Get interfaces own by AssignedObject and is virtual (VLAN interface)
Wei-Yu Chenb43fc322021-07-21 14:50:16 +0800622 for virtual_if in filter(lambda i: str(i.type) == "Virtual", self.interfaces):
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800623 if "vlans" not in self.netplan_config:
624 self.netplan_config["vlans"] = dict()
625
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800626 if not virtual_if.tagged_vlans:
627 # If a virtual interface doesn't have tagged VLAN, skip
628 continue
629
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800630 # vlan_object_id is the "id" on netbox, it's different from known VLAN ID
631 vlan_object_id = virtual_if.tagged_vlans[0].id
632 vlan_object = netboxapi.ipam.vlans.get(vlan_object_id)
633 virtual_if_ips = netboxapi.ipam.ip_addresses.filter(
634 interface_id=virtual_if.id
635 )
636
Wei-Yu Chen6ca7bc12021-07-19 19:53:43 +0800637 routes = []
638 for ip in virtual_if_ips:
639 reserved_ips = NBPrefix.all_reserved_by_ip(str(ip))
640 for reserved_ip in reserved_ips:
641 destination = reserved_ip["custom_fields"].get("rfc3442routes", "")
642 if destination:
643 for dest_ip in destination.split():
644 new_route = {
645 "to": dest_ip,
646 "via": str(netaddr.IPNetwork(reserved_ip["ip4"]).ip),
647 "metric": 100,
648 }
649 if new_route not in routes:
650 routes.append(new_route)
651
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800652 self.netplan_config["vlans"][virtual_if.name] = {
653 "id": vlan_object.vid,
654 "link": virtual_if.label,
655 "addresses": [ip.address for ip in virtual_if_ips],
656 }
657
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800658 # Only the fabric virtual interface will need to route to other network segments
659 if routes and "fab" in virtual_if.name:
Wei-Yu Chen6ca7bc12021-07-19 19:53:43 +0800660 self.netplan_config["vlans"][virtual_if.name]["routes"] = routes
661
Zack Williams5d66d182021-07-27 23:08:28 -0700662 return self.netplan_config
663
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800664 def generate_nftables(self):
665
666 ret = dict()
667
668 primary_ip = self.data.primary_ip.address if self.data.primary_ip else None
669 external_if = self.interfaces_by_ip[primary_ip] if primary_ip else None
670 internal_if = None
671
672 if external_if is None:
673 logger.error("The primary interface wasn't set for device %s", self.name)
674 sys.exit(1)
675
676 for intf in filter(
677 lambda i: str(i.type) != "Virtual" and i.mgmt_only is False,
678 self.interfaces_by_ip.values(),
679 ):
680 if intf.id != external_if.id:
681 internal_if = intf
682 break
683
684 ret["external_if"] = external_if.name
685 ret["internal_if"] = internal_if.name
686
687 if self.services:
688 ret["services"] = list()
689
690 for service in self.services:
691 ret["services"].append(
692 {
693 "name": service.name,
694 "protocol": service.protocol.value,
695 "port": service.port,
696 }
697 )
698
699 # Only management server needs to be configured the whitelist netrange of internal interface
700 if self.data.device_role.name == "Router":
701 ret["allow_subnets"] = list()
702 ret["ue_routing"] = dict()
703 ret["ue_routing"]["ue_subnets"] = self.data.config_context["ue_subnets"]
704 for prefix in NBPrefix.all_prefixes().values():
705 if prefix.data.description:
706 ret["allow_subnets"].append(prefix.data.prefix)
707
708 if "fab" in prefix.data.description:
709 ret["ue_routing"].setdefault("src_subnets", [])
710 ret["ue_routing"]["src_subnets"].append(prefix.data.prefix)
711
712 if (
713 not ret["ue_routing"].get("snat_addr")
714 and "fab" in prefix.data.description
715 ):
716 for ip, device in prefix.aos.items():
717 if device.name == self.name:
Wei-Yu Chene6a71db2021-08-23 17:10:35 +0800718 ret["ue_routing"]["snat_addr"] = str(
719 netaddr.IPNetwork(ip).ip
720 )
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800721 break
722
723 return ret
724
Zack Williams5d66d182021-07-27 23:08:28 -0700725 def generate_extra_config(self):
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800726 """
727 Generate the extra configs which need in management server configuration
728 This function should only be called when the device role is "Router"
729 """
Zack Williams5d66d182021-07-27 23:08:28 -0700730
731 if self.extra_config:
732 return self.extra_config
733
734 primary_ip = self.data.primary_ip.address if self.data.primary_ip else None
735
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800736 service_names = list(map(lambda x: x.name, self.services))
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800737
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800738 if "dns" in service_names:
739 unbound_listen_ips = []
740 unbound_allow_ips = []
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800741
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800742 for ip, intf in self.interfaces_by_ip.items():
743 if ip != primary_ip and intf.mgmt_only == False:
744 unbound_listen_ips.append(ip)
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800745
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800746 for prefix in NBPrefix.all_prefixes().values():
747 if prefix.data.description:
748 unbound_allow_ips.append(prefix.data.prefix)
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800749
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800750 if unbound_listen_ips:
751 self.extra_config["unbound_listen_ips"] = unbound_listen_ips
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800752
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800753 if unbound_allow_ips:
754 self.extra_config["unbound_allow_ips"] = unbound_allow_ips
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800755
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800756 if "ntp" in service_names:
757 ntp_client_allow = []
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800758
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800759 for prefix in NBPrefix.all_prefixes().values():
760 if prefix.data.description:
761 ntp_client_allow.append(prefix.data.prefix)
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800762
Wei-Yu Chenc517f552021-07-29 21:16:45 +0800763 if ntp_client_allow:
764 self.extra_config["ntp_client_allow"] = ntp_client_allow
Wei-Yu Chen8ade3302021-07-19 20:53:47 +0800765
Zack Williams5d66d182021-07-27 23:08:28 -0700766 return self.extra_config
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800767
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700768
769@yaml.yaml_object(ydump)
770class NBDevice(NBAssignedObject):
771 """
772 Wraps a single Netbox device
773 Also caches all known devices in a class variable (devs)
774 """
775
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800776 objects = dict()
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700777
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800778 def __init__(self, data):
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700779
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800780 super().__init__(data)
781 self.objects[self.id] = self
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700782
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800783 @property
784 def type(self):
785 return "NBDevice"
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700786
787 def get_interfaces(self):
788 if not self.interfaces:
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800789 self.interfaces = netboxapi.dcim.interfaces.filter(device_id=self.id)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700790
791 return self.interfaces
792
793 @classmethod
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800794 def get_by_id(cls, obj_id):
795 obj = cls.objects.get(obj_id, None)
796 obj = obj or NBDevice(netboxapi.dcim.devices.get(obj_id))
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700797
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800798 return obj
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700799
800
801@yaml.yaml_object(ydump)
802class NBVirtualMachine(NBAssignedObject):
803 """
804 VM equivalent of NBDevice
805 """
806
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800807 objects = dict()
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700808
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800809 def __init__(self, data):
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700810
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800811 super().__init__(data)
812 self.objects[self.id] = self
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700813
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800814 @property
815 def type(self):
816 return "NBVirtualMachine"
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700817
818 def get_interfaces(self):
819 if not self.interfaces:
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800820 self.interfaces = netboxapi.virtualization.interfaces.filter(
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700821 virtual_machine_id=self.id
822 )
823
824 return self.interfaces
825
826 @classmethod
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800827 def get_by_id(cls, obj_id):
828 obj = cls.objects.get(obj_id, None)
829 obj = obj or NBVirtualMachine(
830 netboxapi.virtualization.virtual_machines.get(obj_id)
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700831 )
832
Wei-Yu Chen55a86822021-07-08 14:34:59 +0800833 return obj
834
Zack Williams2aeb3ef2021-06-11 17:10:36 -0700835
836@yaml.yaml_object(ydump)
837class NBDNSForwardZone:
838
839 fwd_zones = {}
840
841 def __init__(self, prefix):
842
843 self.domain_extension = prefix.domain_extension
844
845 self.a_recs = {}
846 self.cname_recs = {}
847 self.srv_recs = {}
848 self.ns_recs = []
849 self.txt_recs = {}
850
851 if prefix.dhcp_range:
852 self.create_dhcp_fwd(prefix.dhcp_range)
853
854 for ip, ao in prefix.aos.items():
855 self.add_ao_records(prefix, ip, ao)
856
857 for ip, res in prefix.reserved_ips.items():
858 self.add_reserved(ip, res)
859
860 # reqquired for the add_fwd_cname function below
861 if callable(getattr(prefix, "parent")):
862 parent_prefix = prefix.parent()
863
864 if parent_prefix:
865 self.merge_parent_prefix(parent_prefix, prefix)
866
867 self.fwd_zones[self.domain_extension] = self
868
869 def __repr__(self):
870 return str(
871 {
872 "a": self.a_recs,
873 "cname": self.cname_recs,
874 "ns": self.ns_recs,
875 "srv": self.srv_recs,
876 "txt": self.txt_recs,
877 }
878 )
879
880 @classmethod
881 def add_fwd_cname(cls, cname, fqdn_dest):
882 """
883 Add an arbitrary CNAME (and possibly create the fwd zone if needed) pointing
884 at a FQDN destination name. It's used to support the per-IP "DNS name" field in NetBox
885 Note that the NS record
886 """
887
888 try:
889 fqdn_split = re.compile(r"([a-z]+)\.([a-z.]+)\.")
890 (short_name, extension) = fqdn_split.match(cname).groups()
891
892 except AttributeError:
893 logger.warning(
894 "Invalid DNS CNAME: '%s', must be in FQDN format: 'host.example.com.', ignored",
895 cname,
896 )
897 return
898
899 fake_prefix = AttrDict(
900 {
901 "domain_extension": extension,
902 "dhcp_range": None,
903 "aos": {},
904 "reserved_ips": {},
905 "parent": None,
906 }
907 )
908
909 fwd_zone = cls.get_fwd_zone(fake_prefix)
910
911 fwd_zone.cname_recs[short_name] = fqdn_dest
912
913 @classmethod
914 def get_fwd_zone(cls, prefix):
915 if prefix.domain_extension in cls.fwd_zones:
916 return cls.fwd_zones[prefix.domain_extension]
917
918 return NBDNSForwardZone(prefix)
919
920 @classmethod
921 def all_fwd_zones(cls):
922 return cls.fwd_zones
923
924 @classmethod
925 def to_yaml(cls, representer, node):
926 return representer.represent_dict(
927 {
928 "a": node.a_recs,
929 "cname": node.cname_recs,
930 "ns": node.ns_recs,
931 "srv": node.srv_recs,
932 "txt": node.txt_recs,
933 }
934 )
935
936 def fqdn(self, name):
937 return "%s.%s." % (name, self.domain_extension)
938
939 def create_dhcp_fwd(self, dhcp_range):
940
941 for ip in netaddr.IPNetwork(dhcp_range).iter_hosts():
942 self.a_recs["dhcp%03d" % (ip.words[3])] = str(ip)
943
944 def name_is_duplicate(self, name, target, record_type):
945 """
946 Returns True if name already exists in the zone as an A or CNAME
947 record, False otherwise
948 """
949
950 if name in self.a_recs:
951 logger.warning(
952 "Duplicate DNS record for name %s - A record to '%s', %s record to '%s'",
953 name,
954 self.a_recs[name],
955 record_type,
956 target,
957 )
958 return True
959
960 if name in self.cname_recs:
961 logger.warning(
962 "Duplicate DNS record for name %s - CNAME record to '%s', %s record to '%s'",
963 name,
964 self.cname_recs[name],
965 record_type,
966 target,
967 )
968 return True
969
970 return False
971
972 def add_ao_records(self, prefix, ip, ao):
973
974 name = ao.dns_name(ip, prefix)
975 target_ip = str(netaddr.IPNetwork(ip).ip) # make bare IP, not CIDR format
976
977 # add A records
978 if not self.name_is_duplicate(name, target_ip, "A"):
979 self.a_recs[name] = target_ip
980
981 # add CNAME records that alias to this name
982 for cname in ao.dns_cnames(ip):
983 # check that it isn't a dupe
984 if not self.name_is_duplicate(cname, target_ip, "CNAME"):
985 self.cname_recs[cname] = self.fqdn(name)
986
987 # add NS records if this is a DNS server
988 if ao.has_service(ip, 53, "udp"):
989 self.ns_recs.append(self.fqdn(name))
990
991 # if a DNS name is set, add it as a CNAME
992 if ao.ips[ip]["dns_name"]: # and ip == aos.data.primary_ip.address:
993 self.add_fwd_cname(ao.ips[ip]["dns_name"], self.fqdn(name))
994
995 def add_reserved(self, ip, res):
996
997 target_ip = str(netaddr.IPNetwork(ip).ip) # make bare IP, not CIDR format
998
999 if not self.name_is_duplicate(res["name"], target_ip, "A"):
1000 self.a_recs[res["name"]] = target_ip
1001
1002 def merge_parent_prefix(self, pprefix, prefix):
1003
1004 # only if no NS records exist already
1005 if not self.ns_recs:
1006 # scan parent prefix for services
1007 for ip, ao in pprefix.aos.items():
1008
1009 # Create a DNS within this prefix pointing to out-of-prefix IP
1010 # where DNS server is
1011 name = ao.dns_name(ip, prefix)
1012 target_ip = str(
1013 netaddr.IPNetwork(ip).ip
1014 ) # make bare IP, not CIDR format
1015
1016 # add NS records if this is a DNS server
1017 if ao.has_service(ip, 53, "udp"):
1018 self.a_recs[name] = target_ip
1019 self.ns_recs.append(self.fqdn(name))
1020
1021
1022@yaml.yaml_object(ydump)
1023class NBDNSReverseZones:
1024 def __init__(self):
1025
1026 self.reverse_zones = {}
1027
1028 @classmethod
1029 def to_yaml(cls, representer, node):
1030 return representer.represent_dict(node.reverse_zones)
1031
1032 @classmethod
1033 def canonicalize_rfc1918_prefix(cls, prefix):
1034 """
1035 RFC1918 prefixes need to be expanded to their widest canonical range to
1036 group all reverse lookup domains together for reverse DNS with NSD/Unbound.
1037 """
1038
1039 pnet = netaddr.IPNetwork(str(prefix))
1040 (o1, o2, o3, o4) = pnet.network.words # Split ipv4 octets
1041 cidr_plen = pnet.prefixlen
1042
1043 if o1 == 10:
1044 o2 = o3 = o4 = 0
1045 cidr_plen = 8
1046 elif (o1 == 172 and o2 >= 16 and o2 <= 31) or (o1 == 192 and o2 == 168):
1047 o3 = o4 = 0
1048 cidr_plen = 16
1049
1050 return "%s/%d" % (".".join(map(str, [o1, o2, o3, o4])), cidr_plen)
1051
1052 def add_prefix(self, prefix):
1053
1054 canonical_prefix = self.canonicalize_rfc1918_prefix(prefix)
1055
1056 if canonical_prefix in self.reverse_zones:
1057 rzone = self.reverse_zones[canonical_prefix]
1058 else:
1059 rzone = {
1060 "ns": [],
1061 "ptr": {},
1062 }
1063
1064 if prefix.dhcp_range:
1065 # FIXME: doesn't check for duplicate entries
1066 rzone["ptr"].update(self.create_dhcp_rev(prefix))
1067
1068 for ip, ao in prefix.aos.items():
1069 target_ip = str(netaddr.IPNetwork(ip).ip) # make bare IP, not CIDR format
1070 ao_name = self.get_ao_name(ip, ao, prefix,)
1071 rzone["ptr"][target_ip] = ao_name
1072
1073 # add NS records if this is a DNS server
1074 if ao.has_service(ip, 53, "udp"):
1075 rzone["ns"].append(ao_name)
1076
1077 parent_prefix = prefix.parent()
1078
1079 if parent_prefix:
1080 self.merge_parent_prefix(rzone, parent_prefix)
1081
1082 self.reverse_zones[canonical_prefix] = rzone
1083
1084 def merge_parent_prefix(self, rzone, pprefix):
1085
1086 # parent items
1087 p_ns = []
1088
1089 # scan parent prefix for services
1090 for ip, ao in pprefix.aos.items():
1091
1092 ao_name = self.get_ao_name(ip, ao, pprefix,)
1093
1094 # add NS records if this is a DNS server
1095 if ao.has_service(ip, 53, "udp"):
1096 p_ns.append(ao_name)
1097
1098 # set DNS servers if none in rzone
1099 if not rzone["ns"]:
1100 rzone["ns"] = p_ns
1101
1102 def create_dhcp_rev(self, prefix):
1103
1104 dhcp_rzone = {}
1105
1106 for ip in netaddr.IPNetwork(prefix.dhcp_range).iter_hosts():
1107 dhcp_rzone[str(ip)] = "dhcp%03d.%s." % (
1108 ip.words[3],
1109 prefix.domain_extension,
1110 )
1111
1112 return dhcp_rzone
1113
1114 def get_ao_name(self, ip, ao, prefix):
1115 short_name = ao.dns_name(ip, prefix)
1116 return "%s.%s." % (short_name, prefix.domain_extension)
1117
1118
1119@yaml.yaml_object(ydump)
1120class NBDHCPSubnet:
1121 def __init__(self, prefix):
1122
1123 self.domain_extension = prefix.domain_extension
1124
1125 self.subnet = None
1126 self.range = None
1127 self.first_ip = None
1128 self.hosts = []
1129 self.routers = []
1130 self.dns_servers = []
1131 self.dns_search = []
1132 self.tftpd_server = None
1133 self.ntp_servers = []
1134 self.dhcpd_interface = None
1135
1136 self.add_prefix(prefix)
1137
1138 for ip, ao in prefix.aos.items():
1139 self.add_ao(str(ip), ao, prefix)
1140
1141 parent_prefix = prefix.parent()
1142
1143 if parent_prefix:
1144 self.merge_parent_prefix(parent_prefix)
1145
1146 def add_prefix(self, prefix):
1147
1148 self.subnet = str(prefix)
1149
1150 self.first_ip = str(netaddr.IPAddress(netaddr.IPNetwork(str(prefix)).first + 1))
1151
1152 self.dns_search = [prefix.domain_extension]
1153
1154 if prefix.dhcp_range:
1155 self.range = prefix.dhcp_range
1156
1157 for ip, res in prefix.reserved_ips.items():
1158 # routers are reserved IP's that start with 'router" in the IP description
1159 if re.match("router", res["description"]):
1160 router = {"ip": str(netaddr.IPNetwork(ip).ip)}
1161
1162 if (
1163 "rfc3442routes" in res["custom_fields"]
1164 and res["custom_fields"]["rfc3442routes"]
1165 ):
1166 # split on whitespace
1167 router["rfc3442routes"] = re.split(
1168 r"\s+", res["custom_fields"]["rfc3442routes"]
1169 )
1170
1171 self.routers.append(router)
1172
1173 # set first IP to router if not set otherwise.
1174 if not self.routers:
1175 router = {"ip": self.first_ip}
1176
1177 self.routers.append(router)
1178
1179 def add_ao(self, ip, ao, prefix):
1180
1181 target_ip = str(netaddr.IPNetwork(ip).ip) # make bare IP, not CIDR format
1182
1183 # find the DHCP interface if it's this IP
1184 if target_ip == self.first_ip:
1185 self.dhcpd_interface = ao.interfaces_by_ip[ip].name
1186
1187 name = ao.dns_name(ip, prefix)
1188
1189 # add only devices that have a macaddr for this IP
1190 if ip in ao.interfaces_by_ip:
1191
1192 mac_addr = dict(ao.interfaces_by_ip[ip]).get("mac_address")
1193
1194 if mac_addr and mac_addr.strip(): # if exists and not blank
1195 self.hosts.append(
Wei-Yu Chenb43fc322021-07-21 14:50:16 +08001196 {"name": name, "ip_addr": target_ip, "mac_addr": mac_addr.lower()}
Zack Williams2aeb3ef2021-06-11 17:10:36 -07001197 )
1198
1199 # add dns servers
1200 if ao.has_service(ip, 53, "udp"):
1201 self.dns_servers.append(target_ip)
1202
1203 # add tftp server
1204 if ao.has_service(ip, 69, "udp"):
1205 if not self.tftpd_server:
1206 self.tftpd_server = target_ip
1207 else:
1208 logger.warning(
1209 "Duplicate TFTP servers in prefix, using first of %s and %s",
1210 self.tftpd_server,
1211 target_ip,
1212 )
1213
1214 # add NTP servers
1215 if ao.has_service(ip, 123, "udp"):
1216 self.ntp_servers.append(target_ip)
1217
1218 def merge_parent_prefix(self, pprefix):
1219
1220 # parent items
1221 p_dns_servers = []
1222 p_tftpd_server = None
1223 p_ntp_servers = []
1224
1225 # scan parent prefix for services
1226 for ip, ao in pprefix.aos.items():
1227
1228 target_ip = str(netaddr.IPNetwork(ip).ip)
1229
1230 # add dns servers
1231 if ao.has_service(ip, 53, "udp"):
1232 p_dns_servers.append(target_ip)
1233
1234 # add tftp server
1235 if ao.has_service(ip, 69, "udp"):
1236 if not p_tftpd_server:
1237 p_tftpd_server = target_ip
1238 else:
1239 logger.warning(
1240 "Duplicate TFTP servers in parent prefix, using first of %s and %s",
1241 p_tftpd_server,
1242 target_ip,
1243 )
1244
1245 # add NTP servers
1246 if ao.has_service(ip, 123, "udp"):
1247 p_ntp_servers.append(target_ip)
1248
1249 # merge if doesn't exist in prefix
1250 if not self.dns_servers:
1251 self.dns_servers = p_dns_servers
1252
1253 if not self.tftpd_server:
1254 self.tftpd_server = p_tftpd_server
1255
1256 if not self.ntp_servers:
1257 self.ntp_servers = p_ntp_servers
1258
1259 @classmethod
1260 def to_yaml(cls, representer, node):
1261 return representer.represent_dict(
1262 {
1263 "subnet": node.subnet,
1264 "range": node.range,
1265 "routers": node.routers,
1266 "hosts": node.hosts,
1267 "dns_servers": node.dns_servers,
1268 "dns_search": node.dns_search,
1269 "tftpd_server": node.tftpd_server,
1270 "ntp_servers": node.ntp_servers,
1271 }
1272 )