WIP: adds permitted subnet class and finishes redfish import

This commit is contained in:
ricardo.bartels@telekom.de
2023-02-09 22:48:15 +01:00
parent d580c6d932
commit 1519599805
11 changed files with 408 additions and 316 deletions

View File

@@ -20,35 +20,38 @@ class CommonConfig(ConfigBase):
section_name = common_config_section_name
options = [
ConfigOption("log_level",
str,
description="""\
Logs will always be printed to stdout/stderr.
Logging can be set to following log levels:
ERROR: Fatal Errors which stops regular a run
WARNING: Warning messages won't stop the syncing process but mostly worth
to have a look at.
INFO: Information about objects that will be create/updated/deleted in NetBox
DEBUG: Will log information about retrieved information, changes in internal
content structure and parsed config
DEBUG2: Will also log information about how/why content is parsed or skipped.
DEBUG3: Logs all source and NetBox queries/results to stdout. Very useful for
troubleshooting, but will log any sensitive content contained within a query.
""",
default_value="INFO"),
def __init__(self):
self.options = [
ConfigOption("log_level",
str,
description="""\
Logs will always be printed to stdout/stderr.
Logging can be set to following log levels:
ERROR: Fatal Errors which stops regular a run
WARNING: Warning messages won't stop the syncing process but mostly worth
to have a look at.
INFO: Information about objects that will be create/updated/deleted in NetBox
DEBUG: Will log information about retrieved information, changes in internal
content structure and parsed config
DEBUG2: Will also log information about how/why content is parsed or skipped.
DEBUG3: Logs all source and NetBox queries/results to stdout. Very useful for
troubleshooting, but will log any sensitive content contained within a query.
""",
default_value="INFO"),
ConfigOption("log_to_file",
bool,
description="""Enabling this options will write all
logs to a log file defined in 'log_file'
""",
default_value=True),
ConfigOption("log_to_file",
bool,
description="""Enabling this options will write all
logs to a log file defined in 'log_file'
""",
default_value=True),
ConfigOption("log_file",
str,
description="""Destination of the log file if "log_to_file" is enabled.
Log file will be rotated maximum 5 times once the log file reaches size of 10 MB
""",
default_value="log/netbox_sync.log")
]
ConfigOption("log_file",
str,
description="""Destination of the log file if "log_to_file" is enabled.
Log file will be rotated maximum 5 times once the log file reaches size of 10 MB
""",
default_value="log/netbox_sync.log")
]
super().__init__()

View File

@@ -36,6 +36,9 @@ class ConfigBase:
def validate_options(self):
pass
def set_validation_failed(self):
self._parsing_failed = True
def parse(self, do_log: bool = True):
def _log(handler, message):
@@ -113,7 +116,8 @@ class ConfigBase:
options[config_object.key] = config_object.value
config_options = get_value() or dict()
# check for unknown config options
config_options = get_value()
if not isinstance(config_options, dict):
config_options = dict()

View File

