Skip to content

Commit f13a657

Browse files
authored
Merge pull request #736 from kr3ator/feature/cable_initializers
Startup script for cables
2 parents 4c21344 + d482e62 commit f13a657

File tree

5 files changed

+314
-8
lines changed

5 files changed

+314
-8
lines changed

initializers/cables.yml

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

initializers/dcim_interfaces.yml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,17 @@
1919
# parent: ath0
2020
# - device: server01
2121
# enabled: true
22-
# type: virtual
22+
# type: 1000base-x-sfp
2323
# name: to-server02
2424
# - device: server02
2525
# enabled: true
26-
# type: virtual
26+
# type: 1000base-x-sfp
2727
# name: to-server01
28+
# - device: server02
29+
# enabled: true
30+
# type: 1000base-t
31+
# name: eth0
32+
# - device: server02
33+
# enabled: true
34+
# type: virtual
35+
# name: loopback

initializers/device_types.yml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,7 @@
3131
# - name_template: ttyS[1-48]
3232
# type: rj-45
3333
# power_ports:
34-
# - name: psu0 # both non-template and template field specified; non-template field takes precedence
35-
# name_template: psu[0,1]
34+
# - name_template: psu[0,1]
3635
# type: iec-60320-c14
3736
# maximum_draw: 35
3837
# allocated_draw: 35
@@ -46,7 +45,9 @@
4645
# type: 8p8c
4746
# positions_template: "[3,2]"
4847
# device_bays:
49-
# - name_template: bay[0-9]
48+
# - name: bay0 # both non-template and template field specified; non-template field takes precedence
49+
# name_template: bay[0-9]
50+
# label: test0
5051
# label_template: test[0-5,9,6-8]
5152
# description: Test description
5253
# power_outlets:

startup_scripts/190_device_types.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,13 @@ def expand_templates(params: List[dict], device_type: DeviceType) -> List[dict]:
3636

3737
if field in param:
3838
has_plain_fields = True
39-
expanded.append(param)
4039
elif template_value:
4140
expanded_fields[field] = list(expand_alphanumeric_pattern(template_value))
4241

4342
if expanded_fields and has_plain_fields:
4443
raise ValueError(f"Mix of plain and template keys provided for {templateable_fields}")
45-
46-
if not expanded_fields:
44+
elif not expanded_fields:
45+
expanded.append(param)
4746
continue
4847

4948
elements = list(expanded_fields.values())

startup_scripts/460_cables.py

Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
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(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+
cables = load_yaml("/opt/netbox/initializers/cables.yml")
192+
193+
if cables is None:
194+
sys.exit()
195+
196+
for params in cables:
197+
params["termination_a_class"] = get_termination_class_by_name(params.get("termination_a_class"))
198+
params["termination_b_class"] = get_termination_class_by_name(params.get("termination_b_class"))
199+
200+
term_a = get_termination_object(params, side="a")
201+
term_b = get_termination_object(params, side="b")
202+
203+
check_interface_types(term_a, term_b)
204+
205+
term_a_ct = ContentType.objects.get_for_model(term_a)
206+
term_b_ct = ContentType.objects.get_for_model(term_b)
207+
208+
types_ok, msg = check_termination_types(term_a_ct, term_b_ct)
209+
cable_name = get_cable_name((term_a, term_a_ct), (term_b, term_b_ct))
210+
211+
if not types_ok:
212+
print(f"⚠️ Invalid termination types for {cable_name}. {msg}")
213+
continue
214+
215+
if cable_in_cables((term_a, term_a_ct), (term_b, term_b_ct)):
216+
continue
217+
218+
check_terminations_are_free(term_a, term_b)
219+
220+
params["termination_a_id"] = term_a.id
221+
params["termination_b_id"] = term_b.id
222+
params["termination_a_type"] = term_a_ct
223+
params["termination_b_type"] = term_b_ct
224+
225+
cable = Cable.objects.create(**params)
226+
227+
print(f"🧷 Created cable {cable} {cable_name}")

0 commit comments

Comments
 (0)