Coverage for birdplan/bird_config/sections/functions.py: 96%

100 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-04-23 03:27 +0000

1# 

2# SPDX-License-Identifier: GPL-3.0-or-later 

3# 

4# Copyright (c) 2019-2024, AllWorldIT 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU General Public License as published by 

8# the Free Software Foundation, either version 3 of the License, or 

9# (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, 

12# but WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

14# GNU General Public License for more details. 

15# 

16# You should have received a copy of the GNU General Public License 

17# along with this program. If not, see <http://www.gnu.org/licenses/>. 

18 

19"""BIRD functions configuration.""" 

20 

21import textwrap 

22from collections import OrderedDict 

23from typing import Any, Callable, Dict, List 

24 

25from ...bird_config.globals import BirdConfigGlobals 

26from .base import SectionBase 

27 

28__all__ = ["BirdVariable", "BirdFunction", "SectionFunctions"] 

29 

30 

31class BirdVariable(str): 

32 """BIRD constant class.""" 

33 

34 

35class BirdFunction: # pylint: disable=invalid-name,too-few-public-methods 

36 r''' 

37 BIRD function decorator class. 

38 

39 This decorator is used to decorate a BIRD function returned in Python. 

40 

41 When the decorated function is used, a reference to the function is stored and when the configuration is 

42 generated the function is called and the output included in the configuration functions section. 

43 

44 An example of a decorated function is below... 

45 ``` 

46 @BirdFunction("bgp_some_function") 

47 def bgp_func(self) -> str: 

48 """Test function.""" 

49 

50 return """\ 

51 # Some BIRD function 

52 function bgp_some_function(string filter_name; bool capable; int peer_asn; int type_asn) { 

53 ... 

54 } 

55 }""" 

56 ``` 

57 

58 This would be used in BIRD configuration like this... 

59 ``` 

60 conf.append(f"if {obj.bgpfunc('hello world')} then print 'hello there';") 

61 ``` 

62 

63 Parameters 

64 ---------- 

65 bird_func_name : str 

66 BIRD function name. 

67 

68 ''' # noqa: RST201,RST203,RST215,RST214,RST301 

69 

70 bird_func_name: str 

71 

72 def __init__(self, bird_func_name: str): 

73 """Initialize object.""" 

74 # Lets keep track of our BIRD function name 

75 self.bird_func_name = bird_func_name 

76 

77 def __call__(self, func: Callable[..., str]) -> Callable[..., str]: 

78 """Return the wrapper.""" 

79 

80 def wrapped_function(*args: Any, **kwargs: Any) -> str: 

81 """My decorator.""" 

82 # Grab the parent object 

83 parent_object = args[0] 

84 # Make sure it has a bird_functions attribute 

85 if hasattr(parent_object, "bird_functions"): 

86 # Check if this function exists... 

87 if self.bird_func_name not in parent_object.bird_functions: 

88 # If not add it to the function list 

89 parent_object.bird_functions[self.bird_func_name] = func(*args) 

90 else: 

91 raise RuntimeError("Decorator 'bird_function' used on a class method without a 'bird_functions' attribute") 

92 

93 bird_args: List[str] = [] 

94 # Check if we're not outputting the filter_name 

95 needs_filter_name = not kwargs.get("no_filter_name", False) 

96 if needs_filter_name: 

97 bird_args.append(BirdVariable("filter_name")) 

98 # Loop with python arguments and translate into BIRD arguments 

99 for arg in args[1:]: 

100 value = "" 

101 # Check for unquaoted BIRD variables 

102 if isinstance(arg, BirdVariable): 

103 value = arg 

104 # Check for a boolean 

105 elif isinstance(arg, bool): 

106 if arg: 

107 value = "true" 

108 else: 

109 value = "false" 

110 # Check for a number 

111 elif isinstance(arg, int): 

112 value = f"{arg}" 

113 # Check if this is a string 

114 elif isinstance(arg, str): 

115 value = f'"{arg}"' 

116 # Everything else is not implemented atm 

117 else: 

118 raise NotImplementedError(f"Unknown type for '{self.bird_func_name}' value '{arg}'") 

119 # Add BIRD argument 

120 bird_args.append(value) 

121 

122 # Build the list of BIRD arguments in a string 

123 bird_args_str = ", ".join(bird_args) 

124 # Return the BIRD function call 

125 return f"{self.bird_func_name}({bird_args_str})" 

126 

127 # Finally return the wrapped function 

128 return wrapped_function 

129 

130 

131class SectionFunctions(SectionBase): 

132 """BIRD functions configuration.""" 

133 

134 bird_functions: Dict[str, str] 

135 