@@ -22,114 +22,117 @@ class NetBoxConfig(ConfigBase):
section_name = netbox_config_section_name
options = [
ConfigOption("api_token",
str,
description="""Requires an NetBox API token with full permissions on all objects except
'auth', 'secrets' and 'users'
""",
config_example="XYZ",
mandatory=True,
sensitive=True),
def __init__(self):
self.options = [
ConfigOption("api_token",
str,
description="""Requires an NetBox API token with full permissions on all objects except
'auth', 'secrets' and 'users'
""",
config_example="XYZ",
mandatory=True,
sensitive=True),
ConfigOption("host_fqdn",
str,
description="Requires a hostname or IP which points to your NetBox instance",
config_example="netbox.example.com",
mandatory=True),
ConfigOption("host_fqdn",
str,
description="Requires a hostname or IP which points to your NetBox instance",
config_example="netbox.example.com",
mandatory=True),
ConfigOption("port",
int,
description="""Define the port your NetBox instance is listening on.
If 'disable_tls' is set to "true" this option might be set to 80
""",
default_value=443),
ConfigOption("port",
int,
description="""Define the port your NetBox instance is listening on.
If 'disable_tls' is set to "true" this option might be set to 80
""",
default_value=443),
ConfigOption("disable_tls",
bool,
description="Whether TLS encryption is enabled or disabled",
default_value=False),
ConfigOption("disable_tls",
bool,
description="Whether TLS encryption is enabled or disabled",
default_value=False),
ConfigOption("validate_tls_certs",
bool,
description="""Enforces TLS certificate validation. If this system doesn't trust the NetBox
web server certificate then this option needs to be changed
""",
default_value=True),
ConfigOption("validate_tls_certs",
bool,
description="""Enforces TLS certificate validation. If this system doesn't trust the NetBox
web server certificate then this option needs to be changed
""",
default_value=True),
ConfigOption("proxy",
str,
description="""Defines a proxy which will be used to connect to NetBox.
Proxy setting needs to include the schema.
Proxy basic auth example: http://user:pass@10.10.1.10:312
""",
config_example="http://example.com:3128"),
ConfigOption("proxy",
str,
description="""Defines a proxy which will be used to connect to NetBox.
Proxy setting needs to include the schema.
Proxy basic auth example: http://user:pass@10.10.1.10:312
""",
config_example="http://example.com:3128"),
ConfigOption("client_cert",
str,
description="Specify a client certificate which can be used to authenticate to NetBox",
config_example="client.pem"),
ConfigOption("client_cert",
str,
description="Specify a client certificate which can be used to authenticate to NetBox",
config_example="client.pem"),
ConfigOption("client_cert_key",
str,
description="Specify the client certificate private key belonging to the client cert",
config_example="client.key"),
ConfigOption("client_cert_key",
str,
description="Specify the client certificate private key belonging to the client cert",
config_example="client.key"),
ConfigOption("prune_enabled",
bool,
description="""Whether items which were created by this program but
can't be found in any source anymore will be deleted or not
""",
default_value=False),
ConfigOption("prune_enabled",
bool,
description="""Whether items which were created by this program but
can't be found in any source anymore will be deleted or not
""",
default_value=False),
ConfigOption("prune_delay_in_days",
int,
description="""Orphaned objects will first be tagged before they get deleted.
Once the amount of days passed the object will actually be deleted
""",
default_value=30),
ConfigOption("prune_delay_in_days",
int,
description="""Orphaned objects will first be tagged before they get deleted.
Once the amount of days passed the object will actually be deleted
""",
default_value=30),
ConfigOption("ignore_unknown_source_object_pruning",
bool,
description="""This will tell netbox-sync to ignore objects in NetBox
with tag 'NetBox-synced' from pruning if the source is not defined in
this config file (https://github.com/bb-Ricardo/netbox-sync/issues/176)
""",
default_value=False),
ConfigOption("ignore_unknown_source_object_pruning",
bool,
description="""This will tell netbox-sync to ignore objects in NetBox
with tag 'NetBox-synced' from pruning if the source is not defined in
this config file (https://github.com/bb-Ricardo/netbox-sync/issues/176)
""",
default_value=False),
ConfigOption("default_netbox_result_limit",
int,
description="""The maximum number of objects returned in a single request.
If a NetBox instance is very quick responding the value should be raised
""",
default_value=200),
ConfigOption("default_netbox_result_limit",
int,
description="""The maximum number of objects returned in a single request.
If a NetBox instance is very quick responding the value should be raised
""",
default_value=200),
ConfigOption("timeout",
int,
description="""The maximum time a query is allowed to execute before being
killed and considered failed
""",
default_value=30),
ConfigOption("timeout",
int,
description="""The maximum time a query is allowed to execute before being
killed and considered failed
""",
default_value=30),
ConfigOption("max_retry_attempts",
int,
description="""The amount of times a failed request will be reissued.
Once the maximum is reached the syncing process will be stopped completely.
""",
default_value=4),
ConfigOption("max_retry_attempts",
int,
description="""The amount of times a failed request will be reissued.
Once the maximum is reached the syncing process will be stopped completely.
""",
default_value=4),
ConfigOption("use_caching",
bool,
description="""Defines if caching of NetBox objects is used or not.
If problems with unresolved dependencies occur, switching off caching might help.
""",
default_value=True),
ConfigOption("use_caching",
bool,
description="""Defines if caching of NetBox objects is used or not.
If problems with unresolved dependencies occur, switching off caching might help.
""",
default_value=True),
ConfigOption("cache_directory_location",
str,
description="The location of the directory where the cache files should be stored",
default_value="cache")
]
ConfigOption("cache_directory_location",
str,
description="The location of the directory where the cache files should be stored",
default_value="cache")
]
super().__init__()
def validate_options(self):
@@ -140,4 +143,4 @@ class NetBoxConfig(ConfigBase):
(not option.value.startswith("http") and not option.value.startswith("socks5")):
log.error(f"Config option 'proxy' in '{NetBoxConfig.section_name}' must contain the schema "
f"http, https, socks5 or socks5h")
self._parsing_failed = True
self.set_validation_failed()

