Skip to content

Commit ada4ce7

Browse files
committed
feat: Customizations, time zone, consoleserverports
1 parent 3faaca4 commit ada4ce7

28 files changed

+2081
-548
lines changed

docs/dev/generator.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ This is achieved through `adapter.configure_model(content_type: ContentTypeStr)`
4848
- `extend_content_type`: Define this when a source model extends another source model to merge into a single Nautobot model.
4949
- `forward_references`: Define to forward references to another content type. This is useful when the source data references a content type that is not directly related to the Nautobot content type. For example, a source data references a `dcim.location` content type, but the Nautobot content type is `dcim.locationtype`.
5050
- `disable_related_reference`: Define, to disable storing references to this content type to other content types. This is useful for e.g. `ObjectChange` model.
51-
- `pre_import`: Define a callable to be executed before importing the source data. Can be used to alter or cache the source data before importing. `pre_import(source: RecordData, importer_pass: ImporterPass) -> PreImportResult` is called twice for each source record: on first and second input data iteration.
51+
- `pre_import_record`: Define a callable to be executed before importing the source data. Can be used to alter or cache the source data before importing. `pre_import_record(source: RecordData, importer_pass: ImporterPass) -> PreImportResult` is called twice for each source record: on first and second input data iteration.
5252
- `fields`: Define the source fields and how they should be imported. This argument is a dictionary mapping `FieldName` to `SourceFieldDefinition` instances.
5353
- `SourceFieldDefinition` can be one of:
5454
- `None`: to ignore the field.

nautobot_netbox_importer/diffsync/adapters/netbox.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from gzip import GzipFile
44
from pathlib import Path
5-
from typing import Callable, Generator, NamedTuple, Union
5+
from typing import Callable, Generator, NamedTuple, Sequence, Union
66
from urllib.parse import ParseResult, urlparse
77

88
import ijson
@@ -15,6 +15,8 @@
1515
from nautobot_netbox_importer.generator import SourceAdapter, SourceDataGenerator, SourceRecord
1616
from nautobot_netbox_importer.summary import Pathable
1717

