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

1# SPDX-License-Identifier: Apache-2.0 

2# Copyright (C) 2025 Marcin Zieba <marcinpsk@gmail.com> 

3"""Build name-template variables from NetBox rows.""" 

4 

5import re 

6from typing import NamedTuple 

7 

8from .name_template import ( 

9 NamingContext, 

10 TemplateVariableCondition, 

11 TemplateVariableSource, 

12 variables_for_context, 

13) 

14 

15_ASCII_DIGIT = re.compile("[0-9]") 

16 

17 

18def _extract_trailing_digits(value: str) -> str: 

19 r"""Return the trailing digit run of *value* without regex backtracking. 

20 

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. 

23 

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:] 

30 

31 

32def numeric_suffix(value) -> str: 

33 """Return the number *value* ends with, as a decimal literal, or "0" when it has none. 

34 

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" 

44 

45 

46def _variable_position(position, name): 

47 """Return the bay position that template variables read from a bay with *position* and *name*. 

48 

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 

55 

56 

57def _resolve_bay_position(module_bay): 

58 """Return the raw and numeric positions for *module_bay*. 

59 

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) 

66 

67 

68def bay_naming_values(position, name) -> tuple[str, str]: 

69 """Return what the names in a bay with *position* and *name* are built from. 

70 

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) 

76 

77 

78def _resolve_slot(module_bay, bay_position, parent_bay_position): 

79 """Return the slot value from the module-bay hierarchy. 

80 

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 

97 

98 

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 } 

106 

107 

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() :] 

112 

113 

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) 

135 

136 

137def chassis_position(device): 

138 """Return the virtual-chassis position that the template variables of *device* read, or None. 

139 

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 

145 

146 

147class BayChain(NamedTuple): 

148 """The positions of a module's bay chain that its template variables read.""" 

149 

150 slot: str 

151 bay_position: str 

152 parent_bay_position: str 

153 bay_position_num: str 

154 

155 

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) 

164 

165 

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 ) 

175 

176 

177def build_variables(module_bay, device=None): 

178 """Build template variables from a module bay and optional device. 

179 

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. 

186 

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)) 

192 

193 

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)