View File

@@ -7,10 +7,16 @@
# For a copy, see file LICENSE.txt included in this
# repository or visit: <https://opensource.org/licenses/MIT>.
import os
from module.config import source_config_section_name
from module.config.base import ConfigBase
from module.config.option import ConfigOption
from module.sources.common.source_base import config_option_enabled, config_option_permitted_subnets
from module.sources.common.conifg import *
from module.common.logging import get_logger
from module.sources.common.permitted_subnets import PermittedSubnets
log = get_logger()
class CheckRedfishConfig(ConfigBase):
@@ -18,49 +24,69 @@ class CheckRedfishConfig(ConfigBase):
section_name = source_config_section_name
source_name = None
options = [
config_option_enabled,
def __init__(self):
self.options = [
ConfigOption(**config_option_enabled_definition),
ConfigOption("type",
str,
description="type of source. This defines which source handler to use",
config_example="check_redfish",
mandatory=True),
ConfigOption("type",
str,
description="type of source. This defines which source handler to use",
config_example="check_redfish",
mandatory=True),
ConfigOption("inventory_file_path",
str,
description="define the full path where the check_redfish inventory json files are located",
mandatory=True),
ConfigOption("inventory_file_path",
str,
description="define the full path where the check_redfish inventory json files are located",
mandatory=True),
config_option_permitted_subnets,
ConfigOption(**config_option_permitted_subnets_definition),
ConfigOption("overwrite_host_name",
bool,
description="""define if the host name discovered via check_redfish
overwrites the device host name in NetBox""",
default_value=False),
ConfigOption("overwrite_host_name",
bool,
description="""define if the host name discovered via check_redfish
overwrites the device host name in NetBox""",
default_value=False),
ConfigOption("overwrite_power_supply_name",
bool,
description="""define if the name of the power supply discovered via check_redfish
overwrites the power supply name in NetBox""",
default_value=False),
ConfigOption("overwrite_power_supply_name",
bool,
description="""define if the name of the power supply discovered via check_redfish
overwrites the power supply name in NetBox""",
default_value=False),
ConfigOption("overwrite_power_supply_attributes",
bool,
description="""define if existing power supply attributes are overwritten with data discovered
via check_redfish if False only data which is not preset in NetBox will be added""",
default_value=True),
ConfigOption("overwrite_power_supply_attributes",
bool,
description="""define if existing power supply attributes are overwritten with data discovered
via check_redfish if False only data which is not preset in NetBox will be added""",
default_value=True),
ConfigOption("overwrite_interface_name",
bool,
description="""define if the name of the interface discovered via check_redfish
overwrites the interface name in NetBox""",
default_value=False),
ConfigOption("overwrite_interface_name",
bool,
description="""define if the name of the interface discovered via check_redfish
overwrites the interface name in NetBox""",
default_value=False),
ConfigOption("overwrite_interface_attributes",
bool,
description="""define if existing interface attributes are overwritten with data discovered
via check_redfish if False only data which is not preset in NetBox will be added""",
default_value=True)
]
ConfigOption("overwrite_interface_attributes",
bool,
description="""define if existing interface attributes are overwritten with data discovered
via check_redfish if False only data which is not preset in NetBox will be added""",
default_value=True)
]
super().__init__()
def validate_options(self):
for option in self.options:
if option.key == "inventory_file_path":
if not os.path.exists(option.value):
log.error(f"Inventory file path '{option.value}' not found.")
self.set_validation_failed()
if os.path.isfile(option.value):
log.error(f"Inventory file path '{option.value}' needs to be a directory.")
self.set_validation_failed()
if not os.access(option.value, os.X_OK | os.R_OK):
log.error(f"Inventory file path '{option.value}' not readable.")
self.set_validation_failed()

