WIP: adds ip address support for check_redfish source

refs: #91
This commit is contained in:
ricardo.bartels@telekom.de
2021-09-17 16:18:06 +02:00
parent ffff4904c7
commit 716ee29446
4 changed files with 297 additions and 222 deletions

View File

@@ -7,14 +7,14 @@
# For a copy, see file LICENSE.txt included in this
# repository or visit: <https://opensource.org/licenses/MIT>.
from ipaddress import ip_interface
from ipaddress import ip_interface, ip_address, IPv6Address, IPv4Address, IPv6Network, IPv4Network
import asyncio
import aiodns
from module.common.logging import get_logger
from module.common.misc import grab
from module.netbox.inventory import NBDevice, NBVM, NBInterface, NBVMInterface
from module.netbox.inventory import NBDevice, NBVM, NBInterface, NBVMInterface, NBSite, NBPrefix, NBIPAddress, NBVLAN
log = get_logger()
@@ -78,16 +78,15 @@ def ip_valid_to_add_to_netbox(ip, permitted_subnets, interface_name=None):
if permitted_subnets is None:
return False
if "/" not in ip:
log.error(f"IP {ip} must contain subnet or prefix length")
return False
ip_text = f"'{ip}'"
if interface_name is not None:
ip_text = f"{ip_text} for {interface_name}"
try:
ip_a = ip_interface(ip).ip
if "/" in ip:
ip_a = ip_interface(ip).ip
else:
ip_a = ip_address(ip)
except ValueError:
log.error(f"IP address {ip_text} invalid!")
return False
@@ -317,4 +316,234 @@ def map_object_interfaces_to_current_interfaces(inventory, device_vm_object, int
return return_data
def return_longest_matching_prefix_for_ip(inventory=None, ip_to_match=None, site_name=None):
"""
This is a lazy approach to find longest matching prefix to an IP address.
If site_name is set only IP prefixes from that site are matched.
Parameters
----------
inventory: NetBoxInventory
inventory handler
ip_to_match: (IPv4Address, IPv6Address)
IP address to find prefix for
site_name: str
name of the site the prefix needs to be in
Returns
-------
(NBPrefix, None): longest matching IP prefix, or None if no matching prefix was found
"""
if ip_to_match is None or inventory is None:
return
if not isinstance(ip_to_match, (IPv4Address, IPv6Address)):
raise ValueError("Value of 'ip_to_match' needs to be an IPv4Address or IPv6Address this_object.")
site_object = None
if site_name is not None:
site_object = inventory.get_by_data(NBSite, data={"name": site_name})
if site_object is None:
log.error(f"Unable to find site '{site_name}' for IP {ip_to_match}. "
"Skipping to find Prefix for this IP.")
current_longest_matching_prefix_length = 0
current_longest_matching_prefix = None
for prefix in inventory.get_all_items(NBPrefix):
if grab(prefix, "data.site") != site_object:
continue
prefix_network = grab(prefix, f"data.{NBPrefix.primary_key}")
if prefix_network is None:
continue
if ip_to_match in prefix_network and \
prefix_network.prefixlen >= current_longest_matching_prefix_length:
current_longest_matching_prefix_length = prefix_network.prefixlen
current_longest_matching_prefix = prefix
return current_longest_matching_prefix
def add_ip_address(source_handler, nic_ip, nic_object, site):
# get IP and prefix length
try:
if "/" in nic_ip:
ip_object = ip_interface(nic_ip)
else:
ip_object = ip_address(nic_ip)
except ValueError:
log.error(f"IP '{nic_ip}' ({nic_object.get_display_name()}) does not appear "
"to be a valid IP address. Skipping!")
return
log.debug2(f"Trying to find prefix for IP: {ip_object}")
possible_ip_vrf = None
possible_ip_tenant = None
# test for site prefixes first
matching_site_name = site
matching_ip_prefix = return_longest_matching_prefix_for_ip(source_handler.inventory,
ip_object,
matching_site_name)
# nothing was found then check prefixes with site name
if matching_ip_prefix is None:
matching_site_name = None
matching_ip_prefix = return_longest_matching_prefix_for_ip(source_handler.inventory, ip_object)
# matching prefix found, get data from prefix
if matching_ip_prefix is not None:
this_prefix = grab(matching_ip_prefix, f"data.{NBPrefix.primary_key}")
if matching_site_name is None:
log.debug2(f"Found IP '{ip_object}' matches global prefix '{this_prefix}'")
else:
log.debug2(f"Found IP '{ip_object}' matches site '{matching_site_name}' prefix "
f"'{this_prefix}'")
# check if prefix net size and ip address prefix length match
if not isinstance(ip_object, (IPv6Address, IPv4Address)) and \
this_prefix.prefixlen != ip_object.network.prefixlen:
log.warning(f"IP prefix length of '{ip_object}' ({nic_object.get_display_name()}) "
f"does not match network prefix length '{this_prefix}'!")
# get prefix data
possible_ip_vrf = grab(matching_ip_prefix, "data.vrf")
prefix_tenant = grab(matching_ip_prefix, "data.tenant")
prefix_vlan = grab(matching_ip_prefix, "data.vlan")
# get NIC VLAN data
nic_vlan = grab(nic_object, "data.untagged_vlan")
nic_vlan_tenant = None
if nic_vlan is not None:
nic_vlan_tenant = grab(nic_vlan, "data.tenant")
# check if interface VLAN matches prefix VLAN for IP address
if isinstance(nic_vlan, NBVLAN) and isinstance(prefix_vlan, NBPrefix) and nic_vlan != prefix_vlan:
log.warning(f"Prefix vlan '{prefix_vlan.get_display_name()}' does not match interface vlan "
f"'{nic_vlan.get_display_name()}' for '{nic_object.get_display_name()}")
if prefix_tenant is not None:
possible_ip_tenant = prefix_tenant
elif nic_vlan_tenant is not None:
possible_ip_tenant = nic_vlan_tenant
else:
log_text = f"No matching NetBox prefix for '{ip_object}' found"
if isinstance(ip_object, (IPv6Address, IPv4Address)):
log.warning(f"{log_text}. Unable to add IP address to NetBox.")
return None
else:
log.debug2(log_text)
if matching_ip_prefix is not None and isinstance(ip_object, (IPv6Address, IPv4Address)):
this_prefix = grab(matching_ip_prefix, "data.prefix")
if isinstance(this_prefix, (IPv4Network, IPv6Network)):
ip_object = ip_interface(f"{ip_object}/{this_prefix.prefixlen}")
else:
log.warning(f"{this_prefix.name} got wrong format. Unable to add IP to NetBox")
return None
# try to find matching IP address object
this_ip_object = None
skip_this_ip = False
for ip in source_handler.inventory.get_all_items(NBIPAddress):
# check if address matches (without prefix length)
ip_address_string = grab(ip, "data.address", fallback="")
# not a matching address
if not ip_address_string.startswith(f"{ip_object.ip.compressed}/"):
continue
current_nic = grab(ip, "data.assigned_object_id")
# is it our current ip interface?
if current_nic == nic_object:
this_ip_object = ip
break
# check if IP has the same prefix
# continue if
# * both are in global scope
# * both ara part of the same vrf
if possible_ip_vrf != grab(ip, "data.vrf"):
continue
# IP address is not assigned to any interface
if not isinstance(current_nic, (NBInterface, NBVMInterface)):
this_ip_object = ip
break
# get current IP interface status
current_nic_enabled = grab(current_nic, "data.enabled", fallback=True)
this_nic_enabled = grab(nic_object, "data.enabled", fallback=True)
if current_nic_enabled is True and this_nic_enabled is False:
log.debug(f"Current interface '{current_nic.get_display_name()}' for IP '{ip_object}'"
f" is enabled and this one '{nic_object.get_display_name()}' is disabled. "
f"IP assignment skipped!")
skip_this_ip = True
break
if current_nic_enabled is False and this_nic_enabled is True:
log.debug(f"Current interface '{current_nic.get_display_name()}' for IP '{ip_object}'"
f" is disabled and this one '{nic_object.get_display_name()}' is enabled. "
f"IP will be assigned to this interface.")
this_ip_object = ip
if current_nic_enabled == this_nic_enabled:
state = "enabled" if this_nic_enabled is True else "disabled"
log.warning(f"Current interface '{current_nic.get_display_name()}' for IP "
f"'{ip_object}' and this one '{nic_object.get_display_name()}' are "
f"both {state}. "
f"IP assignment skipped because it is unclear which one is the correct one!")
skip_this_ip = True
break
if skip_this_ip is True:
return
nic_ip_data = {
"address": ip_object.compressed,
"assigned_object_id": nic_object,
}
if not isinstance(this_ip_object, NBIPAddress):
log.debug(f"No existing {NBIPAddress.name} object found. Creating a new one.")
if possible_ip_vrf is not None:
nic_ip_data["vrf"] = possible_ip_vrf
if possible_ip_tenant is not None:
nic_ip_data["tenant"] = possible_ip_tenant
this_ip_object = source_handler.inventory.add_object(NBIPAddress, data=nic_ip_data, source=source_handler)
# update IP address with additional data if not already present
else:
log.debug2(f"Found existing NetBox {NBIPAddress.name} object: {this_ip_object.get_display_name()}")
if grab(this_ip_object, "data.vrf") is None and possible_ip_vrf is not None:
nic_ip_data["vrf"] = possible_ip_vrf
if grab(this_ip_object, "data.tenant") is None and possible_ip_tenant is not None:
nic_ip_data["tenant"] = possible_ip_tenant
this_ip_object.update(data=nic_ip_data, source=source_handler)
return this_ip_object
# EOF

View File

@@ -658,7 +658,7 @@ class NetBoxObject:
self.updated_items.append("tags")
log.info(f"{self.name.capitalize()} '{self.get_display_name()}' attribute 'tags' changed from "
f"'{current_tags.get_display_name()}' to '{new_tags.get_display_name()}'")
f"'{current_tags.get_display_name()}' to '{new_tags.get_display_name()}'")
def add_tags(self, tags_to_add):
"""

View File

@@ -7,26 +7,46 @@
# For a copy, see file LICENSE.txt included in this
# repository or visit: <https://opensource.org/licenses/MIT>.
# from ipaddress import ip_address, ip_network, ip_interface, IPv4Address, IPv6Address
from ipaddress import ip_network
import os
import glob
import json
from packaging import version
from module.common.logging import get_logger, DEBUG3
from module.common.misc import grab, dump, get_string_or_none, plural
from module.common.support import normalize_mac_address, ip_valid_to_add_to_netbox, map_object_interfaces_to_current_interfaces
from module.netbox.object_classes import *
from module.common.logging import get_logger
from module.common.misc import grab, get_string_or_none
from module.common.support import (
normalize_mac_address,
ip_valid_to_add_to_netbox,
map_object_interfaces_to_current_interfaces,
add_ip_address
)
from module.netbox.object_classes import (
NBTag,
NBManufacturer,
NBDeviceType,
NBPlatform,
NBClusterType,
NBClusterGroup,
NBDeviceRole,
NBSite,
NBCluster,
NBDevice,
NBInterface,
NBIPAddress,
NBPrefix,
NBTenant,
NBVRF,
NBVLAN,
NBPowerPort,
NBInventoryItem,
NBCustomField
)
from module.netbox.inventory import interface_speed_type_mapping
log = get_logger()
#
# ToDo:
# * add interface IPs
# * implement checking for permitted IPs
class CheckRedfish:
@@ -58,7 +78,9 @@ class CheckRedfish:
"enabled": True,
"inventory_file_path": None,
"permitted_subnets": None,
"collect_hardware_asset_tag": True
"overwrite_host_name": False,
"overwrite_power_supply_name": False,
"overwrite_interface_name": False
}
init_successful = False
@@ -198,7 +220,7 @@ class CheckRedfish:
if serial is not None:
device_data["serial"] = serial
if name is not None:
if name is not None and self.overwrite_host_name is True:
device_data["name"] = name
device_object.update(data=device_data, source=self)
@@ -282,10 +304,11 @@ class CheckRedfish:
ps_data = {
"device": device_object,
"description": ", ".join(description),
"mark_connected": connected,
"name": name
"mark_connected": connected
}
if name is not None and self.overwrite_power_supply_name is True:
ps_data["name"] = name
if capacity_in_watt is not None:
ps_data["maximum_draw"] = capacity_in_watt
if firmware is not None:
@@ -603,8 +626,9 @@ class CheckRedfish:
def update_network_interface(self, device_object, inventory_data):
port_data_dict = dict()
nic_ips = dict()
discovered_mac_list = list()
for nic_port in grab(inventory_data, "inventory.network_port", fallback=list()):
if grab(nic_port, "operation_status") in ["Disabled"]:
@@ -613,8 +637,6 @@ class CheckRedfish:
port_name = get_string_or_none(grab(nic_port, "name"))
port_id = get_string_or_none(grab(nic_port, "id"))
mac_address = get_string_or_none(grab(nic_port, "addresses.0")) # get 1st mac address
# ipv4_addresses = grab(nic_port, "ipv4_addresses")
# ipv6_addresses = grab(nic_port, "ipv6_addresses")
link_status = get_string_or_none(grab(nic_port, "link_status"))
manager_ids = grab(nic_port, "manager_ids", fallback=list())
hostname = get_string_or_none(grab(nic_port, "hostname"))
@@ -668,6 +690,11 @@ class CheckRedfish:
"health": health_status
}
# collect ip addresses
nic_ips[port_name] = list()
nic_ips[port_name].extend(grab(nic_port, "ipv4_addresses", fallback=list()))
nic_ips[port_name].extend(grab(nic_port, "ipv6_addresses", fallback=list()))
data = map_object_interfaces_to_current_interfaces(self.inventory, device_object, port_data_dict)
for port_name, port_data in port_data_dict.items():
@@ -681,10 +708,12 @@ class CheckRedfish:
del(port_data["health"])
if port_data.get("mac_address") is None:
del (port_data["mac_address"])
if self.overwrite_interface_name is False:
del (port_data["name"])
# create or update interface with data
if nic_object is None:
self.inventory.add_object(NBInterface, data=port_data, source=self)
nic_object = self.inventory.add_object(NBInterface, data=port_data, source=self)
else:
tags = nic_object.get_tags()
if self.source_tag in tags:
@@ -702,6 +731,14 @@ class CheckRedfish:
nic_object.update(data=data_to_update, source=self)
# check for interface ips
for nic_ip in nic_ips.get(port_name, list()):
if ip_valid_to_add_to_netbox(nic_ip, self.permitted_subnets, port_name) is False:
continue
add_ip_address(self, nic_ip, nic_object, grab(device_object, "data.site.data.name"))
def update_all_items(self, items):
if not isinstance(items, list):

View File

@@ -21,7 +21,9 @@ from module.common.misc import grab, dump, get_string_or_none, plural
from module.common.support import (
normalize_mac_address,
ip_valid_to_add_to_netbox,
map_object_interfaces_to_current_interfaces
map_object_interfaces_to_current_interfaces,
return_longest_matching_prefix_for_ip,
add_ip_address
)
from module.netbox.object_classes import *
from module.netbox.inventory import interface_speed_type_mapping
@@ -641,57 +643,6 @@ class VMWareHandler:
f"based on the primary IPv6 '{primary_ip6}'")
return device
def return_longest_matching_prefix_for_ip(self, ip_to_match=None, site_name=None):
"""
This is a lazy approach to find longest matching prefix to an IP address.
If site_name is set only IP prefixes from that site are matched.
Parameters
----------
ip_to_match: (IPv4Address, IPv6Address)
IP address to find prefix for
site_name: str
name of the site the prefix needs to be in
Returns
-------
(NBPrefix, None): longest matching IP prefix, or None if no matching prefix was found
"""
if ip_to_match is None:
return
if not isinstance(ip_to_match, (IPv4Address, IPv6Address)):
raise ValueError("Value of 'ip_to_match' needs to be an IPv4Address or IPv6Address this_object.")
site_object = None
if site_name is not None:
site_object = self.inventory.get_by_data(NBSite, data={"name": site_name})
if site_object is None:
log.error(f"Unable to find site '{site_name}' for IP {ip_to_match}. "
"Skipping to find Prefix for this IP.")
current_longest_matching_prefix_length = 0
current_longest_matching_prefix = None
for prefix in self.inventory.get_all_items(NBPrefix):
if grab(prefix, "data.site") != site_object:
continue
prefix_network = grab(prefix, f"data.{NBPrefix.primary_key}")
if prefix_network is None:
continue
if ip_to_match in prefix_network and \
prefix_network.prefixlen >= current_longest_matching_prefix_length:
current_longest_matching_prefix_length = prefix_network.prefixlen
current_longest_matching_prefix = prefix
return current_longest_matching_prefix
def get_vlan_object_if_exists(self, vlan_data=None):
"""
This function will try to find a matching VLAN object based on 'vlan_data'
@@ -953,149 +904,7 @@ class VMWareHandler:
"to be a valid IP address. Skipping!")
continue
log.debug2(f"Trying to find prefix for IP: {ip_interface_object}")
possible_ip_vrf = None
possible_ip_tenant = None
# test for site prefixes first
matching_site_name = site_name
matching_ip_prefix = self.return_longest_matching_prefix_for_ip(ip_interface_object, matching_site_name)
# nothing was found then check prefixes with site name
if matching_ip_prefix is None:
matching_site_name = None
matching_ip_prefix = self.return_longest_matching_prefix_for_ip(ip_interface_object)
# matching prefix found, get data from prefix
if matching_ip_prefix is not None:
this_prefix = grab(matching_ip_prefix, f"data.{NBPrefix.primary_key}")
if matching_site_name is None:
log.debug2(f"Found IP '{ip_interface_object}' matches global prefix '{this_prefix}'")
else:
log.debug2(f"Found IP '{ip_interface_object}' matches site '{matching_site_name}' prefix "
f"'{this_prefix}'")
# check if prefix net size and ip address prefix length match
if this_prefix.prefixlen != ip_interface_object.network.prefixlen:
log.warning(f"IP prefix length of '{ip_interface_object}' ({nic_object.get_display_name()}) "
f"does not match network prefix length '{this_prefix}'!")
# get prefix data
possible_ip_vrf = grab(matching_ip_prefix, "data.vrf")
prefix_tenant = grab(matching_ip_prefix, "data.tenant")
prefix_vlan = grab(matching_ip_prefix, "data.vlan")
# get NIC VLAN data
nic_vlan = grab(nic_object, "data.untagged_vlan")
nic_vlan_tenant = None
if nic_vlan is not None:
nic_vlan_tenant = grab(nic_vlan, "data.tenant")
# check if interface VLAN matches prefix VLAN for IP address
if isinstance(nic_vlan, NBVLAN) and isinstance(prefix_vlan, NBPrefix) and nic_vlan != prefix_vlan:
log.warning(f"Prefix vlan '{prefix_vlan.get_display_name()}' does not match interface vlan "
f"'{nic_vlan.get_display_name()}' for '{nic_object.get_display_name()}")
if prefix_tenant is not None:
possible_ip_tenant = prefix_tenant
elif nic_vlan_tenant is not None:
possible_ip_tenant = nic_vlan_tenant
else:
log.debug2(f"No matching prefix found for '{ip_interface_object}'")
# try to find matching IP address object
ip_object = None
skip_this_ip = False
for ip in self.inventory.get_all_items(NBIPAddress):
# check if address matches (without prefix length)
ip_address_string = grab(ip, "data.address", fallback="")
# not a matching address
if not ip_address_string.startswith(f"{ip_interface_object.ip.compressed}/"):
continue
current_nic = grab(ip, "data.assigned_object_id")
# is it our current ip interface?
if current_nic == nic_object:
ip_object = ip
break
# check if IP has the same prefix
# continue if
# * both are in global scope
# * both ara part of the same vrf
if possible_ip_vrf != grab(ip, "data.vrf"):
continue
# IP address is not assigned to any interface
if not isinstance(current_nic,(NBInterface, NBVMInterface)):
ip_object = ip
break
# get current IP interface status
current_nic_enabled = grab(current_nic, "data.enabled", fallback=True)
this_nic_enabled = grab(nic_object, "data.enabled", fallback=True)
if current_nic_enabled is True and this_nic_enabled is False:
log.debug(f"Current interface '{current_nic.get_display_name()}' for IP '{ip_interface_object}'"
f" is enabled and this one '{nic_object.get_display_name()}' is disabled. "
f"IP assignment skipped!")
skip_this_ip = True
break
if current_nic_enabled is False and this_nic_enabled is True:
log.debug(f"Current interface '{current_nic.get_display_name()}' for IP '{ip_interface_object}'"
f" is disabled and this one '{nic_object.get_display_name()}' is enabled. "
f"IP will be assigned to this interface.")
ip_object = ip
if current_nic_enabled == this_nic_enabled:
state = "enabled" if this_nic_enabled is True else "disabled"
log.warning(f"Current interface '{current_nic.get_display_name()}' for IP "
f"'{ip_interface_object}' and this one '{nic_object.get_display_name()}' are "
f"both {state}. "
f"IP assignment skipped because it is unclear which one is the correct one!")
skip_this_ip = True
break
if skip_this_ip is True:
continue
nic_ip_data = {
"address": ip_interface_object.compressed,
"assigned_object_id": nic_object,
}
if not isinstance(ip_object, NBIPAddress):
log.debug(f"No existing {NBIPAddress.name} object found. Creating a new one.")
if possible_ip_vrf is not None:
nic_ip_data["vrf"] = possible_ip_vrf
if possible_ip_tenant is not None:
nic_ip_data["tenant"] = possible_ip_tenant
ip_object = self.inventory.add_object(NBIPAddress, data=nic_ip_data, source=self)
# update IP address with additional data if not already present
else:
log.debug2(f"Found existing NetBox {NBIPAddress.name} object: {ip_object.get_display_name()}")
if grab(ip_object, "data.vrf") is None and possible_ip_vrf is not None:
nic_ip_data["vrf"] = possible_ip_vrf
if grab(ip_object, "data.tenant") is None and possible_ip_tenant is not None:
nic_ip_data["tenant"] = possible_ip_tenant
ip_object.update(data=nic_ip_data, source=self)
ip_object = add_ip_address(self, nic_ip, nic_object, site_name)
# continue if address is not a primary IP
if nic_ip not in [p_ipv4, p_ipv6]: