Skip to content

Commit 302c0fe

Browse files
committed
Cable startup script
1 parent 2c757af commit 302c0fe

File tree

3 files changed

+292
-2
lines changed

3 files changed

+292
-2
lines changed

initializers/cables.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
## Required parameters for termination X ('a' or 'b'):
2+
## termination_X_name - name of interface
3+
## termination_X_device - name of the device interface belongs to
4+
## termination_X_class - required if different than Interface which is the default
5+
## Supported termination classes: Interface, ConsolePort, ConsoleServerPort, FrontPort, RearPort
6+
##
7+
## If termination is a circuit then the required parameter is termination_x_circuit.
8+
## Required parameters for a circuit termination:
9+
## termination_x_circuit:
10+
## term_side - termination side of a circuit. Must be A or B
11+
## cid - circuit ID value
12+
## site OR provider_network - name of Site or ProviderNetwork respectively. If both provided, Site takes precedence
13+
##
14+
## Any other Cable parameters supported by Netbox are supported as the top level keys, e.g. 'type', 'status', etc.
15+
##
16+
## - termination_a_name: console
17+
## termination_a_device: spine
18+
## termination_a_class: ConsolePort
19+
## termination_b_name: tty9
20+
## termination_b_device: console-server
21+
## termination_b_class: ConsoleServerPort
22+
## type: cat6
23+
##
24+
# - termination_a_name: to-server02
25+
# termination_a_device: server01
26+
# termination_b_name: to-server01
27+
# termination_b_device: server02
28+
# status: planned
29+
# type: mmf
30+
31+
# - termination_a_name: eth0
32+
# termination_a_device: server02
33+
# termination_b_circuit:
34+
# term_side: A
35+
# cid: Circuit_ID-1
36+
# site: AMS 1
37+
# type: cat6
38+
39+
# - termination_a_name: psu0
40+
# termination_a_device: server04
41+
# termination_a_class: PowerPort
42+
# termination_b_feed:
43+
# name: power feed 1
44+
# power_panel:
45+
# name: power panel AMS 1
46+
# site: AMS 1
47+
48+
# - termination_a_name: outlet1
49+
# termination_a_device: server04
50+
# termination_a_class: PowerOutlet
51+
# termination_b_name: psu1
52+
# termination_b_device: server04
53+
# termination_b_class: PowerPort

initializers/dcim_interfaces.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,13 @@
1010

1111
# - device: server01
1212
# enabled: true
13-
# type: virtual
13+
# type: 1000base-x-sfp
1414
# name: to-server02
1515
# - device: server02
1616
# enabled: true
17-
# type: virtual
17+
# type: 1000base-x-sfp
1818
# name: to-server01
19+
# - device: server02
20+
# enabled: true
21+
# type: 1000base-t
22+
# name: eth0