View File

@@ -7,7 +7,6 @@
# For a copy, see file LICENSE.txt included in this
# repository or visit: <https://opensource.org/licenses/MIT>.
from ipaddress import ip_network
import os
import glob
import json
@@ -18,8 +17,9 @@ from module.sources.common.source_base import SourceBase
from module.sources.check_redfish.config import CheckRedfishConfig
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
from module.common.support import normalize_mac_address
from module.netbox.inventory import NetBoxInventory
from module.sources.common.permitted_subnets import PermittedSubnets
from module.netbox.object_classes import (
NetBoxInterfaceType,
NBTag,
@@ -103,81 +103,15 @@ class CheckRedfish(SourceBase):
log.info(f"Source '{name}' is currently disabled. Skipping")
return
self.permitted_subnets = PermittedSubnets(self.settings.permitted_subnets)
if self.permitted_subnets.validation_failed is True:
log.error(f"Config parsing for source '{self.name}' failed.")
return
self.init_successful = True
self.interface_adapter_type_dict = dict()
def parse_config_settings(self, config_settings):
"""
Validate parsed settings from config file
Parameters
----------
config_settings: dict
dict of config settings
"""
validation_failed = False
for setting in ["inventory_file_path"]:
if config_settings.get(setting) is None:
log.error(f"Config option '{setting}' in 'source/{self.name}' can't be empty/undefined")
validation_failed = True
inv_path = config_settings.get("inventory_file_path")
if not os.path.exists(inv_path):
log.error(f"Inventory file path '{inv_path}' not found.")
validation_failed = True
if os.path.isfile(inv_path):
log.error(f"Inventory file path '{inv_path}' needs to be a directory.")
validation_failed = True
if not os.access(inv_path, os.X_OK | os.R_OK):
log.error(f"Inventory file path '{inv_path}' not readable.")
validation_failed = True
# check permitted ip subnets
permitted_subnets = list()
excluded_subnets = list()
self.settings["excluded_subnets"] = excluded_subnets
if config_settings.get("permitted_subnets") is None:
log.info(f"Config option 'permitted_subnets' in 'source/{self.name}' is undefined. "
f"No IP addresses will be populated to NetBox!")
else:
config_settings["permitted_subnets"] = \
[x.strip() for x in config_settings.get("permitted_subnets").split(",") if x.strip() != ""]
# add "invisible" config option
self.settings["excluded_subnets"] = None
for subnet in config_settings["permitted_subnets"]:
excluded = False
if subnet[0] == "!":
excluded = True
subnet = subnet[1:].strip()
try:
if excluded is True:
excluded_subnets.append(ip_network(subnet))
else:
permitted_subnets.append(ip_network(subnet))
except Exception as e:
log.error(f"Problem parsing permitted subnet: {e}")
validation_failed = True
config_settings["permitted_subnets"] = permitted_subnets
config_settings["excluded_subnets"] = excluded_subnets
if validation_failed is True:
log.error("Config validation failed. Exit!")
exit(1)
for setting in self.settings.keys():
setattr(self, setting, config_settings.get(setting))
def apply(self):
"""
Main source handler method. This method is called for each source from "main" program
@@ -870,17 +804,13 @@ class CheckRedfish(SourceBase):
# collect ip addresses
nic_ips[port_name] = list()
for ipv4_address in grab(nic_port, "ipv4_addresses", fallback=list()):
if ip_valid_to_add_to_netbox(ipv4_address, self.settings.permitted_subnets,
excluded_subnets=self.settings.excluded_subnets,
interface_name=port_name) is False:
if self.permitted_subnets.permitted(ipv4_address, interface_name=port_name) is False:
continue
nic_ips[port_name].append(ipv4_address)
for ipv6_address in grab(nic_port, "ipv6_addresses", fallback=list()):
if ip_valid_to_add_to_netbox(ipv6_address, self.settings.permitted_subnets,
excluded_subnets=self.settings.excluded_subnets,
interface_name=port_name) is False:
if self.permitted_subnets.permitted(ipv6_address, interface_name=port_name) is False:
continue
nic_ips[port_name].append(ipv6_address)

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020 - 2022 Ricardo Bartels. All rights reserved.
#
# netbox-sync.py
#
# This work is licensed under the terms of the MIT license.
# For a copy, see file LICENSE.txt included in this
# repository or visit: <https://opensource.org/licenses/MIT>.
config_option_enabled_definition = {
"key": "enabled",
"value_type": bool,
"description": "Defines if this source is enabled or not",
"default_value": True
}
config_option_permitted_subnets_definition = {
"key": "permitted_subnets",
"value_type": str,
"description": """IP networks eligible to be synced to NetBox. If an IP address is not part of
this networks then it WON'T be synced to NetBox. To excluded small blocks from bigger IP blocks
a leading '!' has to be added
""",
"config_example": "10.0.0.0/8, !10.23.42.0/24"
}

View File

@@ -0,0 +1,113 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2020 - 2022 Ricardo Bartels. All rights reserved.
#
# netbox-sync.py
#
# This work is licensed under the terms of the MIT license.
# 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
from module.common.logging import get_logger
log = get_logger()
class PermittedSubnets:
"""
initializes and verifies if an IP address is part of an permitted subnet
"""
def __init__(self, config_string: str):
self._validation_failed = False
self.included_subnets = list()
self.excluded_subnets = list()
if config_string is None:
log.info(f"Config option 'permitted_subnets' is undefined. No IP addresses will be populated to NetBox!")
self._validation_failed = True
if not isinstance(config_string, str):
raise ValueError("permitted subnets need to be of type string")
subnet_list = [x.strip() for x in config_string.split(",") if x.strip() != ""]
for subnet in subnet_list:
excluded = False
if subnet[0] == "!":
excluded = True
subnet = subnet[1:].strip()
try:
if excluded is True:
self.excluded_subnets.append(ip_network(subnet))
else:
self.included_subnets.append(ip_network(subnet))
except Exception as e:
log.error(f"Problem parsing permitted subnet: {e}")
self._validation_failed = True
@property
def validation_failed(self) -> bool:
return self._validation_failed
def permitted(self, ip, interface_name=None) -> bool:
"""
performs a couple of checks to see if an IP address is valid and allowed
to be added to NetBox
IP address must always be passed as interface notation
* 192.168.0.1/24
* fd00::0/64
* 192.168.23.24/255.255.255.24
Parameters
----------
ip: str
IP address to validate
interface_name: str
name of the interface this IP shall be added. Important for meaningful log messages
Returns
-------
bool: if IP address is valid
"""
if ip is None:
log.warning("No IP address passed to validate if this IP belongs to a permitted subnet")
return False
ip_text = f"'{ip}'"
if interface_name is not None:
ip_text = f"{ip_text} for {interface_name}"
try:
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
if ip_a.is_link_local is True:
log.debug(f"IP address {ip_text} is a link local address. Skipping.")
return False
if ip_a.is_loopback is True:
log.debug(f"IP address {ip_text} is a loopback address. Skipping.")
return False
for excluded_subnet in self.excluded_subnets:
if ip_a in excluded_subnet:
return False
for permitted_subnet in self.included_subnets:
if ip_a in permitted_subnet:
return True
log.debug(f"IP address {ip_text} not part of any permitted subnet. Skipping.")
return False

View File

@@ -25,25 +25,9 @@ from module.netbox.inventory import (
)
from module.common.logging import get_logger
from module.common.misc import grab
from module.config.option import ConfigOption
log = get_logger()
config_option_enabled = \
ConfigOption("enabled",
bool,
description="Defines if this source is enabled or not",
default_value=True)
config_option_permitted_subnets = \
ConfigOption("permitted_subnets",
str,
description="""IP networks eligible to be synced to NetBox. If an IP address is not part of
this networks then it WON'T be synced to NetBox. To excluded small blocks from bigger IP blocks
a leading '!' has to be added
""",
config_example="10.0.0.0/8, !10.23.42.0/24")
class SourceBase:
"""

View File

@@ -10,7 +10,7 @@
from module.config import source_config_section_name
from module.config.base import ConfigBase
from module.config.option import ConfigOption
from module.sources.common.source_base import config_option_enabled, config_option_permitted_subnets
from module.sources.common.conifg import *
class VMWareConfig(ConfigBase):
@@ -18,58 +18,70 @@ class VMWareConfig(ConfigBase):
section_name = source_config_section_name
source_name = None
options = [
config_option_enabled,
def __init__(self):
self.options = [
ConfigOption(**config_option_enabled_definition),
ConfigOption("type",
str,
description="type of source. This defines which source handler to use",
config_example="vmware",
mandatory=True),
ConfigOption("type",
str,
description="type of source. This defines which source handler to use",
config_example="vmware",
mandatory=True),
ConfigOption("host_fqdn",
str,
description="host name / IP address of the vCenter",
config_example="my-netbox.local",
mandatory=True),
ConfigOption("host_fqdn",
str,
description="host name / IP address of the vCenter",
config_example="my-netbox.local",
mandatory=True),
ConfigOption("port",
int,
description="TCP port to connect to",
default_value=443,
mandatory=True),
ConfigOption("port",
int,
description="TCP port to connect to",
default_value=443,
mandatory=True),
ConfigOption("username",
str,
description="username to use to log into vCenter",
config_example="vcenter-admin",
mandatory=True),
ConfigOption("username",
str,
description="username to use to log into vCenter",
config_example="vcenter-admin",
mandatory=True),
ConfigOption("password",
str,
description="password to use to log into vCenter",
config_example="super-secret",
sensitive=True,
mandatory=True),
ConfigOption("password",
str,
description="password to use to log into vCenter",
config_example="super-secret",
sensitive=True,
mandatory=True),
ConfigOption("validate_tls_certs",
bool,
description="""Enforces TLS certificate validation.
If vCenter uses a valid TLS certificate then this option should be set
to 'true' to ensure a secure connection."""),
ConfigOption("validate_tls_certs",
bool,
description="""Enforces TLS certificate validation.
If vCenter uses a valid TLS certificate then this option should be set
to 'true' to ensure a secure connection."""),
ConfigOption("proxy_host",
str,
description="""EXPERIMENTAL: Connect to a vCenter using a proxy server
(socks proxies are not supported). define a host name or an IP address""",
config_example="10.10.1.10"),
ConfigOption("proxy_host",
str,
description="""EXPERIMENTAL: Connect to a vCenter using a proxy server
(socks proxies are not supported). define a host name or an IP address""",
config_example="10.10.1.10"),
ConfigOption("proxy_port",
int,
description="""EXPERIMENTAL: Connect to a vCenter using a proxy server
(socks proxies are not supported).
define proxy server port number""",
config_example=3128),
ConfigOption("proxy_port",
int,
description="""EXPERIMENTAL: Connect to a vCenter using a proxy server
(socks proxies are not supported).
define proxy server port number""",
config_example=3128),
config_option_permitted_subnets
]
ConfigOption(**config_option_permitted_subnets_definition),
ConfigOption("cluster_exclude_filter",
str),
]
super().__init__()
def validate_options(self):
for option in self.options:
pass

View File

@@ -31,6 +31,7 @@ from module.common.logging import get_logger, DEBUG3
from module.common.misc import grab, dump, get_string_or_none, plural, quoted_split
from module.common.support import normalize_mac_address, ip_valid_to_add_to_netbox
from module.netbox.inventory import NetBoxInventory
from module.sources.common.permitted_subnets import PermittedSubnets
from module.netbox.object_classes import (
NetBoxInterfaceType,
NBTag,
@@ -95,14 +96,6 @@ class VMWareHandler(SourceBase):
]
settings = {
"enabled": True,
"host_fqdn": None,
"port": 443,
"username": None,
"password": None,
"validate_tls_certs": False,
"proxy_host": None,
"proxy_port": None,
"cluster_exclude_filter": None,
"cluster_include_filter": None,
"host_exclude_filter": None,

View File

@@ -78,7 +78,7 @@ def main():
inventory = NetBoxInventory()
# establish NetBox connection
#nb_handler = NetBoxHandler(nb_sync_version=__version__)
nb_handler = NetBoxHandler(nb_sync_version=__version__)
# if purge was selected we go ahead and remove all items which were managed by this tools
if args.purge is True:
@@ -94,7 +94,6 @@ def main():
# instantiate source handlers and get attributes
log.info("Initializing sources")
sources = instantiate_sources()
exit(0)
# all sources are unavailable
if len(sources) == 0: