Skip to content

Commit 27fbeec

Browse files
committed
Converted all initializers to plugin format
1 parent 6184b4d commit 27fbeec

File tree

127 files changed

+3663
-815
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

127 files changed

+3663
-815
lines changed

src/netbox_initializers/initializers/__init__.py

Lines changed: 52 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
"device_roles",
2828
"device_types",
2929
"devices",
30-
"dcim_interfaces",
30+
"interfaces",
3131
"platforms",
3232
"route_targets",
3333
"vrfs",
@@ -69,15 +69,18 @@ def load_data(self):
6969
# Must be implemented by specific subclass
7070
pass
7171

72-
def load_yaml(self):
73-
yf = Path(f"{self.data_file_path}/{self.data_file_name}")
72+
def load_yaml(self, data_file_name=None):
73+
if data_file_name:
74+
yf = Path(f"{self.data_file_path}/{data_file_name}")
75+
else:
76+
yf = Path(f"{self.data_file_path}/{self.data_file_name}")
7477
if not yf.is_file():
7578
return None
7679
with yf.open("r") as stream:
7780
yaml = YAML(typ="safe")
7881
return yaml.load(stream)
7982

80-
def pop_custom_fields(params):
83+
def pop_custom_fields(self, params):
8184
if "custom_field_data" in params:
8285
return params.pop("custom_field_data")
8386
elif "custom_fields" in params:
@@ -86,7 +89,7 @@ def pop_custom_fields(params):
8689

8790
return None
8891

89-
def set_custom_fields_values(entity, custom_field_data):
92+
def set_custom_fields_values(self, entity, custom_field_data):
9093
if not custom_field_data:
9194
return
9295