136 def __init__(self, birdconfig_globals: BirdConfigGlobals): 

137 """Initialize the object.""" 

138 super().__init__(birdconfig_globals) 

139 

140 self._section = "Global Functions" 

141 

142 self.bird_functions = OrderedDict() 

143 

144 def configure(self) -> None: 

145 """Configure global constants.""" 

146 super().configure() 

147 

148 # Check if we're adding functions 

149 for _, content in self.bird_functions.items(): 

150 self.conf.add(textwrap.dedent(content)) 

151 self.conf.add("") 

152 

153 @BirdFunction("prefix_is_longer") 

154 def prefix_is_longer(self, *args: Any) -> str: # pylint: disable=unused-argument 

155 """BIRD prefix_is_longer function.""" 

156 

157 return """\ 

158 # Match a prefix longer than "size" 

159 function prefix_is_longer(string filter_name; int size) -> bool { 

160 if (net.len > size) then { 

161 if DEBUG then print filter_name, 

162 " [prefix_is_longer] Matched ", net, " from ", proto, " against size ", size; 

163 return true; 

164 } else { 

165 return false; 

166 } 

167 }""" 

168 

169 @BirdFunction("prefix_is_shorter") 

170 def prefix_is_shorter(self, *args: Any) -> str: # pylint: disable=unused-argument 

171 """BIRD prefix_is_shorter function.""" 

172 

173 return """\ 

174 # Match a prefix shorter than "size" 

175 function prefix_is_shorter(string filter_name; int size) -> bool { 

176 if (net.len < size) then { 

177 if DEBUG then print filter_name, 

178 " [prefix_is_shorter] Matched ", net, " from ", proto, " against size ", size; 

179 return true; 

180 } 

181 return false; 

182 }""" 

183 

184 @BirdFunction("accept_kernel") 

185 def accept_kernel(self, *args: Any) -> str: # pylint: disable=unused-argument 

186 """BIRD accept_kernel function.""" 

187 

188 return f"""\ 

189 # Accept kernel route 

190 function accept_kernel(string filter_name) -> bool {{ 

191 if (!{self.is_kernel()} || {self.is_default()}) then return false; 

192 if DEBUG then print filter_name, 

193 " [accept_kernel] Accepting kernel route ", {self.route_info()}; 

194 accept; 

195 }}""" 

196 

197 @BirdFunction("accept_kernel_default") 

198 def accept_kernel_default(self, *args: Any) -> str: # pylint: disable=unused-argument 

199 """BIRD accept_kernel_default function.""" 

200 

201 return f"""\ 

202 # Accept kernel route 

203 function accept_kernel_default(string filter_name) -> bool {{ 

204 if (!{self.is_kernel()} || !{self.is_default()}) then return false; 

205 if DEBUG then print filter_name, 

206 " [accept_kernel_default] Accepting kernel default route ", {self.route_info()}; 

207 accept; 

208 }}""" 

209 

210 @BirdFunction("accept_static") 

211 def accept_static(self, *args: Any) -> str: # pylint: disable=unused-argument 

212 """BIRD accept_static function.""" 

213 

214 return f"""\ 

215 # Accept static route 

216 function accept_static(string filter_name) -> bool {{ 

217 if (!{self.is_static()} || {self.is_default()}) then return false; 

218 if DEBUG then print filter_name, 

219 " [accept_static] Accepting static route ", {self.route_info()}; 

220 accept; 

221 }}""" 

222 

223 @BirdFunction("accept_static_default") 

224 def accept_static_default(self, *args: Any) -> str: # pylint: disable=unused-argument 

225 """BIRD accept_static_default function.""" 

226 

227 return f"""\ 

228 # Accept static default route 

229 function accept_static_default(string filter_name) -> bool {{ 

230 if (!{self.is_static()} || !{self.is_default()}) then return false; 

231 if DEBUG then print filter_name, 

232 " [accept_static] Accepting static default route ", {self.route_info()}; 

233 accept; 

234 }}""" 

235 

236 @BirdFunction("is_bgp") 

237 def is_bgp(self, *args: Any) -> str: # pylint: disable=unused-argument 

238 """BIRD is_bgp function.""" 

239 

240 return """\ 

241 # Match BGP routes 

242 function is_bgp(string filter_name) -> bool { 

243 if (source = RTS_BGP) then return true; 

244 return false; 

245 }""" 

246 

247 @BirdFunction("is_bogon") 

248 def is_bogon(self, *args: Any) -> str: # pylint: disable=unused-argument 

249 """BIRD is_bogon function.""" 

250 