18+
# from nautobot_netbox_importer.diffsync.models.cables import fix_cables
19+
1820
for _name in (
1921
"base",
2022
"circuits",
@@ -52,6 +54,7 @@ class NetBoxImporterOptions(NamedTuple):
5254
save_json_summary_path: str = ""
5355
save_text_summary_path: str = ""
5456
trace_issues: bool = False
57+
customizations: Sequence[str] = []
5558

5659

5760
AdapterSetupFunction = Callable[[SourceAdapter], None]
@@ -75,6 +78,10 @@ def __init__(self, input_ref: _FileRef, options: NetBoxImporterOptions, job=None
7578

7679
self.options = options
7780

81+
for name in options.customizations:
82+
if name:
83+
register_generator_setup(name)
84+
7885
for name in GENERATOR_SETUP_MODULES:
7986
setup = __import__(name, fromlist=["setup"]).setup
8087
setup(self)
@@ -86,13 +93,16 @@ def load(self) -> None:
8693
fix_power_feed_locations(self)
8794
if self.options.unrack_zero_uheight_devices:
8895
unrack_zero_uheight_devices(self)
96+
97+
# fix_cables(self)
98+
8999
self.post_import()
90100

91101
def import_to_nautobot(self) -> None:
92102
"""Import a NetBox export file into Nautobot."""
93103
commited = False
94104
try:
95-
self._atomic_import()
105+
self._atomic_import() # type: ignore
96106
commited = True
97107
except _DryRunException:
98108
logger.warning("Dry-run mode, no data has been imported.")

nautobot_netbox_importer/diffsync/models/base.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,12 @@ def content_types_mapper_importer(source: RecordData, target: DiffSyncBaseModel)
7474

7575
def setup(adapter: SourceAdapter) -> None:
7676
"""Map NetBox base models to Nautobot."""
77-
adapter.disable_model("sessions.session", "Nautobot has own sessions, sessions should never cross apps.")
7877
adapter.disable_model("admin.logentry", "Not directly used in Nautobot.")
79-
adapter.disable_model("users.userconfig", "May not have a 1 to 1 translation to Nautobot.")
8078
adapter.disable_model("auth.permission", "Handled via a Nautobot model and may not be a 1 to 1.")
79+
adapter.disable_model("dcim.cablepath", "Recreated in Nautobot on signal when circuit termination is created")
80+
adapter.disable_model("extras.imageattachment", "Images are not imported yet.")
81+
adapter.disable_model("sessions.session", "Nautobot has own sessions, sessions should never cross apps.")
82+
adapter.disable_model("users.userconfig", "May not have a 1 to 1 translation to Nautobot.")
8183

8284
_setup_content_types(adapter)
8385

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
# COMPATIBLE_TERMINATION_TYPES = {
2+
# "circuittermination": ["interface", "frontport", "rearport", "circuittermination"],
3+
# "consoleport": ["consoleserverport", "frontport", "rearport"],
4+
# "consoleserverport": ["consoleport", "frontport", "rearport"],
5+
# "interface": ["interface", "circuittermination", "frontport", "rearport"],
6+
# "frontport": [
7+
# "consoleport",
8+
# "consoleserverport",
9+
# "interface",
10+
# "frontport",
11+
# "rearport",
12+
# "circuittermination",
13+
# ],
14+
# "powerfeed": ["powerport"],
15+
# "poweroutlet": ["powerport"],
16+
# "powerport": ["poweroutlet", "powerfeed"],
17+
# "rearport": [
18+
# "consoleport",
19+
# "consoleserverport",
20+
# "interface",
21+
# "frontport",
22+
# "rearport",
23+
# "circuittermination",
24+
# ],
25+
# }
26+
"""DCIM data related functions."""
27+
28+
from nautobot.dcim.constants import COMPATIBLE_TERMINATION_TYPES
29+
30+
from nautobot_netbox_importer.base import ContentTypeStr, Uid
31+
from nautobot_netbox_importer.generator.source import DiffSyncModel, SourceAdapter, SourceModelWrapper
32+
33+
# TBD: Move to base.py
34+
DUMMY_UID = "dummy"
35+
36+
_FALLBACK_TERMINATION_TYPE = "circuittermination"
37+
_CIRCUIT_MODELS = {"circuittermination"}
38+
39+
40+
def _get_first_compatible_termination_type(stripped_type: str) -> ContentTypeStr:
41+
"""Determine the first compatible termination type for a given termination.
42+
43+
This function identifies the first compatible termination type based on the
44+
given termination string, falling back to '_FALLBACK_TERMINATION_TYPE' if
45+
no compatibility is found.
46+
47+
Args:
48+
stripped_type (str): The termination type with 'dcim.' prefix removed
49+
50+
Returns:
51+
str: The compatible termination type with 'dcim.' prefix
52+
53+
Examples:
54+
>>> _get_first_compatible_termination_type("interface")
55+
'dcim.interface'
56+
57+
>>> _get_first_compatible_termination_type("poweroutlet")
58+
'dcim.powerport'
59+
60+
>>> _get_first_compatible_termination_type("unknown")
61+
'circuits.circuittermination'
62+
"""
63+
64+
def get_type(model_name: str) -> ContentTypeStr:
65+
return f"circuits.{model_name}" if model_name in _CIRCUIT_MODELS else f"dcim.{model_name}"
66+
67+
if stripped_type not in COMPATIBLE_TERMINATION_TYPES:
68+
return get_type(_FALLBACK_TERMINATION_TYPE)
69+
70+
types = COMPATIBLE_TERMINATION_TYPES[stripped_type]
71+
if _FALLBACK_TERMINATION_TYPE in types:
72+
return get_type(_FALLBACK_TERMINATION_TYPE)
73+
74+
return get_type(types[0])
75+
76+
77+
def _get_termination(uid: Uid, type_: ContentTypeStr, other_type: ContentTypeStr) -> tuple[Uid, ContentTypeStr]:
78+
"""Determine the appropriate termination for a cable side.
79+
80+
This function evaluates cable termination data and returns correct termination information
81+
based on compatibility rules.
82+
83+
Args:
84+
uid (Uid): UID of the current termination
85+
type_ (ContentTypeStr): Type of the current termination
86+
other_type (ContentTypeStr): Type of the opposite termination
87+
88+
Returns:
89+
tuple[str, str]: A tuple containing (termination_id, termination_type)
90+
91+
Examples:
92+
>>> _get_termination("123", "dcim.interface", "dcim.frontport")
93+
('123', 'dcim.interface')
94+
95+
>>> _get_termination("", "dcim.interface", "dcim.poweroutlet")
96+
('dummy', 'circuit.circuittermination')
97+
98+
>>> _get_termination("123", "", "dcim.frontport")
99+
('123', 'dcim.interface')
100+
101+
>>> _get_termination("123", "dcim.interface", "")
102+
('123', 'dcim.interface')
103+
104+
>>> _get_termination("", "", "")
105+
('dummy', 'circuit.circuittermination')
106+
107+
>>> _get_termination("456", "dcim.powerport", "dcim.poweroutlet")
108+
('456', 'dcim.powerport')
109+
"""
110+
type_stripped = type_.split(".")[1] if type_ else ""
111+
other_stripped = other_type.split(".")[1] if other_type else ""
112+
first_compatible = _get_first_compatible_termination_type(other_stripped)
113+
114+
if not type_:
115+
uid = DUMMY_UID
116+
type_ = first_compatible
117+
118+
if not uid:
119+
uid = DUMMY_UID
120+
121+
if not other_type:
122+
return uid, type_
123+
124+
if type_stripped in COMPATIBLE_TERMINATION_TYPES and other_stripped in COMPATIBLE_TERMINATION_TYPES.get(
125+
type_stripped, []
126+
):
127+
return uid, type_
128+
129+
return DUMMY_UID, first_compatible
130+
131+
132+
def _update_cable_termination(wrapper: SourceModelWrapper, cable: DiffSyncModel, side: str) -> None:
133+
"""Update cable termination information for a specific side.
134+
135+
This function retrieves termination data for the specified side of a cable, determines
136+
the appropriate termination using _get_termination(), and updates the cable if needed.
137+
138+
Args:
139+
wrapper (SourceModelWrapper): Model wrapper containing field definitions
140+
cable (DiffSyncModel): The cable model to update
141+
side (str): Which side of the cable to update ('a' or 'b')
142+
"""
143+
uid = getattr(cable, f"termination_{side}_id", "")
144+
type_ = getattr(cable, f"termination_{side}_type", "")
145+
other_type = getattr(cable, f"termination_{'b' if side == 'a' else 'a'}_type", "")
146+
147+
result_uid, result_type = _get_termination(uid, type_, other_type)
148+
149+
if result_uid == uid and result_type == type_:
150+
return
151+
152+
adapter = wrapper.adapter
153+
154+
field = wrapper.fields[f"termination_{side}_type"]
155+
field.set_nautobot_value(cable, adapter.get_nautobot_content_type_uid(result_type))
156+
157+
field.add_issue(
158+
"UpdatedCableTermination",
159+
f"Cable termination {side.upper()} updated from {uid}, {type_} to {result_uid}, {result_type}",
160+
cable,
161+
)
162+
163+
if result_uid != DUMMY_UID:
164+
return
165+
166+
type_wrapper = adapter.get_or_create_wrapper(result_type)
167+
pk = type_wrapper.get_pk_from_uid(result_uid)
168+
wrapper.add_reference(type_wrapper, pk)
169+
170+
field = wrapper.fields[f"termination_{side}_id"]
171+
field.set_nautobot_value(cable, pk)
172+
173+
# TBD: Cached dummy records needs to be improved to contain required values
174+
type_wrapper.cache_record({"id": DUMMY_UID})
175+
176+
177+
def fix_cables(adapter: SourceAdapter) -> None:
178+
"""Fix cables by ensuring proper terminations.
179+
180+
This function processes all cables from the source adapter and validates/fixes
181+
termination information for both sides of each cable.
182+
183+
Args:
184+
adapter (SourceAdapter): The source adapter containing cable data
185+
"""
186+
wrapper = adapter.get_or_create_wrapper("dcim.cable")
187+
188+
for cable in adapter.get_all(wrapper.nautobot.diffsync_class):
189+
_update_cable_termination(wrapper, cable, "a")
190+
_update_cable_termination(wrapper, cable, "b")

nautobot_netbox_importer/diffsync/models/custom_fields.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
EMPTY_VALUES,
99
DiffSyncBaseModel,
1010
ImporterPass,
11-
PreImportResult,
11+
PreImportRecordResult,
1212
SourceAdapter,
1313
SourceField,
1414
fields,
@@ -95,14 +95,14 @@ def setup(adapter: SourceAdapter) -> None:
9595
"""Map NetBox custom fields to Nautobot."""
9696
choice_sets = {}
9797

98-
def create_choice_set(source: RecordData, importer_pass: ImporterPass) -> PreImportResult:
98+
def create_choice_set(source: RecordData, importer_pass: ImporterPass) -> PreImportRecordResult:
9999
if importer_pass == ImporterPass.DEFINE_STRUCTURE:
100100
choice_sets[source.get("id")] = [
101101
*_convert_choices(source.get("base_choices")),
102102
*_convert_choices(source.get("extra_choices")),
103103
]
104104

105-
return PreImportResult.USE_RECORD
105+
return PreImportRecordResult.USE_RECORD
106106

107107
def define_choice_set(field: SourceField) -> None:
108108
def choices_importer(source: RecordData, target: DiffSyncBaseModel) -> None:
@@ -144,7 +144,7 @@ def create_choices(choices: list, custom_field_uid: Uid) -> None:
144144
# Defined in NetBox but not in Nautobot
145145
adapter.configure_model(
146146
"extras.CustomFieldChoiceSet",
147-
pre_import=create_choice_set,
147+
pre_import_record=create_choice_set,
148148
)
149149

150150
adapter.configure_model(

nautobot_netbox_importer/diffsync/models/dcim.py

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@
44
from uuid import UUID
55

66
from nautobot_netbox_importer.base import RecordData
7-
from nautobot_netbox_importer.generator import DiffSyncBaseModel, PreImportResult, SourceAdapter, SourceField, fields
7+
from nautobot_netbox_importer.generator import (
8+
DiffSyncBaseModel,
9+
PreImportRecordResult,
10+
SourceAdapter,
11+
SourceField,
12+
fields,
13+
)
814

915
from .locations import define_location
1016

@@ -39,13 +45,13 @@ def units_importer(source: RecordData, target: DiffSyncBaseModel) -> None:
3945
field.set_importer(units_importer)
4046

4147

42-
def _pre_import_cable_termination(source: RecordData, _) -> PreImportResult:
48+
def _pre_import_cable_termination(source: RecordData, _) -> PreImportRecordResult:
4349
cable_end = source.pop("cable_end").lower()
4450
source["id"] = source.pop("cable")
4551
source[f"termination_{cable_end}_type"] = source.pop("termination_type")
4652
source[f"termination_{cable_end}_id"] = source.pop("termination_id")
4753

48-
return PreImportResult.USE_RECORD
54+
return PreImportRecordResult.USE_RECORD
4955

5056

5157
def setup(adapter: SourceAdapter) -> None:
@@ -65,11 +71,19 @@ def setup(adapter: SourceAdapter) -> None:
6571
"role": fields.role(adapter, "dcim.rackrole"),
6672
},
6773
)
68-
adapter.configure_model("dcim.cable")
74+
adapter.configure_model(
75+
"dcim.cable",
76+
fields={
77+
"termination_a_type": "termination_a_type",
78+
"termination_a_id": "termination_a_id",
79+
"termination_b_type": "termination_b_type",
80+
"termination_b_id": "termination_b_id",
81+
},
82+
)
6983
adapter.configure_model(
7084
"dcim.cabletermination",
7185
extend_content_type="dcim.cable",
72-
pre_import=_pre_import_cable_termination,
86+
pre_import_record=_pre_import_cable_termination,
7387
)
7488
adapter.configure_model(
7589
"dcim.interface",

0 commit comments

Comments
 (0)