startup_scripts/460_cables.py

Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import sys
2+
from typing import Tuple
3+
4+
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
5+
from dcim.models import (
6+
Cable,
7+
ConsolePort,
8+
ConsoleServerPort,
9+
FrontPort,
10+
Interface,
11+
PowerFeed,
12+
PowerOutlet,
13+
PowerPanel,
14+
PowerPort,
15+
RearPort,
16+
Site,
17+
)
18+
from django.contrib.contenttypes.models import ContentType
19+
from django.db.models import Q
20+
from startup_script_utils import load_yaml
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(Interface)
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 = {"name": feed_params["power_panel"]["name"], "site__name": feed_params["power_panel"]["site"]}
47+
power_panel = PowerPanel.objects.get(**q)
48+
termination = PowerFeed.objects.get(name=feed_params["name"], power_panel=power_panel)
49+
return termination
50+
elif circuit_params:
51+
circuit = Circuit.objects.get(cid=circuit_params.pop("cid"))
52+
term_side = circuit_params.pop("term_side").upper()
53+
54+
site_name = circuit_params.pop("site", None)
55+
provider_network = circuit_params.pop("provider_network", None)
56+
57+
if site_name:
58+
circuit_params["site"] = Site.objects.get(name=site_name)
59+
elif provider_network:
60+
circuit_params["provider_network"] = ProviderNetwork.objects.get(name=provider_network)
61+
else:
62+
raise ValueError(
63+
f"⚠️ Missing one of required parameters: 'site' or 'provider_network' "
64+
f"for side {term_side} of circuit {circuit}"
65+
)
66+
67+
termination, created = CircuitTermination.objects.get_or_create(
68+
circuit=circuit, term_side=term_side, defaults=circuit_params
69+
)
70+
if created:
71+
print(f"⚡ Created new CircuitTermination {termination}")
72+
73+
return termination
74+
75+
raise ValueError(
76+
f"⚠️ Missing parameters for termination_{side}. "
77+
"Need termination_{side}_name AND termination_{side}_device OR termination_{side}_circuit"
78+
)
79+
80+
81+
def get_termination_class(port_class: str):
82+
if not port_class:
83+
return Interface
84+
85+
klass = globals()[port_class]
86+
if klass not in [
87+
Interface,
88+
FrontPort,
89+
RearPort,
90+
CircuitTermination,
91+
ConsolePort,
92+
ConsoleServerPort,
93+
PowerPort,
94+
PowerOutlet,
95+
PowerFeed,
96+
]:
97+
raise Exception(f"⚠️ Requested {port_class} is not supported as a cable termination!")
98+
99+
return klass
100+
101+
102+
def cable_in_cables(term_a: tuple, term_b: tuple) -> bool:
103+
"""Check if cable exist for given terminations.
104+
Each tuple should consist termination object and termination type
105+
"""
106+
107+
cable = Cable.objects.filter(
108+
Q(
109+
termination_a_id=term_a[0].id,
110+
termination_a_type=term_a[1],
111+
termination_b_id=term_b[0].id,
112+
termination_b_type=term_b[1],
113+
)
114+
| Q(
115+
termination_a_id=term_b[0].id,
116+
termination_a_type=term_b[1],
117+
termination_b_id=term_a[0].id,
118+
termination_b_type=term_a[1],
119+
)
120+
)
121+
return cable.exists()
122+
123+
124+
def check_termination_types(type_a, type_b) -> Tuple[bool, str]:
125+
if type_a in POWER_TERMINATIONS and type_b in POWER_TERMINATIONS:
126+
if type_a == type_b:
127+
return False, "Can't connect the same power terminations together"
128+
elif (
129+
type_a == POWER_OUTLET_TERMINATION
130+
and type_b == POWER_FEED_TERMINATION
131+
or type_a == POWER_FEED_TERMINATION
132+
and type_b == POWER_OUTLET_TERMINATION
133+
):
134+
return False, "PowerOutlet can't be connected with PowerFeed"
135+
elif type_a in POWER_TERMINATIONS or type_b in POWER_TERMINATIONS:
136+
return False, "Can't mix power terminations with port terminations"
137+
elif type_a in FRONT_AND_REAR or type_b in FRONT_AND_REAR:
138+
return True, ""
139+
elif (
140+
type_a == CONSOLE_PORT_TERMINATION
141+
and type_b != CONSOLE_SERVER_PORT_TERMINATION
142+
or type_b == CONSOLE_PORT_TERMINATION
143+
and type_a != CONSOLE_SERVER_PORT_TERMINATION
144+
):
145+
return False, "ConsolePorts can only be connected to ConsoleServerPorts or Front/Rear ports"
146+
return True, ""
147+
148+
149+
def get_cable_name(termination_a: tuple, termination_b: tuple) -> str:
150+
"""Returns name of a cable in format:
151+
device_a interface_a <---> interface_b device_b
152+
or for circuits:
153+
circuit_a termination_a <---> termination_b circuit_b
154+
"""
155+
cable_name = []
156+
157+
for is_side_b, termination in enumerate([termination_a, termination_b]):
158+
try:
159+
power_panel_id = getattr(termination[0], "power_panel_id", None)
160+
if power_panel_id:
161+
power_feed = PowerPanel.objects.get(id=power_panel_id)
162+
segment = [f"{power_feed}", f"{termination[0]}"]
163+
else:
164+
segment = [f"{termination[0].device}", f"{termination[0]}"]
165+
except AttributeError:
166+
segment = [f"{termination[0].circuit.cid}", f"{termination[0]}"]
167+
168+
if is_side_b:
169+
segment.reverse()
170+
171+
cable_name.append(" ".join(segment))
172+
173+
return " <---> ".join(cable_name)
174+
175+
176+
def check_interface_types(*args):
177+
for termination in args:
178+
try:
179+
if termination.type in VIRTUAL_INTERFACES:
180+
raise Exception(
181+
f"⚠️ Virtual interfaces are not supported for cabling. "
182+
f"Termination {termination.device} {termination} {termination.type}"
183+
)
184+
except AttributeError:
185+
# CircuitTermination dosn't have a type field
186+
pass
187+
188+
def check_terminations_are_free(*args):
189+
any_failed = False
190+
for termination in args:
191+
if termination.cable_id:
192+
any_failed = True
193+
print(f"⚠️ Termination {termination} is already occupied with cable #{termination.cable_id}")
194+
if any_failed:
195+
raise Exception(f"⚠️ At least one end of the cable is already occupied.")
196+
197+
cables = load_yaml("/opt/netbox/initializers/cables.yml")
198+
199+
if cables is None:
200+
sys.exit()
201+
202+
for params in cables:
203+
params["termination_a_class"] = get_termination_class(params.get("termination_a_class"))
204+
params["termination_b_class"] = get_termination_class(params.get("termination_b_class"))
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}")

0 commit comments

Comments
 (0)