blob: 56c6ed7f43801a959a1d66a6dbe0199491df0c04 [file] [log] [blame]
#!/usr/bin/env python3
# SPDX-FileCopyrightText: © 2021 Open Networking Foundation <support@opennetworking.org>
# SPDX-License-Identifier: Apache-2.0
# tenant_validator.py
# Grab the data from Tenant and check if the information is invalidate
from __future__ import absolute_import
import argparse
import logging
import re
import sys
import netaddr
import pynetbox
import requests
import yaml
logging.basicConfig()
logger = logging.getLogger("TenentValidator")
# The Global variable shared with different netbox api caller function
netboxapi = None
netbox_config = None
misconfs = list()
# The consistent variable should be same in every devices, prefixes of the deployment
site_name = None
deployment_name = None
# A Regex rule to identify if device name is a valid domain
fqdn_regex = re.compile(
r"^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*"
+ r"([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$"
)
class Configuration:
mapping_dict = {
logging.ERROR: logger.error,
logging.WARN: logger.warn,
logging.INFO: logger.info,
}
def __init__(self, obj, message, level=logging.ERROR):
self.level = level
self.obj = obj
self.message = message
# self.mapping_dict[level](self)
def __repr__(self):
return "[%s] object %s: %s\nRef. %s" % (
"/".join(self.obj.url.split("/")[-4:-2]),
self.obj,
self.message,
self.obj.url.replace("10.76.28.11", "netbox.infra.onlab.us").replace(
"api/", ""
),
)
def get_object_type(obj):
return ("/".join(obj.url.split("/")[-4:-2]),)
def validate_vlans(vlans=None):
global misconfs
for vlan in vlans:
if not vlan.group:
misconfs.append(Configuration(vlan, "VLAN group isn't set"))
if not vlan.site:
misconfs.append(Configuration(vlan, "VLAN site isn't set"))
if not vlan.tenant:
misconfs.append(Configuration(vlan, "VLAN tenant isn't set"))
def validate_prefixes(prefixes=None):
global misconfs
tenant_dict = dict()
for prefix in prefixes:
netmask = prefix.prefix.split("/")[-1]
if not re.search(fqdn_regex, prefix.description):
misconfs.append(Configuration(prefix, "Description (FQDN) is invalid"))
else:
device = netboxapi.dcim.devices.filter(tenant_id=prefix.tenant.id)[0]
if device:
sitename = device.name.split(".")[-1]
if sitename not in prefix.description:
misconfs.append(
Configuration(prefix, "Site name should in the prefix FQDN")
)
if not prefix.tenant:
misconfs.append(Configuration(prefix, "Tenant isn't set"))
else:
tenant_dict.setdefault(prefix.tenant, dict())
tenant_dict[prefix.tenant].setdefault(netmask, list())
tenant_dict[prefix.tenant][netmask].append(prefix)
# Assume prefixes in different tenants don't have the intersection
# So parent prefix only check if it has a DHCP sub-prefix in same tenant
for prefixes_by_tenant in tenant_dict.values():
for netmask, prefixes_by_netmask in prefixes_by_tenant.items():
for prefix in prefixes_by_netmask:
if int(netmask) < 26:
children = [
child
for key, value in prefixes_by_tenant.items()
if int(key) > int(netmask)
for child in value
]
children = [
child
for child in children
if str(
netaddr.IPNetwork(
child.prefix.split("/")[0] + "/" + netmask
).cidr
)
== prefix.prefix
]
if children:
continue
dhcp_addr = list(
filter(
lambda ip: ip.status.value == "dhcp",
netboxapi.ipam.ip_addresses.filter(parent=prefix.prefix),
)
)
if not dhcp_addr:
misconfs.append(
Configuration(prefix, "DHCP subnet not found in prefix")
)
continue
if len(dhcp_addr) > 1:
misconfs.append(
Configuration(
prefix, "Prefix should have exact 1 DHCP subnet"
)
)
continue
dhcp_range = dhcp_addr[0].address
ip_addrs = netboxapi.ipam.ip_addresses.filter(parent=dhcp_range)
ip_addrs = ip_addrs.remove(dhcp_addr[0])
if ip_addrs:
misconfs.append(
Configuration(
prefix,
"DHCP range %s contains other IP addresses: %s"
% (dhcp_range, ip_addrs),
)
)
def validate_ip_addresses(ip_addresses=None):
global misconfs
prefix_dict = dict()
for ip_address in ip_addresses:
if netaddr.IPNetwork(ip_address.address).network.is_private():
if not ip_address.vrf:
misconfs.append(
Configuration(ip_address, "VRF isn't set for this IP address")
)
else:
prefix = str(netaddr.IPNetwork(ip_address.address).cidr)
if prefix not in prefix_dict:
prefix_dict[prefix] = netboxapi.ipam.prefixes.get(prefix=prefix)
if prefix_dict[prefix] and prefix_dict[prefix].vrf != ip_address.vrf:
misconfs.append(
Configuration(ip_address, "VRF isn't match with Prefix")
)
def validate_interfaces(interfaces=None):
global misconfs
for interface in interfaces:
count_ips = 0
if get_object_type(interface) == "virtualization/interfaces":
count_ips = len(
netboxapi.ipam.ip_addresses.filter(vminterface_id=interface.id)
)
elif get_object_type(interface) == "dcim/interfaces":
count_ips = interface.count_ipaddresses
if count_ips > 0 and not interface.mac_address:
misconfs.append(
Configuration(
interface,
"VM Interface has IP address assigned but mac address isn't set",
)
)
if (
get_object_type(interface) == "dcim/interfaces"
and interface.type.value == "virtaul"
and (not interface.mgmt_only or not interface.tagged_vlans)
):
misconfs.append(
Configuration(
interface,
"Virtual interface should be management only or VLAN interface",
)
)
if interface.tagged_vlans:
if len(interface.tagged_vlans) > 1:
misconfs.append(
Configuration(interface, "Virtual Interface has multiple VLANs set")
)
elif str(interface.tagged_vlans[0].vid) not in interface.name:
misconfs.append(
Configuration(
interface, "Virtual Interface name not match to VLAN ID"
)
)
def validate_vrfs(vrfs=None):
global misconfs
for vrf in vrfs:
if not vrf.enforce_unique:
misconfs.append(
Configuration(vrf, "VRF doesn't have enforce_unique set as True")
)
if not vrf.tenant:
misconfs.append(Configuration(vrf, "VRF doesn't have tenant set"))
def validate_machines(machines=None):
global misconfs
tenant_info = dict()
for machine in machines:
if not re.search(fqdn_regex, machine.name):
misconfs.append(
Configuration(
machine, "Device/VM FQDN name %s is invalid" % machine.name
)
)
if not machine.tenant:
misconfs.append(Configuration(machine, "Device/VM doesn't have tenant set"))
segments = machine.name.split(".")
if len(segments) != 3:
misconfs.append(
Configuration(
machine,
"Device/VM FQDN should have 3 segments, found %d" % len(segments),
)
)
elif machine.tenant:
if machine.tenant not in tenant_info:
tenant_info.setdefault(
machine.tenant, {"deployment": segments[1], "site": segments[2]}
)
else:
deployment = tenant_info[machine.tenant]["deployment"]
site = tenant_info[machine.tenant]["site"]
if deployment != segments[1] or site != segments[2]:
misconfs.append(
Configuration(
machine,
"Deployment or Site name is not consistent with other Device/VM",
)
)
if (
(
machine.__class__.__name__ == "Devices"
and machine.device_role.name in ["Router", "Switch", "Server"]
)
or machine.__class__.__name__ == "VirtualMachines"
) and not machine.primary_ip:
misconfs.append(
Configuration(machine, "Primary IP must be set for this Device/VM")
)
def validate_tenants(tenants=None):
global misconfs
for tenant in tenants:
if tenant.device_count == 0:
misconfs.append(Configuration(tenant, "0 device was found for this tenant"))
if tenant.prefix_count == 0:
misconfs.append(Configuration(tenant, "0 prefix was found for this tenant"))
if tenant.vrf_count == 0:
misconfs.append(Configuration(tenant, "0 vrf was found for this tenant"))
devices = list(netboxapi.dcim.devices.filter(tenant_id=tenant.id))
mgmtserver = list(filter(lambda d: d.device_role.name == "Router", devices))
if len(mgmtserver) != 1:
misconfs.append(
Configuration(tenant, "Tenant must have exact 1 Router (mgmtserver)")
)
def get_objects(tenant_name=""):
return_dict = dict()
tenants = list(netboxapi.tenancy.tenants.filter(name=tenant_name))
if len(tenants) == 0:
logger.critical("Tenant name %s wasn't found in Netbox", tenant_name)
sys.exit(1)
tenant_id = None if len(tenants) != 1 else tenants[0].id
# If the tenant_id is None, then Netbox API will return all objects by default
devices = list(netboxapi.dcim.devices.filter(tenant_id=tenant_id))
virtual_machines = list(
netboxapi.virtualization.virtual_machines.filter(tenant_id=tenant_id)
)
physical_interfaces = list()
for device in devices:
physical_interfaces.extend(
list(netboxapi.dcim.interfaces.filter(device_id=device.id))
)
virtual_interfaces = list()
for virtual_machine in virtual_machines:
virtual_interfaces.extend(
list(
netboxapi.virtualization.interfaces.filter(
virtual_machine_id=virtual_machine.id
)
)
)
vrfs = list(netboxapi.ipam.vrfs.filter(tenant_id=tenant_id))
vlans = list(netboxapi.ipam.vlans.filter(tenant_id=tenant_id))
ip_addresses = list(netboxapi.ipam.ip_addresses.filter(tenant_id=tenant_id))
prefixes = list()
for vrf in vrfs:
prefixes.extend(list(netboxapi.ipam.prefixes.filter(vrf_id=vrf.id)))
return_dict = {
"tenants": tenants,
"devices": devices,
"physical_interfaces": physical_interfaces,
"virtual_machines": virtual_machines,
"virtual_interfaces": virtual_interfaces,
"vrfs": vrfs,
"prefixes": prefixes,
"vlans": vlans,
"ip_addresses": ip_addresses,
}
return return_dict
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Netbox Tenant Validator")
parser.add_argument(
"settings",
type=argparse.FileType("r"),
help="YAML Ansible inventory file w/NetBox API token",
)
args = parser.parse_args()
netbox_config = yaml.safe_load(args.settings.read())
netboxapi = pynetbox.api(
netbox_config["api_endpoint"], token=netbox_config["token"], threading=True
)
if not netbox_config.get("validate_certs", True):
session = requests.Session()
session.verify = False
netboxapi.http_session = session
mapping_func = {
"tenants": validate_tenants,
"devices": validate_machines,
"physical_interfaces": validate_interfaces,
"virtual_machines": validate_machines,
"virtual_interfaces": validate_interfaces,
"vrfs": validate_vrfs,
"vlans": validate_vlans,
"prefixes": validate_prefixes,
"ip_addresses": validate_ip_addresses,
}
netbox_data = get_objects(netbox_config.get("tenant_name", ""))
for key, validate_func in mapping_func.items():
if netbox_data[key]:
validate_func(netbox_data[key])
if not misconfs:
print("All checks passed.")
else:
for misconf in misconfs:
print(misconf)