Skip to content

Commit 128fd08

Browse files
committed
Merge branch 'develop'
2 parents 8a985d1 + 2da5cd8 commit 128fd08

30 files changed

+1380
-741
lines changed

.dockerignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.git
2+
dist
3+
dumps
4+
**/__pycache__/

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ ehthumbs_vista.db
171171

172172
# Dump file
173173
*.stackdump
174+
dumps/
174175

175176
# Folder config file
176177
[Dd]esktop.ini

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,30 @@
11
# Changelog
22

3+
## v1.1.0 (2021-04-07)
4+
5+
### Added
6+
7+
- Now supports import from NetBox versions up to 2.10.8
8+
- Now compatible with Nautobot 1.0.0b3
9+
10+
### Changed
11+
12+
- #28 - Rework of internal data representations to use primary keys instead of natural keys for most models.
13+
This should fix many "duplicate object" problems reported by earlier versions of this plugin (#11, #19, #25, #26, #27)
14+
15+
### Fixed
16+
17+
- #10 - Catch `ObjectDoesNotExist` exceptions instead of erroring out
18+
- #12 - Duplicate object reports should include primary key
19+
- #13 - Allow import of objects with custom field data referencing custom fields that no longer exist
20+
- #14 - Allow import of objects with old custom field data not matching latest requirements
21+
- #24 - Allow import of EUI MACAddress records
22+
23+
### Removed
24+
25+
- No longer compatible with Nautobot 1.0.0b2 and earlier
26+
27+
328
## v1.0.1 (2021-03-09)
429

530
### Added

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ The plugin is available as a Python package in PyPI and can be installed with pi
1010
pip install nautobot-netbox-importer
1111
```
1212

13-
> The plugin is compatible with Nautobot 1.0 and can handle JSON data exported from NetBox 2.10.3 through 2.10.5 at present.
13+
> The plugin is compatible with Nautobot 1.0.0b3 and later and can handle JSON data exported from NetBox 2.10.x at present.
1414
1515
Once installed, the plugin needs to be enabled in your `nautobot_config.py`:
1616

@@ -22,7 +22,7 @@ PLUGINS = ["nautobot_netbox_importer"]
2222

2323
### Getting a data export from NetBox
2424

25-
From the NetBox root directory, run the following command:
25+
From the NetBox root directory, run the following command to produce a JSON file (here, `/tmp/netbox_data.json`) describing the contents of your NetBox database:
2626

2727
```shell
2828
python netbox/manage.py dumpdata \
@@ -34,7 +34,7 @@ python netbox/manage.py dumpdata \
3434

3535
### Importing the data into Nautobot
3636

37-
From the Nautobot root directory, run `nautobot-server import_netbox_json <json_file> <netbox_version>`, for example `nautobot-server import_netbox_json /tmp/netbox_data.json 2.10.3`.
37+
From within the Nautobot application environment, run `nautobot-server import_netbox_json <json_file> <netbox_version>`, for example `nautobot-server import_netbox_json /tmp/netbox_data.json 2.10.3`.
3838

3939
## Contributing
4040

development/nautobot_config.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,22 @@
7878
"SSL": False,
7979
"DEFAULT_TIMEOUT": 300,
8080
},
81+
"custom_fields": {
82+
"HOST": os.environ.get("REDIS_HOST", "localhost"),
83+
"PORT": os.environ.get("REDIS_PORT", 6379),
84+
"DB": 0,
85+
"PASSWORD": os.environ.get("REDIS_PASSWORD", ""),
86+
"SSL": False,
87+
"DEFAULT_TIMEOUT": 300,
88+
},
89+
"webhooks": {
90+
"HOST": os.environ.get("REDIS_HOST", "localhost"),
91+
"PORT": os.environ.get("REDIS_PORT", 6379),
92+
"DB": 0,
93+
"PASSWORD": os.environ.get("REDIS_PASSWORD", ""),
94+
"SSL": False,
95+
"DEFAULT_TIMEOUT": 300,
96+
},
8197
}
8298

