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