Coverage for netbox_interface_name_rules / naming.py: 98.86%
68 statements
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-16 10:13 +0000
« prev ^ index » next coverage.py v7.13.4, created at 2026-10-16 10:13 +0000
1# SPDX-License-Identifier: Apache-2.0
2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com>
3"""Build name-template variables from NetBox rows."""
5import re
6from typing import NamedTuple
8from .name_template import (
9 NamingContext,
10 TemplateVariableCondition,
11 TemplateVariableSource,
12 variables_for_context,
13)
15_ASCII_DIGIT = re.compile("[0-9]")
18def _extract_trailing_digits(value: str) -> str:
19 r"""Return the trailing digit run of *value* without regex backtracking.
21 This O(n) string scan avoids the polynomial backtracking risk from a
22 trailing-digit regular expression on a long value that ends in a non-digit.
24 Returns an empty string when *value* has no trailing digits.
25 """
26 index = len(value)
27 while index > 0 and value[index - 1].isdigit():
28 index -= 1
29 return value[index:]
32def numeric_suffix(value) -> str:
33 """Return the number *value* ends with, as a decimal literal, or "0" when it has none.
35 A device type may compose the parent into a bay position, so any position can arrive
36 path-shaped, such as TenGigabitEthernet3/2/1. The result goes into arithmetic, so it is
37 canonical: a zero-padded run such as "02" becomes "2", which Python rejects as a literal,
38 and a non-ASCII digit run yields "0" rather than a value the evaluator cannot read.
39 """
40 digits = _extract_trailing_digits(str(value))
41 if not digits.isascii():
42 return "0"
43 return str(int(digits)) if digits else "0"
46def _variable_position(position, name):
47 """Return the bay position that template variables read from a bay with *position* and *name*.
49 A template expression such as ``{module}`` resolves from trailing digits in the bay name.
50 """
51 bay_position = position or "0"
52 if bay_position.startswith("{"):
53 return _extract_trailing_digits(name) or "0"
54 return bay_position
57def _resolve_bay_position(module_bay):
58 """Return the raw and numeric positions for *module_bay*.
60 The raw position comes from ``_variable_position``. The numeric position comes from
61 ``numeric_suffix``, the one function every numeric variable is derived through, so a
62 zero-padded position such as ``"02"`` reaches arithmetic as ``"2"``.
63 """
64 bay_position = _variable_position(module_bay.position, module_bay.name)
65 return bay_position, numeric_suffix(bay_position)
68def bay_naming_values(position, name) -> tuple[str, str]:
69 """Return what the names in a bay with *position* and *name* are built from.
71 The first value is the stored position. NetBox's raw template names read it, and so do the
72 variables of nested modules. The second value is the position that the variables of the module
73 in the bay read. A bay name edit that changes neither value changes no name.
74 """
75 return position, _variable_position(position, name)
78def _resolve_slot(module_bay, bay_position, parent_bay_position):
79 """Return the slot value from the module-bay hierarchy.
81 A nested bay takes the parent or grandparent position. A bay owned by an
82 installed module takes that module's bay position. Other bays use the digits
83 of their own position, as stored, because a slot is a position and not a
84 number; ``slot_num`` is the counterpart arithmetic reads.
85 """
86 if module_bay.parent:
87 parent_bay = module_bay.parent
88 if parent_bay.parent and hasattr(parent_bay.parent, "installed_module"):
89 return parent_bay.parent.position or parent_bay_position
90 return parent_bay_position
91 own_digits = _extract_trailing_digits(bay_position) or "0"
92 if hasattr(module_bay, "module") and module_bay.module:
93 owner_module = module_bay.module
94 if hasattr(owner_module, "module_bay") and owner_module.module_bay: 94 ↛ 96line 94 didn't jump to line 96 because the condition on line 94 was always true
95 return owner_module.module_bay.position or own_digits
96 return own_digits
99def _filter_variables(context, source, values, conditions):
100 """Select the catalogue values of *context* whose condition *conditions* maps to True."""
101 return {
102 variable.name: values[variable.name]
103 for variable in variables_for_context(context, source=source)
104 if variable.condition is None or conditions[variable.condition]
105 }
108def _position_path(position):
109 """Return *position* from its first ASCII digit, or None when it has none."""
110 digit = _ASCII_DIGIT.search(position)
111 return None if digit is None else position[digit.start() :]
114def build_bay_chain_variables(slot, bay_position, parent_bay_position, vc_position=None, *, bay_position_num=None):
115 """Build the catalogue entries derived from resolved module-bay positions."""
116 if bay_position_num is None:
117 bay_position_num = numeric_suffix(bay_position)
118 bay_position_path = _position_path(bay_position)
119 values = {
120 "slot": slot,
121 "slot_num": numeric_suffix(slot),
122 "bay_position": bay_position,
123 "bay_position_num": bay_position_num,
124 "bay_position_path": bay_position_path,
125 "parent_bay_position": parent_bay_position,
126 "parent_bay_position_num": numeric_suffix(parent_bay_position),
127 "sfp_slot": bay_position_num,
128 "vc_position": str(vc_position),
129 }
130 conditions = {
131 TemplateVariableCondition.VIRTUAL_CHASSIS_MEMBER: vc_position is not None,
132 TemplateVariableCondition.BAY_POSITION_HAS_DIGIT: bay_position_path is not None,
133 }
134 return _filter_variables(NamingContext.MODULE_MEMBER, TemplateVariableSource.MODULE_BAY_CHAIN, values, conditions)
137def chassis_position(device):
138 """Return the virtual-chassis position that the template variables of *device* read, or None.
140 Only a virtual-chassis member that has a position has one; position zero is a valid position.
141 """
142 if device is None or device.virtual_chassis_id is None:
143 return None
144 return device.vc_position
147class BayChain(NamedTuple):
148 """The positions of a module's bay chain that its template variables read."""
150 slot: str
151 bay_position: str
152 parent_bay_position: str
153 bay_position_num: str
156def bay_chain(module_bay) -> BayChain:
157 """Return the ``BayChain`` of the module in *module_bay*."""
158 bay_position, bay_position_num = _resolve_bay_position(module_bay)
159 parent_bay_position = "0"
160 if module_bay.parent:
161 parent_bay_position = module_bay.parent.position or "0"
162 slot = _resolve_slot(module_bay, bay_position, parent_bay_position)
163 return BayChain(slot, bay_position, parent_bay_position, bay_position_num)
166def bay_chain_variables(chain, vc_position):
167 """Return the template variables of the bay *chain* at *vc_position*; None leaves ``{vc_position}`` out."""
168 return build_bay_chain_variables(
169 chain.slot,
170 chain.bay_position,
171 chain.parent_bay_position,
172 vc_position=vc_position,
173 bay_position_num=chain.bay_position_num,
174 )
177def build_variables(module_bay, device=None):
178 """Build template variables from a module bay and optional device.
180 The result includes slot, bay position, parent bay position, SFP slot, and a
181 numeric counterpart for every position. A device type may compose the parent
182 into a bay position, giving a path-shaped value such as
183 ``TenGigabitEthernet3/2/1``, so arithmetic templates take the ``_num`` form.
184 A virtual-chassis position is included only for a member device that has a
185 position, and a bay position path only for a bay position that has a digit.
187 A template that uses ``{vc_position}`` for a non-member device fails during
188 evaluation because the variable is intentionally absent. Position zero is
189 retained because it is a valid virtual-chassis position.
190 """
191 return bay_chain_variables(bay_chain(module_bay), chassis_position(device))
194def build_device_interface_variables(interface_name, vc_position):
195 """Build device-interface variables from the current name and VC position."""
196 values = {"base": interface_name, "port": interface_name.rsplit("/", 1)[-1], "vc_position": str(vc_position)}
197 conditions = {TemplateVariableCondition.VIRTUAL_CHASSIS_MEMBER: vc_position is not None}
198 return _filter_variables(NamingContext.DEVICE_INTERFACE, None, values, conditions)