Merge pull request #736 from kr3ator/feature/cable_initializers
Startup script for cables
This commit is contained in:
commit
f13a6573a8
71
initializers/cables.yml
Normal file
71
initializers/cables.yml
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
# # Required parameters for termination X ('a' or 'b'):
|
||||||
|
# #
|
||||||
|
# # ```
|
||||||
|
# # termination_x_name -> name of interface
|
||||||
|
# # termination_x_device -> name of the device interface belongs to
|
||||||
|
# # termination_x_class -> required if different than 'Interface' which is the default
|
||||||
|
# # ```
|
||||||
|
# #
|
||||||
|
# # Supported termination classes: Interface, ConsolePort, ConsoleServerPort, FrontPort, RearPort, PowerPort, PowerOutlet
|
||||||
|
# #
|
||||||
|
# #
|
||||||
|
# # If a termination is a circuit then the required parameter is termination_x_circuit.
|
||||||
|
# # Required parameters for a circuit termination:
|
||||||
|
# #
|
||||||
|
# # ```
|
||||||
|
# # termination_x_circuit:
|
||||||
|
# # term_side -> termination side of a circuit. Must be A or B
|
||||||
|
# # cid -> circuit ID value
|
||||||
|
# # site OR provider_network -> name of Site or ProviderNetwork respectively. If both provided, Site takes precedence
|
||||||
|
# # ```
|
||||||
|
# #
|
||||||
|
# # If a termination is a power feed then the required parameter is termination_x_feed.
|
||||||
|
# #
|
||||||
|
# # ```
|
||||||
|
# # termination_x_feed:
|
||||||
|
# # name -> name of the PowerFeed object
|
||||||
|
# # power_panel:
|
||||||
|
# # name -> name of the PowerPanel the PowerFeed is attached to
|
||||||
|
# # site -> name of the Site in which the PowerPanel is present
|
||||||
|
# # ```
|
||||||
|
# #
|
||||||
|
# # Any other Cable parameters supported by Netbox are supported as the top level keys, e.g. 'type', 'status', etc.
|
||||||
|
# #
|
||||||
|
# # - termination_a_name: console
|
||||||
|
# # termination_a_device: spine
|
||||||
|
# # termination_a_class: ConsolePort
|
||||||
|
# # termination_b_name: tty9
|
||||||
|
# # termination_b_device: console-server
|
||||||
|
# # termination_b_class: ConsoleServerPort
|
||||||
|
# # type: cat6
|
||||||
|
# #
|
||||||
|
# - termination_a_name: to-server02
|
||||||
|
# termination_a_device: server01
|
||||||
|
# termination_b_name: to-server01
|
||||||
|
# termination_b_device: server02
|
||||||
|
# status: planned
|
||||||
|
# type: mmf
|
||||||
|
|
||||||
|
# - termination_a_name: eth0
|
||||||
|
# termination_a_device: server02
|
||||||
|
# termination_b_circuit:
|
||||||
|
# term_side: A
|
||||||
|
# cid: Circuit_ID-1
|
||||||
|
# site: AMS 1
|
||||||
|
# type: cat6
|
||||||
|
|
||||||
|
# - termination_a_name: psu0
|
||||||
|
# termination_a_device: server04
|
||||||
|
# termination_a_class: PowerPort
|
||||||
|
# termination_b_feed:
|
||||||
|
# name: power feed 1
|
||||||
|
# power_panel:
|
||||||
|
# name: power panel AMS 1
|
||||||
|
# site: AMS 1
|
||||||
|
|
||||||
|
# - termination_a_name: outlet1
|
||||||
|
# termination_a_device: server04
|
||||||
|
# termination_a_class: PowerOutlet
|
||||||
|
# termination_b_name: psu1
|
||||||
|
# termination_b_device: server04
|
||||||
|
# termination_b_class: PowerPort
|
@ -19,9 +19,17 @@
|
|||||||
# parent: ath0
|
# parent: ath0
|
||||||
# - device: server01
|
# - device: server01
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# type: virtual
|
# type: 1000base-x-sfp
|
||||||
# name: to-server02
|
# name: to-server02
|
||||||
# - device: server02
|
# - device: server02
|
||||||
# enabled: true
|
# enabled: true
|
||||||
# type: virtual
|
# type: 1000base-x-sfp
|
||||||
# name: to-server01
|
# name: to-server01
|
||||||
|
# - device: server02
|
||||||
|
# enabled: true
|
||||||
|
# type: 1000base-t
|
||||||
|
# name: eth0
|
||||||
|
# - device: server02
|
||||||
|
# enabled: true
|
||||||
|
# type: virtual
|
||||||
|
# name: loopback
|
||||||
|
@ -31,8 +31,7 @@
|
|||||||
# - name_template: ttyS[1-48]
|
# - name_template: ttyS[1-48]
|
||||||
# type: rj-45
|
# type: rj-45
|
||||||
# power_ports:
|
# power_ports:
|
||||||
# - name: psu0 # both non-template and template field specified; non-template field takes precedence
|
# - name_template: psu[0,1]
|
||||||
# name_template: psu[0,1]
|
|
||||||
# type: iec-60320-c14
|
# type: iec-60320-c14
|
||||||
# maximum_draw: 35
|
# maximum_draw: 35
|
||||||
# allocated_draw: 35
|
# allocated_draw: 35
|
||||||
@ -46,7 +45,9 @@
|
|||||||
# type: 8p8c
|
# type: 8p8c
|
||||||
# positions_template: "[3,2]"
|
# positions_template: "[3,2]"
|
||||||
# device_bays:
|
# device_bays:
|
||||||
# - name_template: bay[0-9]
|
# - name: bay0 # both non-template and template field specified; non-template field takes precedence
|
||||||
|
# name_template: bay[0-9]
|
||||||
|
# label: test0
|
||||||
# label_template: test[0-5,9,6-8]
|
# label_template: test[0-5,9,6-8]
|
||||||
# description: Test description
|
# description: Test description
|
||||||
# power_outlets:
|
# power_outlets:
|
||||||
|
@ -36,14 +36,13 @@ def expand_templates(params: List[dict], device_type: DeviceType) -> List[dict]:
|
|||||||
|
|
||||||
if field in param:
|
if field in param:
|
||||||
has_plain_fields = True
|
has_plain_fields = True
|
||||||
expanded.append(param)
|
|
||||||
elif template_value:
|
elif template_value:
|
||||||
expanded_fields[field] = list(expand_alphanumeric_pattern(template_value))
|
expanded_fields[field] = list(expand_alphanumeric_pattern(template_value))
|
||||||
|
|
||||||
if expanded_fields and has_plain_fields:
|
if expanded_fields and has_plain_fields:
|
||||||
raise ValueError(f"Mix of plain and template keys provided for {templateable_fields}")
|
raise ValueError(f"Mix of plain and template keys provided for {templateable_fields}")
|
||||||
|
elif not expanded_fields:
|
||||||
if not expanded_fields:
|
expanded.append(param)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
elements = list(expanded_fields.values())
|
elements = list(expanded_fields.values())
|
||||||
|
227
startup_scripts/460_cables.py
Normal file
227
startup_scripts/460_cables.py
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
import sys
|
||||||
|
from typing import Tuple
|
||||||
|
|
||||||
|
from circuits.models import Circuit, CircuitTermination, ProviderNetwork
|
||||||
|
from dcim.models import (
|
||||||
|
Cable,
|
||||||
|
ConsolePort,
|
||||||
|
ConsoleServerPort,
|
||||||
|
FrontPort,
|
||||||
|
Interface,
|
||||||
|
PowerFeed,
|
||||||
|
PowerOutlet,
|
||||||
|
PowerPanel,
|
||||||
|
PowerPort,
|
||||||
|
RearPort,
|
||||||
|
Site,
|
||||||
|
)
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
|
from django.db.models import Q
|
||||||
|
from startup_script_utils import load_yaml
|
||||||
|
|
||||||
|
CONSOLE_PORT_TERMINATION = ContentType.objects.get_for_model(ConsolePort)
|
||||||
|
CONSOLE_SERVER_PORT_TERMINATION = ContentType.objects.get_for_model(ConsoleServerPort)
|
||||||
|
FRONT_PORT_TERMINATION = ContentType.objects.get_for_model(FrontPort)
|
||||||
|
REAR_PORT_TERMINATION = ContentType.objects.get_for_model(RearPort)
|
||||||
|
FRONT_AND_REAR = [FRONT_PORT_TERMINATION, REAR_PORT_TERMINATION]
|
||||||
|
POWER_PORT_TERMINATION = ContentType.objects.get_for_model(PowerPort)
|
||||||
|
POWER_OUTLET_TERMINATION = ContentType.objects.get_for_model(PowerOutlet)
|
||||||
|
POWER_FEED_TERMINATION = ContentType.objects.get_for_model(PowerFeed)
|
||||||
|
POWER_TERMINATIONS = [POWER_PORT_TERMINATION, POWER_OUTLET_TERMINATION, POWER_FEED_TERMINATION]
|
||||||
|
|
||||||
|
VIRTUAL_INTERFACES = ["bridge", "lag", "virtual"]
|
||||||
|
|
||||||
|
|
||||||
|
def get_termination_object(params: dict, side: str):
|
||||||
|
klass = params.pop(f"termination_{side}_class")
|
||||||
|
name = params.pop(f"termination_{side}_name", None)
|
||||||
|
device = params.pop(f"termination_{side}_device", None)
|
||||||
|
feed_params = params.pop(f"termination_{side}_feed", None)
|
||||||
|
circuit_params = params.pop(f"termination_{side}_circuit", {})
|
||||||
|
|
||||||
|
if name and device:
|
||||||
|
termination = klass.objects.get(name=name, device__name=device)
|
||||||
|
return termination
|
||||||
|
elif feed_params:
|
||||||
|
q = {
|
||||||
|
"name": feed_params["power_panel"]["name"],
|
||||||
|
"site__name": feed_params["power_panel"]["site"],
|
||||||
|
}
|
||||||
|
power_panel = PowerPanel.objects.get(**q)
|
||||||
|
termination = PowerFeed.objects.get(name=feed_params["name"], power_panel=power_panel)
|
||||||
|
return termination
|
||||||
|
elif circuit_params:
|
||||||
|
circuit = Circuit.objects.get(cid=circuit_params.pop("cid"))
|
||||||
|
term_side = circuit_params.pop("term_side").upper()
|
||||||
|
|
||||||
|
site_name = circuit_params.pop("site", None)
|
||||||
|
provider_network = circuit_params.pop("provider_network", None)
|
||||||
|
|
||||||
|
if site_name:
|
||||||
|
circuit_params["site"] = Site.objects.get(name=site_name)
|
||||||
|
elif provider_network:
|
||||||
|
circuit_params["provider_network"] = ProviderNetwork.objects.get(name=provider_network)
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
f"⚠️ Missing one of required parameters: 'site' or 'provider_network' "
|
||||||
|
f"for side {term_side} of circuit {circuit}"
|
||||||
|
)
|
||||||
|
|
||||||
|
termination, created = CircuitTermination.objects.get_or_create(
|
||||||
|
circuit=circuit, term_side=term_side, defaults=circuit_params
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
print(f"⚡ Created new CircuitTermination {termination}")
|
||||||
|
|
||||||
|
return termination
|
||||||
|
|
||||||
|
raise ValueError(
|
||||||
|
f"⚠️ Missing parameters for termination_{side}. "
|
||||||
|
"Need termination_{side}_name AND termination_{side}_device OR termination_{side}_circuit"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_termination_class_by_name(port_class: str):
|
||||||
|
if not port_class:
|
||||||
|
return Interface
|
||||||
|
|
||||||
|
return globals()[port_class]
|
||||||
|
|
||||||
|
|
||||||
|
def cable_in_cables(term_a: tuple, term_b: tuple) -> bool:
|
||||||
|
"""Check if cable exist for given terminations.
|
||||||
|
Each tuple should consist termination object and termination type
|
||||||
|
"""
|
||||||
|
|
||||||
|
cable = Cable.objects.filter(
|
||||||
|
Q(
|
||||||
|
termination_a_id=term_a[0].id,
|
||||||
|
termination_a_type=term_a[1],
|
||||||
|
termination_b_id=term_b[0].id,
|
||||||
|
termination_b_type=term_b[1],
|
||||||
|
)
|
||||||
|
| Q(
|
||||||
|
termination_a_id=term_b[0].id,
|
||||||
|
termination_a_type=term_b[1],
|
||||||
|
termination_b_id=term_a[0].id,
|
||||||
|
termination_b_type=term_a[1],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return cable.exists()
|
||||||
|
|
||||||
|
|
||||||
|
def check_termination_types(type_a, type_b) -> Tuple[bool, str]:
|
||||||
|
if type_a in POWER_TERMINATIONS and type_b in POWER_TERMINATIONS:
|
||||||
|
if type_a == type_b:
|
||||||
|
return False, "Can't connect the same power terminations together"
|
||||||
|
elif (
|
||||||
|
type_a == POWER_OUTLET_TERMINATION
|
||||||
|
and type_b == POWER_FEED_TERMINATION
|
||||||
|
or type_a == POWER_FEED_TERMINATION
|
||||||
|
and type_b == POWER_OUTLET_TERMINATION
|
||||||
|
):
|
||||||
|
return False, "PowerOutlet can't be connected with PowerFeed"
|
||||||
|
elif type_a in POWER_TERMINATIONS or type_b in POWER_TERMINATIONS:
|
||||||
|
return False, "Can't mix power terminations with port terminations"
|
||||||
|
elif type_a in FRONT_AND_REAR or type_b in FRONT_AND_REAR:
|
||||||
|
return True, ""
|
||||||
|
elif (
|
||||||
|
type_a == CONSOLE_PORT_TERMINATION
|
||||||
|
and type_b != CONSOLE_SERVER_PORT_TERMINATION
|
||||||
|
or type_b == CONSOLE_PORT_TERMINATION
|
||||||
|
and type_a != CONSOLE_SERVER_PORT_TERMINATION
|
||||||
|
):
|
||||||
|
return False, "ConsolePorts can only be connected to ConsoleServerPorts or Front/Rear ports"
|
||||||
|
return True, ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_cable_name(termination_a: tuple, termination_b: tuple) -> str:
|
||||||
|
"""Returns name of a cable in format:
|
||||||
|
device_a interface_a <---> interface_b device_b
|
||||||
|
or for circuits:
|
||||||
|
circuit_a termination_a <---> termination_b circuit_b
|
||||||
|
"""
|
||||||
|
cable_name = []
|
||||||
|
|
||||||
|
for is_side_b, termination in enumerate([termination_a, termination_b]):
|
||||||
|
try:
|
||||||
|
power_panel_id = getattr(termination[0], "power_panel_id", None)
|
||||||
|
if power_panel_id:
|
||||||
|
power_feed = PowerPanel.objects.get(id=power_panel_id)
|
||||||
|
segment = [f"{power_feed}", f"{termination[0]}"]
|
||||||
|
else:
|
||||||
|
segment = [f"{termination[0].device}", f"{termination[0]}"]
|
||||||
|
except AttributeError:
|
||||||
|
segment = [f"{termination[0].circuit.cid}", f"{termination[0]}"]
|
||||||
|
|
||||||
|
if is_side_b:
|
||||||
|
segment.reverse()
|
||||||
|
|
||||||
|
cable_name.append(" ".join(segment))
|
||||||
|
|
||||||
|
return " <---> ".join(cable_name)
|
||||||
|
|
||||||
|
|
||||||
|
def check_interface_types(*args):
|
||||||
|
for termination in args:
|
||||||
|
try:
|
||||||
|
if termination.type in VIRTUAL_INTERFACES:
|
||||||
|
raise Exception(
|
||||||
|
f"⚠️ Virtual interfaces are not supported for cabling. "
|
||||||
|
f"Termination {termination.device} {termination} {termination.type}"
|
||||||
|
)
|
||||||
|
except AttributeError:
|
||||||
|
# CircuitTermination doesn't have a type field
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def check_terminations_are_free(*args):
|
||||||
|
any_failed = False
|
||||||
|
for termination in args:
|
||||||
|
if termination.cable_id:
|
||||||
|
any_failed = True
|
||||||
|
print(
|
||||||
|
f"⚠️ Termination {termination} is already occupied "
|
||||||
|
f"with cable #{termination.cable_id}"
|
||||||
|
)
|
||||||
|
if any_failed:
|
||||||
|
raise Exception("⚠️ At least one end of the cable is already occupied.")
|
||||||
|
|
||||||
|
|
||||||
|
cables = load_yaml("/opt/netbox/initializers/cables.yml")
|
||||||
|
|
||||||
|
if cables is None:
|
||||||
|
sys.exit()
|
||||||
|
|
||||||
|
for params in cables:
|
||||||
|
params["termination_a_class"] = get_termination_class_by_name(params.get("termination_a_class"))
|
||||||
|
params["termination_b_class"] = get_termination_class_by_name(params.get("termination_b_class"))
|
||||||
|
|
||||||
|
term_a = get_termination_object(params, side="a")
|
||||||
|
term_b = get_termination_object(params, side="b")
|
||||||
|
|
||||||
|
check_interface_types(term_a, term_b)
|
||||||
|
|
||||||
|
term_a_ct = ContentType.objects.get_for_model(term_a)
|
||||||
|
term_b_ct = ContentType.objects.get_for_model(term_b)
|
||||||
|
|
||||||
|
types_ok, msg = check_termination_types(term_a_ct, term_b_ct)
|
||||||
|
cable_name = get_cable_name((term_a, term_a_ct), (term_b, term_b_ct))
|
||||||
|
|
||||||
|
if not types_ok:
|
||||||
|
print(f"⚠️ Invalid termination types for {cable_name}. {msg}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cable_in_cables((term_a, term_a_ct), (term_b, term_b_ct)):
|
||||||
|
continue
|
||||||
|
|
||||||
|
check_terminations_are_free(term_a, term_b)
|
||||||
|
|
||||||
|
params["termination_a_id"] = term_a.id
|
||||||
|
params["termination_b_id"] = term_b.id
|
||||||
|
params["termination_a_type"] = term_a_ct
|
||||||
|
params["termination_b_type"] = term_b_ct
|
||||||
|
|
||||||
|
cable = Cable.objects.create(**params)
|
||||||
|
|
||||||
|
print(f"🧷 Created cable {cable} {cable_name}")
|
Loading…
Reference in New Issue
Block a user