8399
# This key is used for secure generation of random numbers and strings. It must never be exposed outside of this file.

nautobot_netbox_importer/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ class NautobotNetboxImporterConfig(PluginConfig):
2222
description = "Data importer from NetBox 2.10.x to Nautobot."
2323
base_url = "netbox-importer"
2424
required_settings = []
25+
min_version = "1.0.0b3"
2526
max_version = "1.9999"
2627
default_settings = {}
2728
caching_config = {}

nautobot_netbox_importer/diffsync/adapters/__init__.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
version.parse("2.10.3"): NetBox210DiffSync,
1010
version.parse("2.10.4"): NetBox210DiffSync,
1111
version.parse("2.10.5"): NetBox210DiffSync,
12+
version.parse("2.10.6"): NetBox210DiffSync,
13+
version.parse("2.10.7"): NetBox210DiffSync,
14+
version.parse("2.10.8"): NetBox210DiffSync,
1215
}
1316

1417
__all__ = (

nautobot_netbox_importer/diffsync/adapters/abstract.py

Lines changed: 24 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@
55
from uuid import UUID
66

77
from diffsync import Diff, DiffSync, DiffSyncFlags, DiffSyncModel
8-
from diffsync.exceptions import ObjectAlreadyExists
8+
from diffsync.exceptions import ObjectAlreadyExists, ObjectNotFound
99
from pydantic.error_wrappers import ValidationError
1010
import structlog
1111

1212
import nautobot_netbox_importer.diffsync.models as n2nmodels
13+
from nautobot_netbox_importer.diffsync.models.validation import netbox_pk_to_nautobot_pk
1314

1415

1516
class N2NDiffSync(DiffSync):
@@ -31,7 +32,6 @@ class N2NDiffSync(DiffSync):
3132
permission = n2nmodels.Permission
3233
token = n2nmodels.Token
3334
user = n2nmodels.User
34-
userconfig = n2nmodels.UserConfig
3535

3636
# Circuits
3737
circuit = n2nmodels.Circuit
@@ -76,6 +76,7 @@ class N2NDiffSync(DiffSync):
7676
# Extras
7777
configcontext = n2nmodels.ConfigContext
7878
customfield = n2nmodels.CustomField
79+
customfieldchoice = n2nmodels.CustomFieldChoice
7980
customlink = n2nmodels.CustomLink
8081
exporttemplate = n2nmodels.ExportTemplate
8182
jobresult = n2nmodels.JobResult
@@ -121,17 +122,18 @@ class N2NDiffSync(DiffSync):
121122
# The specific order of models below is constructed empirically, but basically attempts to place all models
122123
# in sequence so that if model A has a hard dependency on a reference to model B, model B gets processed first.
123124
#
125+
# Note: with the latest changes in design for this plugin (using deterministic UUIDs in Nautobot to allow
126+
# direct mapping of NetBox PKs to Nautobot PKs), this order is now far less critical than it was previously.
127+
#
124128

125129
top_level = (
126130
# "contenttype", Not synced, as these are hard-coded in NetBox/Nautobot
127-
"customfield",
128-
"permission",
131+
# "permission", Not synced, as these are superseded by "objectpermission"
129132
"group",
130-
"user",
133+
"user", # Includes NetBox "userconfig" model as well
131134
"objectpermission",
132135
"token",
133-
"userconfig",
134-
# "status", Not synced, as these are hard-coded in NetBox/Nautobot
136+
# "status", Not synced, as these are hard-coded in NetBox and autogenerated in Nautobot
135137
# Need Tenant and TenantGroup before we can populate Sites
136138
"tenantgroup",
137139
"tenant", # Not all Tenants belong to a TenantGroup
@@ -194,7 +196,6 @@ class N2NDiffSync(DiffSync):
194196
# Interface/VMInterface -> Device/VirtualMachine (device)
195197
# Interface comes after Device because it MUST have a Device to be created;
196198
# IPAddress comes after Interface because we use the assigned_object as part of the IP's unique ID.
197-
# We will fixup the Device->primary_ip reference in fixup_data_relations()
198199
"ipaddress",
199200
"cable",
200201
"service",
@@ -207,6 +208,10 @@ class N2NDiffSync(DiffSync):
207208
"webhook",
208209
"taggeditem",
209210
"jobresult",
211+
# Imported last so that any "required=True" CustomFields do not cause Nautobot to reject
212+
# NetBox records that predate the creation of those CustomFields
213+
"customfield",
214+
"customfieldchoice",
210215
)
211216

212217
def __init__(self, *args, **kwargs):
@@ -229,41 +234,10 @@ def add(self, obj: DiffSyncModel):
229234
self._data_by_pk[modelname][obj.pk] = obj
230235
super().add(obj)
231236

232-
def fixup_data_relations(self):
233-
"""Iterate once more over all models and fix up any leftover FK relations."""
234-
for name in self.top_level:
235-
instances = self.get_all(name)
236-
if not instances:
237-
self.logger.info("No instances to review", model=name)
238-
else:
239-
self.logger.info(f"Reviewing all {len(instances)} instances", model=name)
240-
for diffsync_instance in instances:
241-
for fk_field, target_name in diffsync_instance.fk_associations().items():
242-
value = getattr(diffsync_instance, fk_field)
243-
if not value:
244-
continue
245-
if "*" in target_name:
246-
target_content_type_field = target_name[1:]
247-
target_content_type = getattr(diffsync_instance, target_content_type_field)
248-
target_name = target_content_type["model"]
249-
target_class = getattr(self, target_name)
250-
if "pk" in value:
251-
new_value = self.get_fk_identifiers(diffsync_instance, target_class, value["pk"])
252-
if isinstance(new_value, (UUID, int)):
253-
self.logger.error(
254-
"Still unable to resolve reference?",
255-
source=diffsync_instance,
256-
target=target_name,
257-
pk=new_value,
258-
)
259-
else:
260-
self.logger.debug(
261-
"Replacing forward reference with identifiers", pk=value["pk"], identifiers=new_value
262-
)
263-
setattr(diffsync_instance, fk_field, new_value)
264-
265237
def get_fk_identifiers(self, source_object, target_class, pk):
266238
"""Helper to load_record: given a class and a PK, get the identifiers of the given instance."""
239+
if isinstance(pk, int):
240+
pk = netbox_pk_to_nautobot_pk(target_class.get_type(), pk)
267241
target_record = self.get_by_pk(target_class, pk)
268242
if not target_record:
269243
self.logger.debug(
@@ -281,6 +255,8 @@ def get_by_pk(self, obj, pk):
281255
modelname = obj
282256
else:
283257
modelname = obj.get_type()
258+
if pk not in self._data_by_pk[modelname]:
259+
raise ObjectNotFound(f"PK {pk} not found in stored {modelname} instances")
284260
return self._data_by_pk[modelname].get(pk)
285261

286262
def make_model(self, diffsync_model, data):
@@ -293,18 +269,21 @@ def make_model(self, diffsync_model, data):
293269
"This may be an issue with your source data or may reflect a bug in this plugin.",
294270
action="load",
295271
exception=str(exc),
296-
model=diffsync_model,
272+
model=diffsync_model.get_type(),
297273
model_data=data,
298274
)
299275
return None
300276
try:
301277
self.add(instance)
302278
except ObjectAlreadyExists:
279+
existing_instance = self.get(diffsync_model, instance.get_unique_id())
303280
self.logger.warning(
304-
"Apparent duplicate object encountered. "
281+
"Apparent duplicate object encountered? "
305282
"This may be an issue with your source data or may reflect a bug in this plugin.",
306-
model=instance,
307-
model_id=instance.get_unique_id(),
283+
duplicate_id=instance.get_identifiers(),
284+
model=diffsync_model.get_type(),
285+
pk_1=existing_instance.pk,
286+
pk_2=instance.pk,
308287
)
309288
return instance
310289

nautobot_netbox_importer/diffsync/adapters/nautobot.py

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import structlog
88

99
from .abstract import N2NDiffSync
10+
from ..models.abstract import NautobotBaseModel
1011

1112

1213
IGNORED_FIELD_CLASSES = (GenericRel, GenericForeignKey, models.ManyToManyRel, models.ManyToOneRel)
@@ -23,7 +24,7 @@ class NautobotDiffSync(N2NDiffSync):
2324

2425
logger = structlog.get_logger()
2526

26-
def load_model(self, diffsync_model, record):
27+
def load_model(self, diffsync_model, record): # pylint: disable=too-many-branches
2728
"""Instantiate the given DiffSync model class from the given Django record."""
2829
data = {}
2930

@@ -46,7 +47,8 @@ def load_model(self, diffsync_model, record):
4647

4748
# If we got here, the field is some sort of foreign-key reference(s).
4849
if not value:
49-
# It's a null reference though, so we don't need to do anything special with it.
50+
# It's a null or empty list reference though, so we don't need to do anything special with it.
51+
data[field.name] = value
5052
continue
5153

5254
# What's the name of the model that this is a reference to?
@@ -69,16 +71,24 @@ def load_model(self, diffsync_model, record):
6971
continue
7072

7173
if isinstance(value, list):
72-
# This field is a one-to-many or many-to-many field, a list of foreign key references.
73-
# For each foreign key, find the corresponding DiffSync record, and use its
74-
# natural keys (identifiers) in the data in place of the foreign key value.
75-
data[field.name] = [
76-
self.get_fk_identifiers(diffsync_model, target_class, foreign_record.pk) for foreign_record in value
77-
]
78-
elif isinstance(value, (UUID, int)):
79-
# Look up the DiffSync record corresponding to this foreign key,
80-
# and store its natural keys (identifiers) in the data in place of the foreign key value.
81-
data[field.name] = self.get_fk_identifiers(diffsync_model, target_class, value)
74+
# This field is a one-to-many or many-to-many field, a list of object references.
75+
if issubclass(target_class, NautobotBaseModel):
76+
# Replace each object reference with its appropriate primary key value
77+
data[field.name] = [foreign_record.pk for foreign_record in value]
78+
else:
79+
# Since the PKs of these built-in Django models may differ between NetBox and Nautobot,
80+
# e.g., ContentTypes, replace each reference with the natural key (not PK) of the referenced model.
81+
data[field.name] = [
82+
self.get_by_pk(target_name, foreign_record.pk).get_identifiers() for foreign_record in value
83+
]
84+
elif isinstance(value, UUID):
85+
# Standard Nautobot UUID foreign-key reference, no transformation needed.
86+
data[field.name] = value
87+
elif isinstance(value, int):
88+
# Reference to a built-in model by its integer primary key.
89+
# Since this may not be the same value between NetBox and Nautobot (e.g., ContentType references)
90+
# replace the PK with the natural keys of the referenced model.
91+
data[field.name] = self.get_by_pk(target_name, value).get_identifiers()
8292
else:
8393
self.logger.error(f"Invalid PK value {value}")
8494
data[field.name] = None
@@ -89,13 +99,10 @@ def load_model(self, diffsync_model, record):
8999
def load(self):
90100
"""Load all available and relevant data from Nautobot in the appropriate sequence."""
91101
self.logger.info("Loading data from Nautobot into DiffSync...")
92-
for modelname in ("contenttype", "status", *self.top_level):
102+
for modelname in ("contenttype", "permission", "status", *self.top_level):
93103
diffsync_model = getattr(self, modelname)
94104
self.logger.info(f"Loading all {modelname} records...")
95105
for instance in diffsync_model.nautobot_model().objects.all():
96106
self.load_model(diffsync_model, instance)
97107

98-
self.logger.info("Fixing up any previously unresolved object relations...")
99-
self.fixup_data_relations()
100-
101108
self.logger.info("Data loading from Nautobot complete.")

0 commit comments

Comments
 (0)