diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json index 4ee622611c12c48be7874eeab7c8ffadac395889..7ff96dad784fcd70156331ab2d9ce910dac99078 100644 --- a/gso/oss-params-example.json +++ b/gso/oss-params-example.json @@ -1,6 +1,7 @@ { "GENERAL": { - "public_hostname": "https://gap.geant.org" + "public_hostname": "https://gap.geant.org", + "environment": "lab" }, "NETBOX": { "api": "https://127.0.0.1:8000", @@ -45,6 +46,30 @@ "dns_view": "default" } }, + "MONITORING": { + "LIBRENMS": { + "endpoint": "https://librenms.test.gap.geant.org/", + "token": "<token>", + "DEVICE_GROUPS": { + "routers_lab": "lab_routers", + "routers_prod": "prod_routers" + } + }, + "SNMP": { + "version": "v2c", + "V2": { + "community": "librenms-community" + }, + "V3": { + "authlevel": "AuthPriv", + "authname": "librenms", + "authpass": "<password1>", + "authalgo": "sha", + "cryptopass": "<password2>", + "cryptoalgo": "aes" + } + } + }, "PROVISIONING_PROXY": { "scheme": "https", "api_base": "localhost:44444", diff --git a/gso/services/librenms.py b/gso/services/librenms.py new file mode 100644 index 0000000000000000000000000000000000000000..1ab11514c3972389d4f01c34165aee79503e25f8 --- /dev/null +++ b/gso/services/librenms.py @@ -0,0 +1,176 @@ +""" +The LibreNMS module interacts with the LibreNMS instance when +- Creating a device. +- Validating the input of a device. +- Terminating a device. +""" +import json +import logging +import requests +from gso import settings + + +logger = logging.getLogger(__name__) + + +class CfgStruct(object): + pass + + +def _get_cfg(): + """ + Internal function to retrieve all needed configuration. + """ + oss = settings.load_oss_params() + cfg = CfgStruct() + # Hack for later ease: 1st setattr will fill in the inner's dict + setattr(cfg, "_hack", "") + # Update inner dict + cfg.__dict__.update(oss.MONITORING) + assert cfg.__dict__ is not None + + # Add parameters on-the-fly + cfg.headers = {"X-Auth-Token": cfg.LIBRENMS.token} + + sep = "/" + if cfg.LIBRENMS.endpoint.endswith("/"): + sep = "" + cfg.base_url = f"{cfg.LIBRENMS.endpoint}{sep}api/v0" + cfg.url_devices = f"{cfg.base_url}/devices" + cfg.url_switches = f"{cfg.base_url}/devicegroups/switches" + cfg.device_groups = cfg.LIBRENMS.DEVICE_GROUPS + cfg.environment = oss.GENERAL.environment + if cfg.environment.startswith("lab"): + cfg_dg_rtr_lab = cfg.device_groups.routers_lab + cfg.url_routers = f"{cfg.base_url}/devicegroups/{cfg_dg_rtr_lab}" + elif cfg.environment.startswith("prod"): + cfg_dg_rtr_prod = cfg.device_groups.routers_prod + cfg.url_routers = f"{cfg.base_url}/devicegroups/{cfg_dg_rtr_prod}" + + return cfg + + +def validate_device(fqdn: str): + """ + Function that validates the existence of a device in LibreNMS. + + :param FQDN of the device to validate. + """ + CFG = _get_cfg() + + # Validate existence + nms_result = requests.get( + CFG.url_devices, headers=CFG.headers) + assert nms_result is not None + + device_id = list(map( + lambda x: x.get("device_id"), + filter(lambda x: x.get("hostname") == fqdn, + nms_result.json().get("devices")))) + + if len(device_id) != 1 or device_id[0] is None: + error_msg = f"Device with FQDN={fqdn} is not registered in LibreNMS" + print(error_msg) + raise AssertionError(error_msg) + + # Validate correctness + device_id = device_id[0] + url_device = f"{CFG.url_devices}/{device_id}" + logger.debug(f"Connecting to URL: {url_device}" + f"with headers: {CFG.headers}") + nms_result = requests.get( + url_device, headers=CFG.headers) + logger.debug(f"LibreNMS response={nms_result.content}") + + if nms_result.status_code != 200: + print(nms_result.content) + raise AssertionError(nms_result.content) + + # nms_dev_sysname = nms_result.json().get("sysName") + nms_dev_hostname = nms_result.json().get("devices")[0].get("hostname") + if fqdn != nms_dev_hostname: + error_msg = f"Device with FQDN={fqdn} may not be correctly "\ + f"registered in LibreNMS (expected FQDN: {nms_dev_hostname})" + print(error_msg) + raise AssertionError(error_msg) + + +def register_device(fqdn: str): + """ + Function that registers a new device in LibreNMS. + + :param FQDN of the device to register. + """ + CFG = _get_cfg() + logger.debug(f"Registering FQDN={fqdn} in LibreNMS") + + device_data = { + "display": fqdn, + "hostname": fqdn, + "sysName": fqdn, + # "override_icmp_disable": "true", + # IMPORTANT: uncomment if testing with FQDNs that are not reachable + # from LibreNMS (e.g. ContainerLab routers) + # "force_add": "true" + } + if CFG.SNMP.version == "v2c": + device_data.update({ + "community": CFG.SNMP.V2.community + }) + + elif CFG.SNMP.version == "v3": + for key in [ + "authlevel", "authname", "authpass", "authalgo", + "cryptopass", "cryptoalgo"]: + device_data.update({key: getattr(CFG.SNMP.V3, key)}) + + logger.debug(f"Connecting to URL: {CFG.url_devices}" + f"with headers: {CFG.headers} and" + f"payload: {device_data}") + nms_result = requests.post( + CFG.url_devices, headers=CFG.headers, + data=json.dumps(device_data)) + logger.debug(f"LibreNMS response={nms_result.content}") + + if nms_result.status_code != 200: + print(nms_result.content) + raise AssertionError(nms_result.content) + + +def deregister_device(fqdn: str): + """ + Function that reregisters a device from LibreNMS. + + :param FQDN of the device to deregister. + """ + CFG = _get_cfg() + logger.debug(f"Deregistering FQDN={fqdn} from LibreNMS") + + nms_result = requests.get( + CFG.url_devices, headers=CFG.headers) + assert nms_result is not None + device_id = list(map( + lambda x: x.get("device_id"), + filter(lambda x: x.get("hostname") == fqdn, + nms_result.json().get("devices")))) + if len(device_id) != 1: + return + device_id = device_id[0] + + # https://docs.librenms.org/API/Devices/#endpoint-categories + device_data = { + "field": "disabled", + "data": "1" + } + url_device = f"{CFG.url_devices}/{device_id}" + logger.debug(f"Connecting to URL: {url_device}" + f"with headers: {CFG.headers} and" + f"payload: {device_data}") + nms_result = requests.patch( + url_device, headers=CFG.headers, + data=json.dumps(device_data)) + logger.debug(f"LibreNMS response={nms_result.content}") + + # Fail silently if device was not registered + if nms_result.status_code != 200: + print(nms_result.content) diff --git a/gso/settings.py b/gso/settings.py index 78a27d756b03de700f1f7afcbcca6fe681a960f9..0e9d55e3f4c5345628031f7fe4ea3168d37ba7f9 100644 --- a/gso/settings.py +++ b/gso/settings.py @@ -21,6 +21,8 @@ class GeneralParams(BaseSettings): public_hostname: str """The hostname that :term:`GSO` is publicly served at, used for building the callback URL that the provisioning proxy uses.""" + environment: str + """The environment in which :term:`GSO` runs.""" class CeleryParams(BaseSettings): @@ -94,6 +96,59 @@ class IPAMParams(BaseSettings): LT_IAS: ServiceNetworkParams +class MonitoringLibreNMSDevGroupsParams(BaseSettings): + """ + Parameters related to LibreNMS device groups. + """ + routers_lab: str + routers_prod: str + + +class MonitoringSNMPV2Params(BaseSettings): + """ + Parameters related to SNMPv2. + """ + community: str + + +class MonitoringSNMPV3Params(BaseSettings): + """ + Parameters related to SNMPv3. + """ + authlevel: str + authname: str + authpass: str + authalgo: str + cryptopass: str + cryptoalgo: str + + +class MonitoringSNMPParams(BaseSettings): + """ + Parameters related to SNMP. + """ + version: str + V2: MonitoringSNMPV2Params + V3: MonitoringSNMPV3Params + + +class MonitoringLibreNMSParams(BaseSettings): + """ + Parameters related to LibreNMS. + """ + endpoint: str + token: str + DEVICE_GROUPS: MonitoringLibreNMSDevGroupsParams + + +class MonitoringParams(BaseSettings): + """ + Parameters related to the monitoring. + """ + LIBRENMS: MonitoringLibreNMSParams + SNMP: MonitoringSNMPParams + + class ProvisioningProxyParams(BaseSettings): """Parameters for the provisioning proxy.""" @@ -118,6 +173,7 @@ class OSSParams(BaseSettings): GENERAL: GeneralParams IPAM: IPAMParams NETBOX: NetBoxParams + MONITORING: MonitoringParams PROVISIONING_PROXY: ProvisioningProxyParams CELERY: CeleryParams