Coverage for netbox_interface_name_rules / family / targets.py: 92.31%

42 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"""Compute the names a rule intends for one interface family. 

4 

5Every planner names a family through this module, so an installed plan, a prospective plan and a 

6structural plan cannot spell the same family differently. Nothing here reads or writes a row: the 

7inputs are names, channel identifiers and the rule. 

8""" 

9 

10from dataclasses import dataclass 

11 

12from ..choices import BreakoutModeChoices 

13from ..name_template import evaluate_name_template 

14from .domain import FamilyStatus 

15 

16AMBIGUOUS_SUFFIX_REASON = "channel suffix is ambiguous or unavailable" 

17UNCLAIMED_BASE_REASON = "no single interface template claims this name as its raw name or its renamed form" 

18 

19 

20@dataclass(frozen=True, slots=True) 

21class FamilyTargets: 

22 """The names a rule intends for one channelized family, and why it may refuse.""" 

23 

24 parent_name: str 

25 channels: tuple[tuple[str | None, str], ...] 

26 status: FamilyStatus | None = None 

27 reason: str = "" 

28 

29 

30def child_name_suffix(child_name, parent_name): # pragma: no cover - requires channelization support 

31 """Return the suffix *child_name* adds to *parent_name*, or None when it adds none. 

32 

33 The first character must be non-alphanumeric so ``et0``/``et01`` is never mistaken for a 

34 family; the punctuation itself is free-form (``:``, ``-``, ``_`` and ``@`` all occur in the 

35 wild), so it is not restricted to a fixed separator. 

36 """ 

37 if not parent_name or not child_name.startswith(parent_name): 

38 return None 

39 suffix = child_name[len(parent_name) :] 

40 if not suffix or suffix[0].isalnum(): 

41 return None 

42 return suffix 

43 

44 

45def template_channel_suffixes(templates): # pragma: no cover - requires channelization support 

46 """Map each channel identifier to the name suffixes the module type's templates give it. 

47 

48 The suffix comes from the template family itself, so a child that lost its parent's prefix in 

49 an earlier partial rename can still be repaired. A module type with several families may spell 

50 the same channel differently in each (``et0:2`` vs ``sw0.2``), so the suffixes are collected per 

51 channel rather than overwritten: recovery only uses one when every family agrees on it. 

52 """ 

53 parents = {template.pk: template.resolved for template in templates if template.channels is not None} 

54 suffixes: dict[int, set[str]] = {} 

55 for template in templates: 

56 if template.channel_id is None or template.parent_id not in parents: 

57 continue 

58 suffix = child_name_suffix(template.resolved, parents[template.parent_id]) 

59 if suffix is not None: 

60 suffixes.setdefault(template.channel_id, set()).add(suffix) 

61 return suffixes 

62 

63 

64def breaks_out(rule) -> bool: 

65 """Return whether *rule* builds an interface family, flat or channelized.""" 

66 return rule.channel_count > 0 

67 

68 

69def builds_channelized_family(rule) -> bool: 

70 """Return whether *rule* builds a channelized family instead of flat sibling interfaces.""" 

71 return rule.channel_count > 0 and rule.breakout_mode == BreakoutModeChoices.CHANNELIZED 

72 

73 

74def builds_flat_family(rule) -> bool: 

75 """Return whether *rule* builds a flat breakout family of sibling interfaces.""" 

76 return rule.channel_count > 0 and rule.breakout_mode == BreakoutModeChoices.FLAT 

77 

78 

79def names_installed_parent(rule) -> bool: 

80 """Return whether *rule* gives the parent of an installed channelized family a new name.""" 

81 if rule.channel_count <= 0: 

82 return True # a simple rule renames the parent, and the channels follow it in lockstep 

83 return bool(used_parent_template(rule)) 

84 

85 

86def used_parent_template(rule) -> str: 

87 """Return the parent name template that *rule* applies, or an empty string when it applies none.""" 

88 return rule.parent_name_template if builds_channelized_family(rule) else "" 