251 return f"""\ 

252 # Match on IP bogons 

253 function is_bogon(string filter_name) -> bool {{ 

254 if ((net.type = NET_IP4 && net ~ BOGONS_V4) || (net.type = NET_IP6 && net ~ BOGONS_V6)) then {{ 

255 if DEBUG then print filter_name, 

256 " [is_bogon] Matched ", {self.route_info()}; 

257 return true; 

258 }} 

259 return false; 

260 }}""" 

261 

262 @BirdFunction("is_connected") 

263 def is_connected(self, *args: Any) -> str: # pylint: disable=unused-argument 

264 """BIRD is_connected function.""" 

265 

266 return """\ 

267 # Match connected route 

268 function is_connected(string filter_name) -> bool { 

269 if (proto = "direct4" || proto = "direct6") then return true; 

270 return false; 

271 }""" 

272 

273 @BirdFunction("is_default") 

274 def is_default(self, *args: Any) -> str: # pylint: disable=unused-argument 

275 """BIRD is_default function.""" 

276 

277 return """\ 

278 # Match default route 

279 function is_default(string filter_name) -> bool { 

280 if ((net.type = NET_IP4 && net = DEFAULT_ROUTE_V4) || (net.type = NET_IP6 && net = DEFAULT_ROUTE_V6)) then 

281 return true; 

282 return false; 

283 }""" 

284 

285 @BirdFunction("is_kernel") 

286 def is_kernel(self, *args: Any) -> str: # pylint: disable=unused-argument 

287 """BIRD is_kernel function.""" 

288 

289 # NK: Below we explicitly exclude krt_source = 186 as we seem to import IPv6 routes into Bird for some reason 

290 return """\ 

291 # Match kernel route 

292 function is_kernel(string filter_name) -> bool { 

293 if (source = RTS_INHERIT && krt_source != 186) then return true; 

294 return false; 

295 }""" 

296 

297 @BirdFunction("is_static") 

298 def is_static(self, *args: Any) -> str: # pylint: disable=unused-argument 

299 """BIRD is_static function.""" 

300 

301 return """\ 

302 # Match static route 

303 function is_static(string filter_name) -> bool { 

304 if (proto = "static4" || proto = "static6") then return true; 

305 return false; 

306 }""" 

307 

308 @BirdFunction("redistribute_kernel") 

309 def redistribute_kernel(self, *args: Any) -> str: # pylint: disable=unused-argument 

310 """BIRD redistribute_kernel function.""" 

311 

312 return f"""\ 

313 # Accept kernel route 

314 function redistribute_kernel(string filter_name) -> bool {{ 

315 if (!{self.is_kernel()} || {self.is_default()}) then return false; 

316 if DEBUG then print filter_name, 

317 " [redistribute_kernel] Accepting kernel route ", {self.route_info()}; 

318 accept; 

319 }}""" 

320 

321 @BirdFunction("redistribute_kernel_default") 

322 def redistribute_kernel_default(self, *args: Any) -> str: # pylint: disable=unused-argument 

323 """BIRD redistribute_kernel_default function.""" 

324 

325 return f"""\ 

326 # Accept kernel route 

327 function redistribute_kernel_default(string filter_name) -> bool {{ 

328 if (!{self.is_kernel()} || !{self.is_default()}) then return false; 

329 if DEBUG then print filter_name, 

330 " [redistribute_kernel_default] Accepting kernel default route ", {self.route_info()}; 

331 accept; 

332 }}""" 

333 

334 @BirdFunction("redistribute_static") 

335 def redistribute_static(self, *args: Any) -> str: # pylint: disable=unused-argument 

336 """BIRD redistribute_static function.""" 

337 

338 return f"""\ 

339 # Accept static route 

340 function redistribute_static(string filter_name) -> bool {{ 

341 if (!{self.is_static()} || {self.is_default()}) then return false; 

342 if DEBUG then print filter_name, 

343 " [redistribute_static] Accepting static route ", {self.route_info()}; 

344 accept; 

345 }}""" 

346 

347 @BirdFunction("redistribute_static_default") 

348 def redistribute_static_default(self, *args: Any) -> str: # pylint: disable=unused-argument 

349 """BIRD redistribute_static_default function.""" 

350 

351 return f"""\ 

352 # Accept static default route 

353 function redistribute_static_default(string filter_name) -> bool {{ 

354 if (!{self.is_static()} || !{self.is_default()}) then return false; 

355 if DEBUG then print filter_name, 

356 " [redistribute_static] Accepting static default route ", {self.route_info()}; 

357 accept; 

358 }}""" 

359 

360 def route_info(self) -> str: # pylint: disable=unused-argument 

361 """BIRD route_info function.""" 

362 

363 return """net, " from ", proto"""