diff --git a/README.md b/README.md index d0f14ed..65f1eaa 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ PLUGINS_CONFIG = { "NAUTOBOT_INFOBLOX_PASSWORD": os.getenv("NAUTOBOT_INFOBLOX_PASSWORD", ""), "NAUTOBOT_INFOBLOX_VERIFY_SSL": os.getenv("NAUTOBOT_INFOBLOX_VERIFY_SSL", "true"), "NAUTOBOT_INFOBLOX_WAPI_VERSION": os.getenv("NAUTOBOT_INFOBLOX_WAPI_VERSION", "v2.12"), + "enable_sync_to_infoblox": False, + "enable_rfc1918_network_containers": False, } } ``` diff --git a/development/nautobot_config.py b/development/nautobot_config.py index 87e7733..1c486e2 100644 --- a/development/nautobot_config.py +++ b/development/nautobot_config.py @@ -306,6 +306,7 @@ "NAUTOBOT_INFOBLOX_VERIFY_SSL": os.getenv("NAUTOBOT_INFOBLOX_VERIFY_SSL", "true"), "NAUTOBOT_INFOBLOX_WAPI_VERSION": os.getenv("NAUTOBOT_INFOBLOX_WAPI_VERSION", "v2.12"), "enable_sync_to_infoblox": False, + "enable_rfc1918_network_containers": True, }, "nautobot_ssot": { "hide_example_jobs": True, diff --git a/nautobot_ssot_infoblox/__init__.py b/nautobot_ssot_infoblox/__init__.py index 0ba8aa0..1be9fe0 100644 --- a/nautobot_ssot_infoblox/__init__.py +++ b/nautobot_ssot_infoblox/__init__.py @@ -28,6 +28,7 @@ class NautobotSSoTInfobloxConfig(PluginConfig): max_version = "1.9999" default_settings = { "enable_sync_to_infoblox": False, + "enable_rfc1918_network_containers": False, } caching_config = {} diff --git a/nautobot_ssot_infoblox/diffsync/adapters/infoblox.py b/nautobot_ssot_infoblox/diffsync/adapters/infoblox.py index af2da1a..8defc7b 100644 --- a/nautobot_ssot_infoblox/diffsync/adapters/infoblox.py +++ b/nautobot_ssot_infoblox/diffsync/adapters/infoblox.py @@ -1,7 +1,10 @@ """Infoblox Adapter for Infoblox integration with SSoT plugin.""" +import ipaddress + from diffsync import DiffSync from nautobot_ssot_infoblox.diffsync.client import InfobloxApi from nautobot_ssot_infoblox.diffsync.models.infoblox import ( + InfobloxAggregate, InfobloxIPAddress, InfobloxNetwork, InfobloxVLANView, @@ -80,3 +83,34 @@ def load(self): self.load_ipaddresses() self.load_vlanviews() self.load_vlans() + + +class InfobloxAggregateAdapter(DiffSync): + """DiffSync adapter using requests to communicate to Infoblox server.""" + + aggregate = InfobloxAggregate + + top_level = ["aggregate"] + + def __init__(self, *args, job=None, sync=None, **kwargs): + """Initialize Infoblox. + + Args: + job (object, optional): Infoblox job. Defaults to None. + sync (object, optional): Infoblox DiffSync. Defaults to None. + """ + super().__init__(*args, **kwargs) + self.job = job + self.sync = sync + self.conn = InfobloxApi() + + def load(self): + """Method for loading aggregate models.""" + for container in self.conn.get_network_containers(): + network = ipaddress.ip_network(container["network"]) + if network.is_private: + new_aggregate = self.aggregate( + network=container["network"], + description=container["comment"] if container.get("comment") else "", + ) + self.add(new_aggregate) diff --git a/nautobot_ssot_infoblox/diffsync/adapters/nautobot.py b/nautobot_ssot_infoblox/diffsync/adapters/nautobot.py index 241d15c..3b15497 100644 --- a/nautobot_ssot_infoblox/diffsync/adapters/nautobot.py +++ b/nautobot_ssot_infoblox/diffsync/adapters/nautobot.py @@ -1,8 +1,14 @@ """Nautobot Adapter for Infoblox integration with SSoT plugin.""" import re from diffsync import DiffSync -from nautobot.ipam.models import IPAddress, Prefix, VLAN, VLANGroup -from nautobot_ssot_infoblox.diffsync.models import NautobotNetwork, NautobotIPAddress, NautobotVlanGroup, NautobotVlan +from nautobot.ipam.models import Aggregate, IPAddress, Prefix, VLAN, VLANGroup +from nautobot_ssot_infoblox.diffsync.models import ( + NautobotAggregate, + NautobotNetwork, + NautobotIPAddress, + NautobotVlanGroup, + NautobotVlan, +) from nautobot_ssot_infoblox.utils import nautobot_vlan_status @@ -77,3 +83,31 @@ def load(self): self.load_ipaddresses() self.load_vlangroups() self.load_vlans() + + +class NautobotAggregateAdapter(DiffSync): + """DiffSync adapter using ORM to communicate to Nautobot Aggregrates.""" + + aggregate = NautobotAggregate + + top_level = ["aggregate"] + + def __init__(self, *args, job=None, sync=None, **kwargs): + """Initialize Nautobot. + + Args: + job (object, optional): Nautobot job. Defaults to None. + sync (object, optional): Nautobot DiffSync. Defaults to None. + """ + super().__init__(*args, **kwargs) + self.job = job + self.sync = sync + + def load(self): + """Method to load aggregate models from Nautobot.""" + for aggregate in Aggregate.objects.all(): + _aggregate = self.aggregate( + network=str(aggregate.prefix), + description=aggregate.description, + ) + self.add(_aggregate) diff --git a/nautobot_ssot_infoblox/diffsync/client.py b/nautobot_ssot_infoblox/diffsync/client.py index d56b1f6..a09ca68 100644 --- a/nautobot_ssot_infoblox/diffsync/client.py +++ b/nautobot_ssot_infoblox/diffsync/client.py @@ -1,4 +1,4 @@ -"""All interactions with infoblox.""" +"""All interactions with infoblox.""" # pylint: disable=too-many-lines import copy import json @@ -120,6 +120,22 @@ def _get_network_ref(self, prefix): # pylint: disable=inconsistent-return-state if item["network"] == prefix: return item["_ref"] + def _get_network_container_ref(self, prefix): # pylint: disable=inconsistent-return-statements + """Fetch the _ref of a networkcontainer resource. + + Args: + prefix (str): IPv4 Prefix to fetch the _ref for. + + Returns: + (str) networkcontainer _ref or None + + Returns Response: + "networkcontainer/ZG5zLm5ldHdvcmtfY29udGFpbmVyJDE5Mi4xNjguMi4wLzI0LzA:192.168.2.0/24/default" + """ + for item in self.get_network_containers(): + if item["network"] == prefix: + return item["_ref"] + def get_all_ipv4address_networks(self, prefix): """Gets all used / unused IPv4 addresses within the supplied network. @@ -233,7 +249,7 @@ def get_all_networks(self, prefix=None): logger.info(response.json) return response.json().get("result") - def create_network(self, prefix): + def create_network(self, prefix, comment=None): """Create a network. Args: @@ -245,7 +261,7 @@ def create_network(self, prefix): Return Response: "network/ZG5zLm5ldHdvcmskMTkyLjE2OC4wLjAvMjMvMA:192.168.0.0/23/default" """ - params = {"network": prefix} + params = {"network": prefix, "comment": comment} api_path = "network" response = self._request("POST", api_path, params=params) logger.info(response.text) @@ -298,6 +314,71 @@ def update_network(self, prefix, comment=None): logger.info(response) return response + def create_network_container(self, prefix, comment=None): + """Create a network container. + + Args: + prefix (str): IP network to create. + + Returns: + (str) of reference network + + Return Response: + "networkcontainer/ZG5zLm5ldHdvcmskMTkyLjE2OC4wLjAvMjMvMA:192.168.0.0/23/default" + """ + params = {"network": prefix, "comment": comment} + api_path = "networkcontainer" + response = self._request("POST", api_path, params=params) + logger.info(response.text) + return response.text + + def delete_network_container(self, prefix): + """Delete a network container. + + Args: + prefix (str): IPv4 prefix to delete. + + Returns: + (dict) deleted prefix. + + Returns Response: + {"deleted": "networkcontainer/ZG5zLm5ldHdvcmskMTkyLjAuMi4wLzI0LzA:192.0.2.0/24/default"} + """ + resource = self._get_network_container_ref(prefix) + + if resource: + self._delete(resource) + response = {"deleted": resource} + else: + response = {"error": f"{prefix} not found."} + + logger.info(response) + return response + + def update_network_container(self, prefix, comment=None): + """Update a network container. + + Args: + (str): IPv4 prefix to update. + comment (str): IPv4 prefix update comment. + + Returns: + (dict) updated prefix. + + Return Response: + {"updated": "networkcontainer/ZG5zLm5ldHdvcmskMTkyLjE2OC4wLjAvMjMvMA:192.168.0.0/23/default"} + """ + resource = self._get_network_container_ref(prefix) + + if resource: + params = {"network": prefix, "comment": comment} + self._update(resource, **params) + response = {"updated": resource} + else: + response = {"error": f"error updating {prefix}"} + logger.info(response) + return response + def get_host_record_by_name(self, fqdn): """Gets the host record by using FQDN. @@ -633,26 +714,6 @@ def get_authoritative_zone(self): "fqdn": "test-site", "view": "default" }, - { - "_ref": "zone_auth/ZG5zLnpvbmUkLl9kZWZhdWx0LmNvbS5uZXR3b3JrdG9jb2Rl:networktocode.com/default", - "fqdn": "networktocode.com", - "view": "default" - }, - { - "_ref": "zone_auth/ZG5zLnpvbmUkLl9kZWZhdWx0LmNvbS5uZXR3b3JrdG9jb2RlLm5ldHdvcms:network.networktocode.com/default", - "fqdn": "network.networktocode.com", - "view": "default" - }, - { - "_ref": "zone_auth/ZG5zLnpvbmUkLl9kZWZhdWx0LmNvbS5uZXR3b3JrdG9jb2RlLm5ldHdvcmsudGVzdC1zaXRl:test-site.network.networktocode.com/default", - "fqdn": "test-site.network.networktocode.com", - "view": "default" - }, - { - "_ref": "zone_auth/ZG5zLnpvbmUkLl9kZWZhdWx0LmFycGEuaW4tYWRkci4xMA:10.0.0.0%2F8/default", - "fqdn": "10.0.0.0/8", - "view": "default" - } ] """ url_path = "zone_auth" @@ -992,3 +1053,24 @@ def update_ipaddress(self, ip_address, **data): # pylint: disable=inconsistent- response = self._request("PUT", path=resource, params=params, json=data) logger.info("Infoblox IP Address updated: %s", response.json()) return response.json() + + def get_network_containers(self): + """Get all Network Containers. + + Returns: + (list) of record dicts + + Return Response: + [ + { + "_ref": "networkcontainer/ZG5zLm5ldHdvcmtfY29udGFpbmVyJDE5Mi4xNjguMi4wLzI0LzA:192.168.2.0/24/default", + "network": "192.168.2.0/24", + "network_view": "default" + } + ] + """ + url_path = "networkcontainer" + params = {"_return_as_object": 1, "_return_fields": "network,comment,network_view"} + response = self._request("GET", url_path, params=params) + logger.info(response.json) + return response.json().get("result") diff --git a/nautobot_ssot_infoblox/diffsync/models/__init__.py b/nautobot_ssot_infoblox/diffsync/models/__init__.py index 86731aa..4c2eb68 100644 --- a/nautobot_ssot_infoblox/diffsync/models/__init__.py +++ b/nautobot_ssot_infoblox/diffsync/models/__init__.py @@ -1,13 +1,15 @@ """Initialize models for Nautobot and Infoblox.""" -from .nautobot import NautobotNetwork, NautobotIPAddress, NautobotVlanGroup, NautobotVlan -from .infoblox import InfobloxNetwork, InfobloxIPAddress, InfobloxVLANView, InfobloxVLAN +from .nautobot import NautobotAggregate, NautobotNetwork, NautobotIPAddress, NautobotVlanGroup, NautobotVlan +from .infoblox import InfobloxAggregate, InfobloxNetwork, InfobloxIPAddress, InfobloxVLANView, InfobloxVLAN __all__ = [ + "NautobotAggregate", "NautobotNetwork", "NautobotIPAddress", "NautobotVlanGroup", "NautobotVlan", + "InfobloxAggregate", "InfobloxNetwork", "InfobloxIPAddress", "InfobloxVLANView", diff --git a/nautobot_ssot_infoblox/diffsync/models/base.py b/nautobot_ssot_infoblox/diffsync/models/base.py index c7eff51..20c49c4 100644 --- a/nautobot_ssot_infoblox/diffsync/models/base.py +++ b/nautobot_ssot_infoblox/diffsync/models/base.py @@ -51,3 +51,14 @@ class IPAddress(DiffSyncModel): prefix: str status: str description: Optional[str] + + +class Aggregate(DiffSyncModel): + """Aggregate model for DiffSync.""" + + _modelname = "aggregate" + _identifiers = ("network",) + _attributes = ("description",) + + network: str + description: Optional[str] diff --git a/nautobot_ssot_infoblox/diffsync/models/infoblox.py b/nautobot_ssot_infoblox/diffsync/models/infoblox.py index 1d3b395..56f85a0 100644 --- a/nautobot_ssot_infoblox/diffsync/models/infoblox.py +++ b/nautobot_ssot_infoblox/diffsync/models/infoblox.py @@ -1,5 +1,5 @@ """Infoblox Models for Infoblox integration with SSoT plugin.""" -from nautobot_ssot_infoblox.diffsync.models.base import Network, IPAddress, Vlan, VlanView +from nautobot_ssot_infoblox.diffsync.models.base import Aggregate, Network, IPAddress, Vlan, VlanView class InfobloxNetwork(Network): @@ -59,3 +59,27 @@ def update(self, attrs): json = {"comment": attrs["description"]} self.diffsync.conn.update_ipaddress(address=self.get_identifiers()["address"], data=json) return super().update(attrs) + + +class InfobloxAggregate(Aggregate): + """Infoblox implementation of the Aggregate Model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Network Container object in Infoblox.""" + diffsync.conn.create_network_container( + prefix=ids["network"], comment=attrs["description"] if attrs.get("description") else "" + ) + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Network Container object in Infoblox.""" + self.diffsync.conn.update_network_container( + prefix=self.get_identifiers()["network"], comment=attrs["description"] if attrs.get("description") else "" + ) + return super().update(attrs) + + def delete(self): + """Delete Network Container object in Infoblox.""" + self.diffsync.conn.delete_network_container(self.get_identifiers()["network"]) + return super().delete() diff --git a/nautobot_ssot_infoblox/diffsync/models/nautobot.py b/nautobot_ssot_infoblox/diffsync/models/nautobot.py index 204925b..9513f6d 100644 --- a/nautobot_ssot_infoblox/diffsync/models/nautobot.py +++ b/nautobot_ssot_infoblox/diffsync/models/nautobot.py @@ -2,11 +2,13 @@ from django.utils.text import slugify from nautobot.extras.models import Status as OrmStatus from nautobot.extras.models import Tag +from nautobot.ipam.models import RIR +from nautobot.ipam.models import Aggregate as OrmAggregate from nautobot.ipam.models import IPAddress as OrmIPAddress from nautobot.ipam.models import Prefix as OrmPrefix from nautobot.ipam.models import VLAN as OrmVlan from nautobot.ipam.models import VLANGroup as OrmVlanGroup -from nautobot_ssot_infoblox.diffsync.models.base import Network, IPAddress, Vlan, VlanView +from nautobot_ssot_infoblox.diffsync.models.base import Aggregate, Network, IPAddress, Vlan, VlanView class NautobotNetwork(Network): @@ -141,3 +143,35 @@ def delete(self): _vlan = OrmVlan.objects.get(vid=self.get_identifiers()["vid"]) _vlan.delete() return super().delete() + + +class NautobotAggregate(Aggregate): + """Nautobot implementation of the Aggregate Model.""" + + @classmethod + def create(cls, diffsync, ids, attrs): + """Create Aggregate object in Nautobot.""" + rir, _ = RIR.objects.get_or_create(name="RFC1918", slug="rfc1918", is_private=True) + _aggregate = OrmAggregate( + prefix=ids["network"], + rir=rir, + description=attrs["description"] if attrs.get("description") else "", + ) + _aggregate.tags.add(Tag.objects.get(slug="created-by-infoblox")) + _aggregate.validated_save() + return super().create(ids=ids, diffsync=diffsync, attrs=attrs) + + def update(self, attrs): + """Update Aggregate object in Nautobot.""" + _aggregate = OrmAggregate.objects.get(prefix=self.network) + if attrs.get("description"): + _aggregate.description = attrs["description"] + _aggregate.validated_save() + return super().update(attrs) + + def delete(self): + """Delete Aggregate object in Nautobot.""" + self.diffsync.job.log_warning(f"Aggregate {self.network} will be deleted.") + _aggregate = OrmAggregate.objects.get(prefix=self.get_identifiers()["network"]) + _aggregate.delete() + return super().delete() diff --git a/nautobot_ssot_infoblox/jobs.py b/nautobot_ssot_infoblox/jobs.py index b9e9cbf..ff4a99a 100644 --- a/nautobot_ssot_infoblox/jobs.py +++ b/nautobot_ssot_infoblox/jobs.py @@ -105,7 +105,52 @@ def sync_data(self): self.log_success(message="Sync complete.") +class InfobloxNetworkContainerSource(DataSource, Job): + """Infoblox SSoT Network Container Source.""" + + debug = BooleanVar(description="Enable for verbose debug logging.") + + class Meta: # pylint: disable=too-few-public-methods + """Information about the Job.""" + + name = "Infoblox Network Containers" + data_source = "InfobloxNetworkContainers" + data_source_icon = static("nautobot_ssot_infoblox/infoblox_logo.png") + description = "Sync Network Container (Aggregate) infomation from Infoblox to Nautobot" + + @classmethod + def data_mappings(cls): + """Shows mapping of models between Infoblox and Nautobot.""" + return (DataMapping("networkcontainer", None, "Aggregate", reverse("ipam:aggregate_list")),) + + def sync_data(self): + """Method to handle synchronization of data to Nautobot.""" + self.log_info(message="Connecting to Infoblox") + infoblox_adapter = infoblox.InfobloxAggregateAdapter(job=self, sync=self.sync) + self.log_info(message="Loading data from Infoblox...") + infoblox_adapter.load() + self.log_info(message="Connecting to Nautobot...") + nb_adapter = nautobot.NautobotAggregateAdapter(job=self, sync=self.sync) + self.log_info(message="Loading data from Nautobot...") + nb_adapter.load() + self.log_info(message="Performing diff of data between Infoblox and Nautobot.") + diff = nb_adapter.diff_from(infoblox_adapter, flags=DiffSyncFlags.CONTINUE_ON_FAILURE) + self.sync.diff = diff.dict() + self.sync.save() + self.log_info(message=diff.summary()) + if not self.kwargs["dry_run"]: + self.log_info(message="Performing data synchronization from Infoblox to Nautobot.") + try: + nb_adapter.sync_from(infoblox_adapter, flags=DiffSyncFlags.CONTINUE_ON_FAILURE) + except ObjectNotCreated as err: + self.log_debug(f"Unable to create object. {err}") + self.log_success(message="Sync complete.") + + jobs = [InfobloxDataSource] if PLUGIN_CFG["enable_sync_to_infoblox"]: jobs.append(InfobloxDataTarget) + +if PLUGIN_CFG["enable_rfc1918_network_containers"]: + jobs.append(InfobloxNetworkContainerSource)