blob: baa4a79b31931df73a2054c61c1ff8b8bcd46dc1 [file] [log] [blame]
Wei-Yu Chene0877d02021-07-12 23:12:50 +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# tenant_validator.py
7# Grab the data from Tenant and check if the information is invalidate
8
9import re
10import yaml
11import logging
12import argparse
13import nbhelper
14import pynetbox
15import requests
16import netaddr
17
18
19logging.basicConfig()
20logger = logging.getLogger("TenentValidator")
21
22# The Global variable shared with different netbox api caller function
23netboxapi = None
24netbox_config = None
25misconfs = list()
26
27# The consistent variable should be same in every devices, prefixes of the deployment
28site_name = None
29deployment_name = None
30
31# A Regex rule to identify if device name is a valid domain
32fqdn_regex = re.compile(
33 "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
34)
35
36
37class Configuration(object):
38
39 mapping_dict = {
40 logging.ERROR: logger.error,
41 logging.WARN: logger.warn,
42 logging.INFO: logger.info,
43 }
44
45 def __init__(self, obj, message, level=logging.ERROR):
46 self.level = level
47 self.obj = obj
48 self.message = message
49
50 # self.mapping_dict[level](self)
51
52 def __repr__(self):
53 return "[%s] object %s: %s\nRef. %s" % (
54 "/".join(self.obj.url.split("/")[-4:-2]),
55 self.obj,
56 self.message,
57 self.obj.url.replace("10.76.28.11", "netbox.infra.onlab.us").replace(
58 "api/", ""
59 ),
60 )
61
62
63def get_object_type(obj):
64 return ("/".join(obj.url.split("/")[-4:-2]),)
65
66
67def validate_vlans(vlans=list()):
68 global misconfs
69
70 for vlan in vlans:
71 if not vlan.group:
72 misconfs.append(Configuration(vlan, "VLAN group isn't set"))
73 if not vlan.site:
74 misconfs.append(Configuration(vlan, "VLAN site isn't set"))
75 if not vlan.tenant:
76 misconfs.append(Configuration(vlan, "VLAN tenant isn't set"))
77
78
79def validate_prefixes(prefixes=list()):
80 global misconfs
81
82 tenant_dict = dict()
83
84 for prefix in prefixes:
85 netmask = prefix.prefix.split("/")[-1]
86
87 if not re.search(fqdn_regex, prefix.description):
88 misconfs.append(Configuration(prefix, "Description (FQDN) is invalid"))
89 else:
90 device = netboxapi.dcim.devices.filter(tenant_id=prefix.tenant.id)[0]
91 if device:
92 sitename = device.name.split(".")[-1]
93 if sitename not in prefix.description:
94 misconfs.append(
95 Configuration(prefix, "Site name should in the prefix FQDN")
96 )
97
98 if not prefix.tenant:
99 misconfs.append(Configuration(prefix, "Tenant isn't set"))
100 else:
101 tenant_dict.setdefault(prefix.tenant, dict())
102 tenant_dict[prefix.tenant].setdefault(netmask, list())
103 tenant_dict[prefix.tenant][netmask].append(prefix)
104
105 # Assume prefixes in different tenants don't have the intersection
106 # So parent prefix only check if it has a DHCP sub-prefix in same tenant
107 for prefixes_by_tenant in tenant_dict.values():
108 for netmask, prefixes_by_netmask in prefixes_by_tenant.items():
109 for prefix in prefixes_by_netmask:
110 if int(netmask) < 26:
111 children = [
112 child
113 for key, value in prefixes_by_tenant.items()
114 if int(key) > int(netmask)
115 for child in value
116 ]
117
118 children = [
119 child
120 for child in children
121 if str(
122 netaddr.IPNetwork(
123 child.prefix.split("/")[0] + "/" + netmask
124 ).cidr
125 )
126 == prefix.prefix
127 ]
128 if children:
129 continue
130
131 dhcp_addr = list(
132 filter(
133 lambda ip: ip.status.value == "dhcp",
134 netboxapi.ipam.ip_addresses.filter(parent=prefix.prefix),
135 )
136 )
137
138 if not dhcp_addr:
139 misconfs.append(
140 Configuration(prefix, "DHCP subnet not found in prefix")
141 )
142 continue
143
144 if len(dhcp_addr) > 1:
145 misconfs.append(
146 Configuration(
147 prefix, "Prefix should have exact 1 DHCP subnet"
148 )
149 )
150 continue
151
152 dhcp_range = dhcp_addr[0].address
153 ip_addrs = netboxapi.ipam.ip_addresses.filter(parent=dhcp_range)
154 ip_addrs = list(filter(lambda ip: ip != dhcp_addr[0], ip_addrs))
155 if ip_addrs:
156 misconfs.append(
157 Configuration(
158 prefix,
159 "DHCP range %s contains other IP addresses: %s"
160 % (dhcp_range, ip_addrs),
161 )
162 )
163
164
165def validate_ip_addresses(ip_addresses=list()):
166 global misconfs
167 prefix_dict = dict()
168
169 for ip_address in ip_addresses:
170
171 if netaddr.IPNetwork(ip_address.address).network.is_private():
172 if not ip_address.vrf:
173 misconfs.append(
174 Configuration(ip_address, "VRF isn't set for this IP address")
175 )
176 else:
177 prefix = str(netaddr.IPNetwork(ip_address.address).cidr)
178 if prefix not in prefix_dict:
179 prefix_dict[prefix] = netboxapi.ipam.prefixes.get(prefix=prefix)
180
181 if prefix_dict[prefix] and prefix_dict[prefix].vrf != ip_address.vrf:
182 misconfs.append(
183 Configuration(ip_address, "VRF isn't match with Prefix")
184 )
185
186
187def validate_interfaces(interfaces=list()):
188 global misconfs
189
190 for interface in interfaces:
191 count_ips = 0
192
193 if get_object_type(interface) == "virtualization/interfaces":
194 count_ips = len(
195 netboxapi.ipam.ip_addresses.filter(vminterface_id=interface.id)
196 )
197 elif get_object_type(interface) == "dcim/interfaces":
198 count_ips = interface.count_ipaddresses
199
200 if count_ips > 0 and not interface.mac_address:
201 misconfs.append(
202 Configuration(
203 interface,
204 "VM Interface has IP address assigned but mac address isn't set",
205 )
206 )
207
208 if (
209 get_object_type(interface) == "dcim/interfaces"
210 and interface.type.value == "virtaul"
211 and (not interface.mgmt_only or not interface.tagged_vlans)
212 ):
213 misconfs.append(
214 Configuration(
215 interface,
216 "Virtual interface should be management only or VLAN interface",
217 )
218 )
219
220 if interface.tagged_vlans:
221 if len(interface.tagged_vlans) > 1:
222 misconfs.append(
223 Configuration(interface, "Virtual Interface has multiple VLANs set")
224 )
225 elif str(interface.tagged_vlans[0].vid) not in interface.name:
226 misconfs.append(
227 Configuration(
228 interface, "Virtual Interface name not match to VLAN ID"
229 )
230 )
231
232
233def validate_vrfs(vrfs=list()):
234 global misconfs
235
236 for vrf in vrfs:
237 if not vrf.enforce_unique:
238 misconfs.append(
239 Configuration(vrf, "VRF doesn't have enforce_unique set as True")
240 )
241
242 if not vrf.tenant:
243 misconfs.append(Configuration(vrf, "VRF doesn't have tenant set"))
244
245
246def validate_machines(machines=list()):
247 global misconfs
248
249 tenant_info = dict()
250
251 for machine in machines:
252 if not re.search(fqdn_regex, machine.name):
253 misconfs.append(
254 Configuration(
255 machine, "Device/VM FQDN name %s is invalid" % machine.name
256 )
257 )
258
259 if not machine.tenant:
260 misconfs.append(Configuration(machine, "Device/VM doesn't have tenant set"))
261
262 segments = machine.name.split(".")
263 if len(segments) != 3:
264 misconfs.append(
265 Configuration(
266 machine,
267 "Device/VM FQDN should have 3 segments, found %d" % len(segments),
268 )
269 )
270 elif machine.tenant:
271 if machine.tenant not in tenant_info:
272 tenant_info.setdefault(
273 machine.tenant, {"deployment": segments[1], "site": segments[2]}
274 )
275 else:
276 deployment = tenant_info[machine.tenant]["deployment"]
277 site = tenant_info[machine.tenant]["site"]
278 if deployment != segments[1] or site != segments[2]:
279 misconfs.append(
280 Configuration(
281 machine,
282 "Deployment or Site name is not consistent with other Device/VM",
283 )
284 )
285
286 if (
287 (
288 machine.__class__.__name__ == "Devices"
289 and machine.device_role.name in ["Router", "Switch", "Server"]
290 )
291 or machine.__class__.__name__ == "VirtualMachines"
292 ) and not machine.primary_ip:
293 misconfs.append(
294 Configuration(machine, "Primary IP must be set for this Device/VM")
295 )
296
297
298def validate_tenants(tenants=list()):
299 global misconfs
300
301 for tenant in tenants:
302 if tenant.device_count == 0:
303 misconfs.append(Configuration(tenant, "0 device was found for this tenant"))
304 if tenant.prefix_count == 0:
305 misconfs.append(Configuration(tenant, "0 prefix was found for this tenant"))
306 if tenant.vrf_count == 0:
307 misconfs.append(Configuration(tenant, "0 vrf was found for this tenant"))
308
309 devices = list(netboxapi.dcim.devices.filter(tenant_id=tenant.id))
310 mgmtserver = list(filter(lambda d: d.device_role.name == "Router", devices))
311 if len(mgmtserver) != 1:
312 misconfs.append(
313 Configuration(tenant, "Tenant must have exact 1 Router (mgmtserver)")
314 )
315
316
317def get_objects(tenant_name=""):
318 return_dict = dict()
319
320 tenants = list(netboxapi.tenancy.tenants.filter(name=tenant_name))
321 if len(tenants) == 0:
322 logger.critical("Tenant name %s wasn't found in Netbox" % tenant_name)
323 sys.exit(1)
324 tenant_id = None if len(tenants) != 1 else tenants[0].id
325
326 # If the tenant_id is None, then Netbox API will return all objects by default
327 devices = list(netboxapi.dcim.devices.filter(tenant_id=tenant_id))
328 virtual_machines = list(
329 netboxapi.virtualization.virtual_machines.filter(tenant_id=tenant_id)
330 )
331
332 physical_interfaces = list()
333 for device in devices:
334 physical_interfaces.extend(
335 list(netboxapi.dcim.interfaces.filter(device_id=device.id))
336 )
337
338 virtual_interfaces = list()
339 for virtual_machine in virtual_machines:
340 virtual_interfaces.extend(
341 list(
342 netboxapi.virtualization.interfaces.filter(
343 virtual_machine_id=virtual_machine.id
344 )
345 )
346 )
347
348 vrfs = list(netboxapi.ipam.vrfs.filter(tenant_id=tenant_id))
349 vlans = list(netboxapi.ipam.vlans.filter(tenant_id=tenant_id))
350 ip_addresses = list(netboxapi.ipam.ip_addresses.filter(tenant_id=tenant_id))
351
352 prefixes = list()
353 for vrf in vrfs:
354 prefixes.extend(list(netboxapi.ipam.prefixes.filter(vrf_id=vrf.id)))
355
356 return_dict = {
357 "tenants": tenants,
358 "devices": devices,
359 "physical_interfaces": physical_interfaces,
360 "virtual_machines": virtual_machines,
361 "virtual_interfaces": virtual_interfaces,
362 "vrfs": vrfs,
363 "prefixes": prefixes,
364 "vlans": vlans,
365 "ip_addresses": ip_addresses,
366 }
367
368 return return_dict
369
370
371if __name__ == "__main__":
372
373 parser = argparse.ArgumentParser(description="Netbox Tenant Validator")
374 parser.add_argument(
375 "settings",
376 type=argparse.FileType("r"),
377 help="YAML Ansible inventory file w/NetBox API token",
378 )
379
380 args = parser.parse_args()
381 netbox_config = yaml.safe_load(args.settings.read())
382 netboxapi = pynetbox.api(
383 netbox_config["api_endpoint"], token=netbox_config["token"], threading=True
384 )
385
386 if not netbox_config.get("validate_certs", False):
387 session = requests.Session()
388 session.verify = False
389 netboxapi.http_session = session
390
391 mapping_func = {
392 "tenants": validate_tenants,
393 "devices": validate_machines,
394 "physical_interfaces": validate_interfaces,
395 "virtual_machines": validate_machines,
396 "virtual_interfaces": validate_interfaces,
397 "vrfs": validate_vrfs,
398 "vlans": validate_vlans,
399 "prefixes": validate_prefixes,
400 "ip_addresses": validate_ip_addresses,
401 }
402
403 netbox_data = get_objects(netbox_config.get("tenant_name", ""))
404
405 for key, validate_func in mapping_func.items():
406 if netbox_data[key]:
407 validate_func(netbox_data[key])
408
409 if not misconfs:
410 print("All checks passed.")
411 else:
412 for misconf in misconfs:
413 print(misconf)