89 

90 

91def one_family_per_name_set(candidates): 

92 """Return the index of one candidate base per family, in candidate order. 

93 

94 *candidates* pairs each base name with the names the rule intends for the family built on it. 

95 Two bases that intend the same names describe one family an earlier apply already started, so 

96 only one of them may build it; the base the family already names is preferred, because renaming 

97 it onto itself is the no-op the other base cannot manage. 

98 """ 

99 kept: dict[tuple[str, ...], int] = {} 

100 for index, (base_name, target_names) in enumerate(candidates): 

101 current = kept.get(target_names) 

102 if current is None or (base_name == target_names[0] and candidates[current][0] != target_names[0]): 

103 kept[target_names] = index 

104 return tuple(sorted(kept.values())) 

105 

106 

107def flat_family_names(rule, variables, base_name): 

108 """Return the names of the flat sibling family that *rule* defines on *base_name*.""" 

109 family_variables = {**variables, "base": base_name} 

110 return tuple( 

111 evaluate_name_template( 

112 rule.name_template, 

113 {**family_variables, "channel": str(rule.channel_start + offset)}, 

114 ) 

115 for offset in range(rule.channel_count) 

116 ) 

117 

118 

119def channelized_family_names(rule, current_name, base_name, variables): # pragma: no cover - channelization only 

120 """Return ``(parent_name, ((channel_id, name), ...))`` for the family *rule* builds on *current_name*. 

121 

122 *base_name* is ``{base}`` for the parent and every channel; ``{channel}`` is 

123 ``channel_start + channel_id - 1``. A blank parent template leaves *current_name* alone. 

124 Takes names rather than the interface so prediction can reuse it without a row to point at. 

125 """ 

126 family_variables = {**variables, "base": base_name} 

127 parent_name = current_name 

128 if rule.parent_name_template: 

129 parent_name = evaluate_name_template(rule.parent_name_template, family_variables) 

130 channels = tuple( 

131 ( 

132 channel_id, 

133 evaluate_name_template( 

134 rule.name_template, {**family_variables, "channel": str(rule.channel_start + channel_id - 1)} 

135 ), 

136 ) 

137 for channel_id in range(1, rule.channel_count + 1) 

138 ) 

139 return parent_name, channels 

140 

141 

142def intended_family_names(rule, variables, current_name, base_name): 

143 """Return every name *rule* intends for the family it builds on *current_name*, with *base_name* as ``{base}``. 

144 

145 A rule without channels intends one name. A base whose names cannot be evaluated is its own 

146 family: it names nothing else, so nothing else can be grouped with it. 

147 """ 

148 try: 

149 if rule.channel_count <= 0: 

150 return (evaluate_name_template(rule.name_template, {**variables, "base": base_name}),) 

151 if builds_channelized_family(rule): 151 ↛ 154line 151 didn't jump to line 154 because the condition on line 151 was always true

152 parent_name, channels = channelized_family_names(rule, current_name, base_name, variables) 

153 return (parent_name, *(name for _channel_id, name in channels)) 

154 return flat_family_names(rule, variables, base_name) 

155 except (TypeError, ValueError): 

156 return (current_name,) 

157 

158 

159def _simple_child_target(child_name, channel_id, parent_name, parent_target, suffixes): # pragma: no cover 

160 """Return a simple rule's child target, or None plus the reason it cannot be derived.""" 

161 suffix = child_name_suffix(child_name, parent_name) 

162 if suffix is None: 

163 candidates = suffixes.get(channel_id, set()) 

164 if len(candidates) == 1: 

165 suffix = next(iter(candidates)) 

166 if suffix is None: 

167 return None, AMBIGUOUS_SUFFIX_REASON 

168 return parent_target + suffix, "" 

169 

170 

171def _failed(parent_name, children, error): # pragma: no cover - requires channelization support 

172 """Return targets that leave the whole family alone because a template could not be evaluated.""" 

173 reason = f"failed to evaluate family targets: {error}" 