@@ -131,14 +134,58 @@ def split_params(self, params: dict, unique_params: list = None) -> Tuple[dict,
131134
return matching_params, params
132135

133136

137+
class InitializationError(Exception):
138+
pass
139+
140+
134141
def register_initializer(name: str, initializer: BaseInitializer):
135142
INITIALIZER_REGISTRY[name] = initializer
136143

137144

138145
# All initializers must be imported here, to be registered
146+
from .aggregates import AggregateInitializer
139147
from .asns import ASNInitializer
148+
from .cables import CableInitializer
149+
from .circuit_types import CircuitTypeInitializer
150+
from .circuits import CircuitInitializer
151+
from .cluster_groups import ClusterGroupInitializer
140152
from .cluster_types import ClusterTypesInitializer
153+
from .clusters import ClusterInitializer
154+
from .contact_groups import ContactGroupInitializer
155+
from .contact_roles import ContactRoleInitializer
156+
from .contacts import ContactInitializer
157+
from .custom_fields import CustomFieldInitializer
158+
from .custom_links import CustomLinkInitializer
159+
from .device_roles import DeviceRoleInitializer
160+
from .device_types import DeviceTypeInitializer
161+
from .devices import DeviceInitializer
141162
from .groups import GroupInitializer
163+
from .interfaces import InterfaceInitializer
164+
from .ip_addresses import IPAddressInitializer
165+
from .locations import LocationInitializer
166+
from .manufacturers import ManufacturerInitializer
167+
from .object_permissions import ObjectPermissionInitializer
168+
from .platforms import PlatformInitializer
169+
from .power_feeds import PowerFeedInitializer
170+
from .power_panels import PowerPanelInitializer
171+
from .prefix_vlan_roles import RoleInitializer
172+
from .prefixes import PrefixInitializer
173+
from .primary_ips import PrimaryIPInitializer
174+
from .providers import ProviderInitializer
175+
from .rack_roles import RackRoleInitializer
176+
from .racks import RackInitializer
177+
from .regions import RegionInitializer
142178
from .rirs import RIRInitializer
179+
from .route_targets import RouteTargetInitializer
143180
from .services import ServiceInitializer
181+
from .sites import SiteInitializer
182+
from .tags import TagInitializer
183+
from .tenant_groups import TenantGroupInitializer
184+
from .tenants import TenantInitializer
144185
from .users import UserInitializer
186+
from .virtual_machines import VirtualMachineInitializer
187+
from .virtualization_interfaces import VMInterfaceInitializer
188+
from .vlan_groups import VLANGroupInitializer
189+
from .vlans import VLANInitializer
190+
from .vrfs import VRFInitializer
191+
from .webhooks import WebhookInitializer
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from ipam.models import RIR, Aggregate
2+
from netaddr import IPNetwork
3+
from tenancy.models import Tenant
4+
5+
from . import BaseInitializer, register_initializer
6+
7+
MATCH_PARAMS = ["prefix", "rir"]
8+
REQUIRED_ASSOCS = {"rir": (RIR, "name")}
9+
OPTIONAL_ASSOCS = {
10+
"tenant": (Tenant, "name"),
11+
}
12+
13+
14+
class AggregateInitializer(BaseInitializer):
15+
data_file_name = "aggregates.yml"
16+
17+
def load_data(self):
18+
aggregates = self.load_yaml()
19+
if aggregates is None:
20+
return
21+
for params in aggregates:
22+
custom_field_data = self.pop_custom_fields(params)
23+
24+
params["prefix"] = IPNetwork(params["prefix"])
25+
26+
for assoc, details in REQUIRED_ASSOCS.items():
27+
model, field = details
28+
query = {field: params.pop(assoc)}
29+
30+
params[assoc] = model.objects.get(**query)
31+
32+
for assoc, details in OPTIONAL_ASSOCS.items():
33+
if assoc in params:
34+
model, field = details
35+
query = {field: params.pop(assoc)}
36+
37+
params[assoc] = model.objects.get(**query)
38+
39+
matching_params, defaults = self.split_params(params, MATCH_PARAMS)
40+
aggregate, created = Aggregate.objects.get_or_create(
41+
**matching_params, defaults=defaults
42+
)
43+
44+
if created:
45+
print("🗞️ Created Aggregate", aggregate.prefix)
46+
47+
self.set_custom_fields_values(aggregate, custom_field_data)
48+
49+
50+
register_initializer("aggregates", AggregateInitializer)
Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
from typing import Tuple
2+
3+
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
4+
from dcim.models import (
5+
Cable,
6+
ConsolePort,
7+
ConsoleServerPort,
8+
FrontPort,
9+
Interface,
10+
PowerFeed,
11+
PowerOutlet,
12+
PowerPanel,
13+
PowerPort,
14+
RearPort,
15+
Site,
16+
)
17+
from django.contrib.contenttypes.models import ContentType
18+
from django.db.models import Q
19+
20+
from . import BaseInitializer, register_initializer
21+
22+
CONSOLE_PORT_TERMINATION = ContentType.objects.get_for_model(ConsolePort)
23+
CONSOLE_SERVER_PORT_TERMINATION = ContentType.objects.get_for_model(ConsoleServerPort)
24+
FRONT_PORT_TERMINATION = ContentType.objects.get_for_model(FrontPort)
25+
REAR_PORT_TERMINATION = ContentType.objects.get_for_model(RearPort)
26+
FRONT_AND_REAR = [FRONT_PORT_TERMINATION, REAR_PORT_TERMINATION]
27+
POWER_PORT_TERMINATION = ContentType.objects.get_for_model(PowerPort)
28+
POWER_OUTLET_TERMINATION = ContentType.objects.get_for_model(PowerOutlet)
29+
POWER_FEED_TERMINATION = ContentType.objects.get_for_model(PowerFeed)
30+
POWER_TERMINATIONS = [POWER_PORT_TERMINATION, POWER_OUTLET_TERMINATION, POWER_FEED_TERMINATION]
31+
32+
VIRTUAL_INTERFACES = ["bridge", "lag", "virtual"]
33+
34+
35+
def get_termination_object(params: dict, side: str):
36+
klass = params.pop(f"termination_{side}_class")
37+
name = params.pop(f"termination_{side}_name", None)
38+
device = params.pop(f"termination_{side}_device", None)
39+
feed_params = params.pop(f"termination_{side}_feed", None)
40+
circuit_params = params.pop(f"termination_{side}_circuit", {})
41+
42+
if name and device:
43+
termination = klass.objects.get(name=name, device__name=device)
44+
return termination
45+
elif feed_params:
46+
q = {
47+
"name": feed_params["power_panel"]["name"],
48+
"site__name": feed_params["power_panel"]["site"],
49+
}
50+
power_panel = PowerPanel.objects.get(**q)
51+
termination = PowerFeed.objects.get(name=feed_params["name"], power_panel=power_panel)
52+
return termination
53+
elif circuit_params:
54+
circuit = Circuit.objects.get(cid=circuit_params.pop("cid"))
55+
term_side = circuit_params.pop("term_side").upper()
56+
57+
site_name = circuit_params.pop("site", None)
58+
provider_network = circuit_params.pop("provider_network", None)
59+
60+
if site_name:
61+
circuit_params["site"] = Site.objects.get(name=site_name)
62+
elif provider_network:
63+
circuit_params["provider_network"] = ProviderNetwork.objects.get(name=provider_network)
64+
else:
65+
raise ValueError(
66+
f"⚠️ Missing one of required parameters: 'site' or 'provider_network' "
67+
f"for side {term_side} of circuit {circuit}"
68+
)
69+
70+
termination, created = CircuitTermination.objects.get_or_create(
71+
circuit=circuit, term_side=term_side, defaults=circuit_params
72+
)
73+
if created:
74+
print(f"⚡ Created new CircuitTermination {termination}")
75+
76+
return termination
77+
78+
raise ValueError(
79+
f"⚠️ Missing parameters for termination_{side}. "
80+
"Need termination_{side}_name AND termination_{side}_device OR termination_{side}_circuit"
81+
)
82+
83+
84+
def get_termination_class_by_name(port_class: str):
85+
if not port_class:
86+
return Interface
87+
88+
return globals()[port_class]
89+
90+
91+
def cable_in_cables(term_a: tuple, term_b: tuple) -> bool:
92+
"""Check if cable exist for given terminations.
93+
Each tuple should consist termination object and termination type
94+
"""
95+
96+
cable = Cable.objects.filter(
97+
Q(
98+
termination_a_id=term_a[0].id,
99+
termination_a_type=term_a[1],
100+
termination_b_id=term_b[0].id,
101+
termination_b_type=term_b[1],
102+
)
103+
| Q(
104+
termination_a_id=term_b[0].id,
105+
termination_a_type=term_b[1],
106+
termination_b_id=term_a[0].id,
107+
termination_b_type=term_a[1],
108+
)
109+
)
110+
return cable.exists()
111+
112+
113+
def check_termination_types(type_a, type_b) -> Tuple[bool, str]:
114+
if type_a in POWER_TERMINATIONS and type_b in POWER_TERMINATIONS:
115+
if type_a == type_b:
116+
return False, "Can't connect the same power terminations together"
117+
elif (
118+
type_a == POWER_OUTLET_TERMINATION
119+
and type_b == POWER_FEED_TERMINATION
120+
or type_a == POWER_FEED_TERMINATION
121+
and type_b == POWER_OUTLET_TERMINATION
122+
):
123+
return False, "PowerOutlet can't be connected with PowerFeed"
124+
elif type_a in POWER_TERMINATIONS or type_b in POWER_TERMINATIONS:
125+
return False, "Can't mix power terminations with port terminations"
126+
elif type_a in FRONT_AND_REAR or type_b in FRONT_AND_REAR:
127+
return True, ""
128+
elif (
129+
type_a == CONSOLE_PORT_TERMINATION
130+
and type_b != CONSOLE_SERVER_PORT_TERMINATION
131+
or type_b == CONSOLE_PORT_TERMINATION
132+
and type_a != CONSOLE_SERVER_PORT_TERMINATION
133+
):
134+
return False, "ConsolePorts can only be connected to ConsoleServerPorts or Front/Rear ports"
135+
return True, ""
136+
137+
138+
def get_cable_name(termination_a: tuple, termination_b: tuple) -> str:
139+
"""Returns name of a cable in format:
140+
device_a interface_a <---> interface_b device_b
141+
or for circuits:
142+
circuit_a termination_a <---> termination_b circuit_b
143+
"""
144+
cable_name = []
145+
146+
for is_side_b, termination in enumerate([termination_a, termination_b]):
147+
try:
148+
power_panel_id = getattr(termination[0], "power_panel_id", None)
149+
if power_panel_id:
150+
power_feed = PowerPanel.objects.get(id=power_panel_id)
151+
segment = [f"{power_feed}", f"{termination[0]}"]
152+
else:
153+
segment = [f"{termination[0].device}", f"{termination[0]}"]
154+
except AttributeError:
155+
segment = [f"{termination[0].circuit.cid}", f"{termination[0]}"]
156+
157+
if is_side_b:
158+
segment.reverse()
159+
160+
cable_name.append(" ".join(segment))
161+
162+
return " <---> ".join(cable_name)
163+
164+
165+
def check_interface_types(*args):
166+
for termination in args:
167+
try:
168+
if termination.type in VIRTUAL_INTERFACES:
169+
raise Exception(
170+
f"⚠️ Virtual interfaces are not supported for cabling. "
171+
f"Termination {termination.device} {termination} {termination.type}"
172+
)
173+
except AttributeError:
174+
# CircuitTermination doesn't have a type field
175+
pass
176+
177+
178+
def check_terminations_are_free(*args):
179+
any_failed = False
180+
for termination in args:
181+
if termination.cable_id:
182+
any_failed = True
183+
print(
184+
f"⚠️ Termination {termination} is already occupied "
185+
f"with cable #{termination.cable_id}"
186+
)
187+
if any_failed:
188+
raise Exception("⚠️ At least one end of the cable is already occupied.")
189+
190+
191+
class CableInitializer(BaseInitializer):
192+
data_file_name = "cables.yml"
193+
194+
def load_data(self):
195+
cables = self.load_yaml()
196+
if cables is None:
197+
return
198+
for params in cables:
199+
params["termination_a_class"] = get_termination_class_by_name(
200+
params.get("termination_a_class")
201+
)
202+
params["termination_b_class"] = get_termination_class_by_name(
203+
params.get("termination_b_class")
204+
)
205+
206+
term_a = get_termination_object(params, side="a")
207+
term_b = get_termination_object(params, side="b")
208+
209+
check_interface_types(term_a, term_b)
210+
211+
term_a_ct = ContentType.objects.get_for_model(term_a)
212+
term_b_ct = ContentType.objects.get_for_model(term_b)
213+
214+
types_ok, msg = check_termination_types(term_a_ct, term_b_ct)
215+
cable_name = get_cable_name((term_a, term_a_ct), (term_b, term_b_ct))
216+
217+
if not types_ok:
218+
print(f"⚠️ Invalid termination types for {cable_name}. {msg}")
219+
continue
220+
221+
if cable_in_cables((term_a, term_a_ct), (term_b, term_b_ct)):
222+
continue
223+
224+
check_terminations_are_free(term_a, term_b)
225+
226+
params["termination_a_id"] = term_a.id
227+
params["termination_b_id"] = term_b.id
228+
params["termination_a_type"] = term_a_ct
229+
params["termination_b_type"] = term_b_ct
230+
231+
cable = Cable.objects.create(**params)
232+
233+
print(f"🧷 Created cable {cable} {cable_name}")
234+
235+
236+
register_initializer("cables", CableInitializer)

0 commit comments

Comments
 (0)