174 return FamilyTargets( 

175 parent_name=parent_name, 

176 channels=tuple((child_name, reason) for child_name, _channel_id in children), 

177 status=FamilyStatus.FAILED, 

178 reason=reason, 

179 ) 

180 

181 

182def _unclaimed(parent_name, children): # pragma: no cover - requires channelization support 

183 """Return targets that leave a family alone because no template gives it a raw name.""" 

184 return FamilyTargets( 

185 parent_name=parent_name, 

186 channels=tuple((child_name, UNCLAIMED_BASE_REASON) for child_name, _channel_id in children), 

187 status=FamilyStatus.BLOCKED, 

188 reason=UNCLAIMED_BASE_REASON, 

189 ) 

190 

191 

192def _breakout_targets(rule, variables, parent_name, base_name, parent_channels, children): # pragma: no cover 

193 """Return the names a breakout rule intends for an existing channelized family.""" 

194 if parent_channels != rule.channel_count: 

195 reason = f"installed parent declares {parent_channels} channels but the rule defines {rule.channel_count}" 

196 return FamilyTargets( 

197 parent_name=parent_name, 

198 channels=tuple((child_name, reason) for child_name, _channel_id in children), 

199 status=FamilyStatus.BLOCKED, 

200 reason=reason, 

201 ) 

202 if len(children) < parent_channels: 

203 reason = f"installed family is missing {parent_channels - len(children)} of {parent_channels} channels" 

204 return FamilyTargets( 

205 parent_name=parent_name, 

206 channels=tuple((child_name, reason) for child_name, _channel_id in children), 

207 status=FamilyStatus.BLOCKED, 

208 reason=reason, 

209 ) 

210 try: 

211 parent_target = parent_name 

212 if names_installed_parent(rule): 

213 parent_target = evaluate_name_template(rule.parent_name_template, {**variables, "base": base_name}) 

214 channels = tuple( 

215 ( 

216 evaluate_name_template( 

217 rule.name_template, 

218 {**variables, "base": base_name, "channel": str(rule.channel_start + channel_id - 1)}, 

219 ), 

220 "", 

221 ) 

222 for _child_name, channel_id in children 

223 ) 

224 except (TypeError, ValueError) as error: 

225 return _failed(parent_name, children, error) 

226 return FamilyTargets(parent_name=parent_target, channels=channels) 

227 

228 

229def lockstep_family_targets(rule, variables, parent_name, base_name, children, suffixes): # pragma: no cover 

230 """Return the names a simple rule intends for a family renamed in lockstep with its parent. 

231 

232 The rule's channel count says nothing here: this family already exists, and every member keeps 

233 the suffix it carries under whatever name the parent takes. 

234 """ 

235 try: 

236 parent_target = evaluate_name_template(rule.name_template, {**variables, "base": base_name}) 

237 except (TypeError, ValueError) as error: 

238 return _failed(parent_name, children, error) 

239 channels = tuple( 

240 _simple_child_target(child_name, channel_id, parent_name, parent_target, suffixes) 

241 for child_name, channel_id in children 

242 ) 

243 return FamilyTargets(parent_name=parent_target, channels=channels) 

244 

245 

246def channelized_family_targets( # pragma: no cover - requires channelization support 

247 rule, variables, parent_name, base_name, parent_channels, children, suffixes 

248) -> FamilyTargets: 

249 """Return the names *rule* intends for the channelized family named *parent_name*. 

250 

251 *base_name* is the value of ``{base}``, or None when no template claims the parent. 

252 *children* pairs each channel's current name with its channel identifier, in channel order. 

253 A breakout rule renames the channels it describes; any other rule carries the family along with 

254 its parent, keeping each channel's own suffix. 

255 """ 

256 if base_name is None: 

257 return _unclaimed(parent_name, children) 

258 if rule.channel_count > 0: 

259 return _breakout_targets(rule, variables, parent_name, base_name, parent_channels, children) 

260 return lockstep_family_targets(rule, variables, parent_name, base_name, children, suffixes)