Coverage for birdplan/__init__.py: 78%

692 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"""BirdPlan package.""" 

20 

21# pylint: disable=too-many-lines 

22 

23import grp 

24import json 

25import logging 

26import os 

27import pathlib 

28import pwd 

29import re 

30from typing import Any, Dict, List, Optional, Union 

31 

32import birdclient 

33import jinja2 

34import packaging.version 

35 

36from .bird_config import BirdConfig 

37from .console.colors import colored 

38from .exceptions import BirdPlanError 

39from .version import __version__ 

40from .yaml import YAML, YAMLError 

41 

42__all__ = [ 

43 "BirdPlan", 

44 "__version__", 

45] 

46 

47# Some types we need 

48BirdPlanBGPPeerSummary = Dict[str, Dict[str, Any]] 

49BirdPlanBGPPeerShow = Dict[str, Any] 

50BirdPlanBGPPeerGracefulShutdownStatus = Dict[str, Dict[str, bool]] 

51BirdPlanBGPPeerQuarantineStatus = Dict[str, Dict[str, bool]] 

52BirdPlanOSPFInterfaceStatus = Dict[str, Dict[str, Dict[str, Any]]] 

53BirdPlanOSPFSummary = Dict[str, Dict[str, Any]] 

54 

55# Check we have a sufficiently new version of birdclient 

56if packaging.version.parse(birdclient.__version__) < packaging.version.parse("0.0.9"): 

57 raise BirdPlanError("birdplan requires birdclient version 0.0.9 or newer") 

58 

59 

60class BirdPlan: # pylint: disable=too-many-public-methods 

61 """Main BirdPlan class.""" 

62 

63 _birdconf: BirdConfig 

64 _config: Dict[str, Any] 

65 _state_file: Optional[str] 

66 _yaml: YAML 

67 

68 def __init__(self, test_mode: bool = False) -> None: 

69 """Initialize object.""" 

70 

71 self._birdconf = BirdConfig(test_mode=test_mode) 

72 self._config = {} 

73 self._state_file = None 

74 self._yaml = YAML() 

75 

76 def load(self, **kwargs: Any) -> None: 

77 """ 

78 Initialize object. 

79 

80 Parameters 

81 ---------- 

82 plan_file : str 

83 Source plan file to generate configuration from. 

84 

85 state_file : Optional[str] 

86 Optional state file, used for commands like BGP graceful shutdown. 

87 

88 ignore_irr_changes : bool 

89 Optional parameter to ignore IRR lookups during configuration load. 

90 

91 ignore_peeringdb_changes : bool 

92 Optional parameter to ignore peering DB lookups during configuraiton load. 

93 

94 use_cached : bool 

95 Optional parameter to use cached values from state during configuration load. 

96 

97 """ 

98 

99 # Grab parameters 

100 plan_file: Optional[str] = kwargs.get("plan_file") 

101 state_file: Optional[str] = kwargs.get("state_file") 

102 ignore_irr_changes: bool = kwargs.get("ignore_irr_changes", False) 

103 ignore_peeringdb_changes: bool = kwargs.get("ignore_peeringdb_changes", False) 

104 use_cached: bool = kwargs.get("use_cached", False) 

105 

106 # Make sure we have the parameters we need 

107 if not plan_file: 

108 raise BirdPlanError("Required parameter 'plan_file' not found") 

109 

110 # Create search paths for Jinja2 

111 search_paths = [os.path.dirname(plan_file)] 

112 # We need to pass Jinja2 our filename, as it is in the search path 

113 plan_file_fname = os.path.basename(plan_file) 

114 

115 # Render first with jinja 

116 template_env = jinja2.Environment( # nosec 

117 loader=jinja2.FileSystemLoader(searchpath=search_paths), 

118 trim_blocks=True, 

119 lstrip_blocks=True, 

120 ) 

121 

122 # Check if we can load the configuration 

123 try: 

124 raw_config = template_env.get_template(plan_file_fname).render() 

125 except jinja2.TemplateError as err: 

126 raise BirdPlanError(f"Failed to template BirdPlan configuration file '{plan_file}': {err}") from None 

127 

128 # Load configuration using YAML 

129 try: 

130 self.config = self.yaml.load(raw_config) 

131 except YAMLError as err: # pragma: no cover 

132 raise BirdPlanError(f" Failed to parse BirdPlan configuration in '{plan_file}': {err}") from None 

133 

134 # Set our state file and load state 

135 self.state_file = state_file 

136 self.load_state() 

137 

138 # Make sure we have configuration... 

139 if not self.config: 

140 raise BirdPlanError("No configuration found") 

141 

142 # Check configuration options are supported 

143 for config_item in self.config: 

144 if config_item not in ("router_id", "kernel", "log_file", "debug", "static", "export_kernel", "bgp", "rip", "ospf"): 

145 raise BirdPlanError(f"The config item '{config_item}' is not supported") 

146 

147 # Setup globals we need 

148 self.birdconf.birdconfig_globals.ignore_irr_changes = ignore_irr_changes 

149 self.birdconf.birdconfig_globals.ignore_peeringdb_changes = ignore_peeringdb_changes 

150 self.birdconf.birdconfig_globals.use_cached = use_cached 

151 

152 # Configure sections 

153 self._config_global() 

154 self._config_kernel() 

155 self._config_static() 

156 self._config_export_kernel() 

157 self._config_rip() 

158 self._config_ospf() 

159 self._config_bgp() 

160 

161 def configure(self) -> str: 

162 """ 

163 Create BIRD configuration. 

164 

165 Returns 

166 ------- 

167 str : Bird configuration as a string. 

168 

169 """ 

170 return "\n".join(self.birdconf.get_config()) 

171 

172 def commit_state(self) -> None: 

173 """Commit our current state.""" 

174 

175 # Raise an exception if we don't have a state file loaded 

176 if self.state_file is None: 

177 raise BirdPlanError("Commit of BirdPlan state requires a state file, none loaded") 

178 

179 # Try get user and group ID's 

180 try: 

181 birdplan_uid = pwd.getpwnam("birdplan").pw_uid 

182 except KeyError: 

183 birdplan_uid = None 

184 try: 

185 birdplan_gid = grp.getgrnam("birdplan").gr_gid 

186 except KeyError: 

187 birdplan_gid = None 

188 

189 # Write out state file 

190 try: 

191 fd = os.open(self.state_file, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o640) 

192 # Chown the file if we have the user and group ID's 

193 if birdplan_uid and birdplan_gid: 

194 os.fchown(fd, birdplan_uid, birdplan_gid) 

195 # Open for writing 

196 with os.fdopen(fd, "w") as file: 

197 file.write(json.dumps(self.state)) 

198 except OSError as err: # pragma: no cover 

199 raise BirdPlanError(f"Failed to open '{self.state_file}' for writing: {err}") from None 

200 

201 def load_state(self) -> None: 

202 """Load our state.""" 

203 

204 # Clear state 

205 self.state = {} 

206 

207 # Skip if we don't have a state file 

208 if not self.state_file: 

209 return 

210 

211 # Check if the state file exists... 

212 if os.path.isfile(self.state_file): 

213 # Read in state file 

214 try: 

215 self.state = json.loads(pathlib.Path(self.state_file).read_text(encoding="UTF-8")) 

216 except OSError as err: 

217 raise BirdPlanError(f"Failed to read BirdPlan state file '{self.state_file}': {err}") from None 

218 except json.JSONDecodeError as err: # pragma: no cover 

219 # We use the state_file here because the size of raw_state may be larger than 100MiB 

220 raise BirdPlanError(f" Failed to parse BirdPlan state file '{self.state_file}': {err}") from None 

221 

222 def state_ospf_summary(self, bird_socket: Optional[str] = None) -> BirdPlanOSPFSummary: 

223 """ 

224 Return OSPF summary. 

225 

226 Returns 

227 ------- 

228 BirdPlanOSPFSummary 

229 Dictionary containing the OSPF summary. 

230 

231 eg. 

232 { 

233 'name1': { 

234 'channel': ..., 

235 'info': ..., 

236 'input_filter': ..., 

237 'name': ..., 

238 'output_filter': ..., 

239 'preference': ..., 

240 'proto': ..., 

241 'routes_exported': ..., 

242 'routes_imported': ..., 

243 'since': ..., 

244 'state': ... 

245 'table': ..., 

246 } 

247 'name2': { 

248 ..., 

249 } 

250 } 

251 

252 """ # noqa: RST201,RST203,RST301 

253 

254 # Raise an exception if we don't have a state file loaded 

255 if self.state_file is None: 

256 raise BirdPlanError("The use of OSPF summary requires a state file, none loaded") 

257 

258 # Initialize our return structure 

259 ret: BirdPlanOSPFSummary = {} 

260 

261 # Return if we don't have any BGP state 

262 if "ospf" not in self.state: 

263 return ret 

264 

265 # Query bird client for the current protocols 

266 birdc = birdclient.BirdClient(control_socket=bird_socket) 

267 bird_protocols = birdc.show_protocols() 

268 

269 for name, data in bird_protocols.items(): 

270 if data["proto"] != "OSPF": 

271 continue 

272 ret[name] = data 

273 

274 return ret 

275 

276 def state_bgp_peer_summary(self, bird_socket: Optional[str] = None) -> BirdPlanBGPPeerSummary: 

277 """ 

278 Return BGP peer summary. 

279 

280 Returns 

281 ------- 

282 BirdPlanBGPPeerStatus 

283 Dictionary containing the BGP peer summary. 

284 

285 eg. 

286 { 

287 'peer1': { 

288 'name': ..., 

289 'asn': ..., 

290 'description': ..., 

291 'protocols': { 

292 'ipv4': ..., 

293 'ipv6': ..., 

294 } 

295 } 

296 'peer2': { 

297 ..., 

298 } 

299 } 

300 

301 """ # noqa: RST201,RST203,RST301 

302 

303 # Raise an exception if we don't have a state file loaded 

304 if self.state_file is None: 

305 raise BirdPlanError("The use of BGP peer summary requires a state file, none loaded") 

306 

307 # Initialize our return structure 

308 ret: BirdPlanBGPPeerSummary = {} 

309 

310 # Return if we don't have any BGP state 

311 if "bgp" not in self.state: 

312 return ret 

313 

314 # Query bird client for the current protocols 

315 birdc = birdclient.BirdClient(control_socket=bird_socket) 

316 bird_protocols = birdc.show_protocols() 

317 

318 # Check if we have any peers in our state 

319 if "peers" in self.state["bgp"]: 

320 # If we do loop with them 

321 for peer, peer_state in self.state["bgp"]["peers"].items(): 

322 # Start with a clear status 

323 ret[peer] = { 

324 "name": peer, 

325 "asn": peer_state["asn"], 

326 "description": peer_state["description"], 

327 "protocols": peer_state["protocols"], 

328 } 

329 

330 # Next loop through each protocol 

331 for ipv, peer_state_protocol in peer_state["protocols"].items(): 

332 # If we don't have a live session, skip adding it 

333 if peer_state_protocol["name"] not in bird_protocols: 

334 continue 

335 # Set protocol name 

336 ret[peer]["protocols"][ipv]["protocol"] = ipv 

337 # But if we do, add it 

338 ret[peer]["protocols"][ipv]["status"] = bird_protocols[peer_state_protocol["name"]] 

339 

340 return ret 

341 

342 def state_bgp_peer_show(self, peer: str, bird_socket: Optional[str] = None) -> BirdPlanBGPPeerShow: 

343 """ 

344 Return the status of a specific BGP peer. 

345 

346 Returns 

347 ------- 

348 BirdPlanBGPPeerShow 

349 Dictionary containing the status of a BGP peer. 

350 

351 eg. 

352 { 

353 'asn': ..., 

354 'description': ..., 

355 'protocols': { 

356 'ipv4': { 

357 ..., 

358 'status': ..., 

359 } 

360 'ipv6': ..., 

361 }, 

362 } 

363 

364 """ # noqa: RST201,RST203,RST301 

365 

366 # Raise an exception if we don't have a state file loaded 

367 if self.state_file is None: 

368 raise BirdPlanError("The use of BGP peer show requires a state file, none loaded") 

369 

370 # Return if we don't have any BGP state 

371 if "bgp" not in self.state: 

372 raise BirdPlanError("No BGP state found") 

373 # Check if the configured state has this peer, if not return 

374 if peer not in self.state["bgp"]["peers"]: 

375 raise BirdPlanError(f"BGP peer '{peer}' not found in configured state") 

376 

377 # Make things easier below 

378 configured = self.state["bgp"]["peers"][peer] 

379 

380 # Set our peer info to the configured state 

381 ret: BirdPlanBGPPeerShow = configured 

382 

383 # Add peer name 

384 ret["name"] = peer 

385 

386 # Query bird client for the current protocols 

387 birdc = birdclient.BirdClient(control_socket=bird_socket) 

388 

389 # Loop with protocols and grab live bird status 

390 for ipv, protocol_info in configured["protocols"].items(): 

391 bird_state = birdc.show_protocol(protocol_info["name"]) 

392 # Skip if we have no bird state 

393 if not bird_state: 

394 continue 

395 # Set the protocol status 

396 ret["protocols"][ipv]["status"] = bird_state 

397 

398 return ret 

399 

400 def state_bgp_peer_graceful_shutdown_set(self, peer: str, value: bool) -> None: 

401 """ 

402 Set the BGP graceful shutdown override state for a peer. 

403 

404 Parameters 

405 ---------- 

406 peer : str 

407 Peer name to set to BGP graceful shutdown state for. 

408 Pattern matches can be specified with '*'. 

409 

410 value : bool 

411 State of the graceful shutdown option for this peer. 

412 

413 """ 

414 

415 # Raise an exception if we don't have a state file loaded 

416 if self.state_file is None: 

417 raise BirdPlanError("The use of BGP graceful shutdown override requires a state file, none loaded") 

418 

419 # Prepare the state structure if its not got what we need 

420 if "bgp" not in self.state: 

421 self.state["bgp"] = {} 

422 

423 # Make sure we have the global setting 

424 if "+graceful_shutdown" not in self.state["bgp"]: 

425 self.state["bgp"]["+graceful_shutdown"] = {} 

426 # Set the global setting for this pattern 

427 self.state["bgp"]["+graceful_shutdown"][peer] = value 

428 

429 def state_bgp_peer_graceful_shutdown_remove(self, peer: str) -> None: 

430 """ 

431 Remove a BGP graceful shutdown override flag from a peer or pattern. 

432 

433 Parameters 

434 ---------- 

435 peer : str 

436 Peer name or pattern to remove the BGP graceful shutdown override flag from. 

437 

438 """ 

439 

440 # Raise an exception if we don't have a state file loaded 

441 if self.state_file is None: 

442 raise BirdPlanError("The use of BGP graceful shutdown override requires a state file, none loaded") 

443 

444 # Prepare the state structure if its not got what we need 

445 if "bgp" not in self.state: 

446 return 

447 

448 # Remove from the global settings 

449 if "+graceful_shutdown" in self.state["bgp"]: 

450 # Check it exists first, if not raise an exception 

451 if peer not in self.state["bgp"]["+graceful_shutdown"]: 

452 raise BirdPlanError(f"BGP peer '{peer}' graceful shutdown override not found") 

453 # Remove peer from graceful shutdown list 

454 del self.state["bgp"]["+graceful_shutdown"][peer] 

455 # If the result is an empty dict, just delete it too 

456 if not self.state["bgp"]["+graceful_shutdown"]: 

457 del self.state["bgp"]["+graceful_shutdown"] 

458 

459 def state_bgp_peer_graceful_shutdown_status(self) -> BirdPlanBGPPeerGracefulShutdownStatus: 

460 """ 

461 Return the status of BGP peer graceful shutdown. 

462 

463 Returns 

464 ------- 

465 BirdPlanBGPPeerGracefulShutdownStatus 

466 Dictionary containing the status of overrides and peers. 

467 

468 eg. 

469 { 

470 'overrides': { 

471 'p*': True, 

472 'peer1': False, 

473 } 

474 'current': { 

475 'peer1': False, 

476 } 

477 'pending': { 

478 'peer1': False, 

479 } 

480 } 

481 

482 """ # noqa: RST201,RST203,RST301 

483 

484 # Raise an exception if we don't have a state file loaded 

485 if self.state_file is None: 

486 raise BirdPlanError("The use of BGP graceful shutdown override requires a state file, none loaded") 

487 

488 # Initialize our return structure 

489 ret: BirdPlanBGPPeerGracefulShutdownStatus = { 

490 "overrides": {}, 

491 "current": {}, 

492 "pending": {}, 

493 } 

494 

495 # Return if we don't have any BGP state 

496 if "bgp" not in self.state: 

497 return ret 

498 

499 # Pull in any overrides we may have 

500 if "+graceful_shutdown" in self.state["bgp"]: 

501 ret["overrides"] = self.state["bgp"]["+graceful_shutdown"] 

502 

503 # Check if we have any peers in our state 

504 if "peers" in self.state["bgp"]: 

505 # If we do loop with them 

506 for peer, peer_state in self.state["bgp"]["peers"].items(): 

507 # And check if they have a graceful shutdown state or not 

508 ret["current"][peer] = peer_state.get("graceful_shutdown", False) 

509 

510 # Generate the override status as if we were doing a configure 

511 for peer in self.birdconf.protocols.bgp.peers: 

512 ret["pending"][peer] = self.birdconf.protocols.bgp.peer(peer).graceful_shutdown 

513 

514 return ret 

515 

516 def state_bgp_peer_quarantine_set(self, peer: str, value: bool) -> None: 

517 """ 

518 Set the BGP quarantine override state for a peer. 

519 

520 Parameters 

521 ---------- 

522 peer : str 

523 Peer name to set to BGP quarantine state for. 

524 Pattern matches can be specified with '*'. 

525 

526 value : bool 

527 State of the quarantine option for this peer. 

528 

529 """ 

530 

531 # Raise an exception if we don't have a state file loaded 

532 if self.state_file is None: 

533 raise BirdPlanError("The use of BGP quarantine override requires a state file, none loaded") 

534 

535 # Prepare the state structure if its not got what we need 

536 if "bgp" not in self.state: 

537 self.state["bgp"] = {} 

538 

539 # Make sure we have the global setting 

540 if "+quarantine" not in self.state["bgp"]: 

541 self.state["bgp"]["+quarantine"] = {} 

542 # Set the global setting for this pattern 

543 self.state["bgp"]["+quarantine"][peer] = value 

544 

545 def state_bgp_peer_quarantine_remove(self, peer: str) -> None: 

546 """ 

547 Remove a BGP quarantine override flag from a peer or pattern. 

548 

549 Parameters 

550 ---------- 

551 peer : str 

552 Peer name or pattern to remove the BGP quarantine override flag from. 

553 

554 """ 

555 

556 # Raise an exception if we don't have a state file loaded 

557 if self.state_file is None: 

558 raise BirdPlanError("The use of BGP quarantine override requires a state file, none loaded") 

559 

560 # Prepare the state structure if its not got what we need 

561 if "bgp" not in self.state: 

562 return 

563 

564 # Remove from the global settings 

565 if ("+quarantine" not in self.state["bgp"]) or (peer not in self.state["bgp"]["+quarantine"]): 

566 raise BirdPlanError(f"BGP peer '{peer}' quarantine override not found") 

567 

568 # Remove peer from quarantine list 

569 del self.state["bgp"]["+quarantine"][peer] 

570 # If the result is an empty dict, just delete it too 

571 if not self.state["bgp"]["+quarantine"]: 

572 del self.state["bgp"]["+quarantine"] 

573 

574 def state_bgp_peer_quarantine_status(self) -> BirdPlanBGPPeerQuarantineStatus: 

575 """ 

576 Return the status of BGP peer quarantine. 

577 

578 Returns 

579 ------- 

580 BirdPlanBGPPeerQuarantineStatus 

581 Dictionary containing the status of overrides and peers. 

582 

583 eg. 

584 { 

585 'overrides': { 

586 'p*': True, 

587 'peer1': False, 

588 } 

589 'current': { 

590 'peer1': False, 

591 } 

592 'pending': { 

593 'peer1': False, 

594 } 

595 } 

596 

597 """ # noqa: RST201,RST203,RST301 

598 

599 # Raise an exception if we don't have a state file loaded 

600 if self.state_file is None: 

601 raise BirdPlanError("The use of BGP quarantine override requires a state file, none loaded") 

602 

603 # Initialize our return structure 

604 ret: BirdPlanBGPPeerQuarantineStatus = { 

605 "overrides": {}, 

606 "current": {}, 

607 "pending": {}, 

608 } 

609 

610 # Return if we don't have any BGP state 

611 if "bgp" not in self.state: 

612 return ret 

613 

614 # Pull in any overrides we may have 

615 if "+quarantine" in self.state["bgp"]: 

616 ret["overrides"] = self.state["bgp"]["+quarantine"] 

617 

618 # Check if we have any peers in our state 

619 if "peers" in self.state["bgp"]: 

620 # If we do loop with them 

621 for peer, peer_state in self.state["bgp"]["peers"].items(): 

622 # And check if they have a quarantine state or not 

623 ret["current"][peer] = peer_state.get("quarantine", False) 

624 

625 # Generate the override status as if we were doing a configure 

626 for peer in self.birdconf.protocols.bgp.peers: 

627 ret["pending"][peer] = self.birdconf.protocols.bgp.peer(peer).quarantine 

628 

629 return ret 

630 

631 def state_ospf_set_interface_cost(self, area: str, interface: str, cost: int) -> None: 

632 """ 

633 Set an OSPF interface cost override. 

634 

635 Parameters 

636 ---------- 

637 area : str 

638 Interface to set the OSPF cost for. 

639 

640 interface : str 

641 Interface to set the OSPF cost for. 

642 

643 cost : int 

644 OSPF interface cost. 

645 

646 """ 

647 

648 # Raise an exception if we don't have a state file loaded 

649 if self.state_file is None: 

650 raise BirdPlanError("The use of OSPF interface cost override requires a state file, none loaded") 

651 

652 # Prepare the state structure if its not got what we need 

653 if "ospf" not in self.state: 

654 self.state["ospf"] = {} 

655 if "areas" not in self.state["ospf"]: 

656 self.state["ospf"]["areas"] = {} 

657 if area not in self.state["ospf"]["areas"]: 

658 self.state["ospf"]["areas"][area] = {} 

659 if "+interfaces" not in self.state["ospf"]["areas"][area]: 

660 self.state["ospf"]["areas"][area]["+interfaces"] = {} 

661 if interface not in self.state["ospf"]["areas"][area]["+interfaces"]: 

662 self.state["ospf"]["areas"][area]["+interfaces"][interface] = {} 

663 

664 # Set the interface cost value 

665 self.state["ospf"]["areas"][area]["+interfaces"][interface]["cost"] = cost 

666 

667 def state_ospf_remove_interface_cost(self, area: str, interface: str) -> None: 

668 """ 

669 Remove an OSPF interface cost override. 

670 

671 Parameters 

672 ---------- 

673 area : str 

674 OSPF area which contains the interface. 

675 

676 interface : str 

677 Interface to remove the OSPF cost for. 

678 

679 """ 

680 

681 # Raise an exception if we don't have a state file loaded 

682 if self.state_file is None: 

683 raise BirdPlanError("The use of OSPF interface cost override requires a state file, none loaded") 

684 

685 # Check if this cost override exists 

686 if ( # pylint: disable=too-many-boolean-expressions 

687 "ospf" not in self.state 

688 or "areas" not in self.state["ospf"] 

689 or area not in self.state["ospf"]["areas"] 

690 or "+interfaces" not in self.state["ospf"]["areas"][area] 

691 or interface not in self.state["ospf"]["areas"][area]["+interfaces"] 

692 or "cost" not in self.state["ospf"]["areas"][area]["+interfaces"][interface] 

693 ): 

694 raise BirdPlanError(f"OSPF area '{area}' interface '{interface}' cost override not found") 

695 

696 # Remove OSPF interface cost from state 

697 del self.state["ospf"]["areas"][area]["+interfaces"][interface]["cost"] 

698 # Remove hanging data structure endpoint 

699 if not self.state["ospf"]["areas"][area]["+interfaces"][interface]: 

700 del self.state["ospf"]["areas"][area]["+interfaces"][interface] 

701 if not self.state["ospf"]["areas"][area]["+interfaces"]: 

702 del self.state["ospf"]["areas"][area]["+interfaces"] 

703 

704 def state_ospf_set_interface_ecmp_weight(self, area: str, interface: str, ecmp_weight: int) -> None: 

705 """ 

706 Set an OSPF interface ECMP weight override. 

707 

708 Parameters 

709 ---------- 

710 area : str 

711 OSPF area which contains the interface. 

712 

713 interface : str 

714 Interface to set the OSPF ECMP weight for. 

715 

716 ecmp_weight : int 

717 OSPF interface ECMP weight. 

718 

719 """ 

720 

721 # Raise an exception if we don't have a state file loaded 

722 if self.state_file is None: 

723 raise BirdPlanError("The use of OSPF interface ECMP weight override requires a state file, none loaded") 

724 

725 # Prepare the state structure if its not got what we need 

726 if "ospf" not in self.state: 

727 self.state["ospf"] = {} 

728 if "areas" not in self.state["ospf"]: 

729 self.state["ospf"]["areas"] = {} 

730 if area not in self.state["ospf"]["areas"]: 

731 self.state["ospf"]["areas"][area] = {} 

732 if "+interfaces" not in self.state["ospf"]["areas"][area]: 

733 self.state["ospf"]["areas"][area]["+interfaces"] = {} 

734 if interface not in self.state["ospf"]["areas"][area]["+interfaces"]: 

735 self.state["ospf"]["areas"][area]["+interfaces"][interface] = {} 

736 

737 # Set the interface ecmp_weight value 

738 self.state["ospf"]["areas"][area]["+interfaces"][interface]["ecmp_weight"] = ecmp_weight 

739 

740 def state_ospf_remove_interface_ecmp_weight(self, area: str, interface: str) -> None: 

741 """ 

742 Remove an OSPF interface ECMP weight override. 

743 

744 Parameters 

745 ---------- 

746 area : str 

747 OSPF area which contains the interface. 

748 

749 interface : str 

750 Interface to remove the OSPF ECMP weight for. 

751 

752 """ 

753 

754 # Raise an exception if we don't have a state file loaded 

755 if self.state_file is None: 

756 raise BirdPlanError("The use of OSPF interface ECMP weight override requires a state file, none loaded") 

757 

758 # Check if this ECMP weight override exists 

759 if ( # pylint: disable=too-many-boolean-expressions 

760 "ospf" not in self.state 

761 or "areas" not in self.state["ospf"] 

762 or area not in self.state["ospf"]["areas"] 

763 or "+interfaces" not in self.state["ospf"]["areas"][area] 

764 or interface not in self.state["ospf"]["areas"][area]["+interfaces"] 

765 or "ecmp_weight" not in self.state["ospf"]["areas"][area]["+interfaces"][interface] 

766 ): 

767 raise BirdPlanError(f"OSPF area '{area}' interface '{interface}' ECMP weight override not found") 

768 

769 # Remove OSPF interface ECMP weight from state 

770 del self.state["ospf"]["areas"][area]["+interfaces"][interface]["ecmp_weight"] 

771 # Remove hanging data structure endpoint 

772 if not self.state["ospf"]["areas"][area]["+interfaces"][interface]: 

773 del self.state["ospf"]["areas"][area]["+interfaces"][interface] 

774 if not self.state["ospf"]["areas"][area]["+interfaces"]: 

775 del self.state["ospf"]["areas"][area]["+interfaces"] 

776 

777 def state_ospf_interface_status(self) -> BirdPlanOSPFInterfaceStatus: # pylint: disable=too-many-branches 

778 """ 

779 Return the status of OSPF interfaces. 

780 

781 Returns 

782 ------- 

783 BirdPlanOSPFInterfaceStatus 

784 Dictionary containing the status of overrides and peers. 

785 

786 eg. 

787 { 

788 'overrides': { 

789 'areas': { 

790 '0': { 

791 'interfaces': { 

792 'eth0': { 

793 'cost': 10, 

794 'ecmp_weight': 100, 

795 } 

796 } 

797 } 

798 } 

799 }, 

800 'current': { 

801 'areas': { 

802 '0': { 

803 'interfaces': { 

804 'eth0': { 

805 'cost': 10, 

806 'ecmp_weight': 100, 

807 } 

808 } 

809 } 

810 } 

811 }, 

812 'pending': { 

813 'areas': { 

814 '0': { 

815 'interfaces': { 

816 'eth0': { 

817 'cost': 10, 

818 'ecmp_weight': 100, 

819 } 

820 } 

821 } 

822 } 

823 } 

824 } 

825 

826 """ # noqa: RST201,RST203,RST301 

827 

828 # Raise an exception if we don't have a state file loaded 

829 if self.state_file is None: 

830 raise BirdPlanError("The use of OSPF interface override requires a state file, none loaded") 

831 

832 # Initialize our return structure 

833 ret: BirdPlanOSPFInterfaceStatus = { 

834 "overrides": {}, 

835 "current": {}, 

836 "pending": {}, 

837 } 

838 

839 # Return if we don't have any OSPF state 

840 if "ospf" not in self.state or "areas" not in self.state["ospf"]: 

841 return ret 

842 

843 # Process overrides 

844 for area_name, area in self.state["ospf"]["areas"].items(): 

845 # Make sure we have interfaces in the area 

846 if "+interfaces" not in area: 

847 continue 

848 # Loop with interfaces 

849 for interface_name, interface in area["+interfaces"].items(): 

850 # Check our structure is setup 

851 if "areas" not in ret["overrides"]: 

852 ret["overrides"]["areas"] = {} 

853 if area_name not in ret["overrides"]["areas"]: 

854 ret["overrides"]["areas"][area_name] = {} 

855 if "interfaces" not in ret["overrides"]["areas"][area_name]: 

856 ret["overrides"]["areas"][area_name]["interfaces"] = {} 

857 # Link interface 

858 ret["overrides"]["areas"][area_name]["interfaces"][interface_name] = interface 

859 

860 # Process current state 

861 for area_name, area in self.state["ospf"]["areas"].items(): 

862 # Make sure we have interfaces in the area 

863 if "interfaces" not in area: 

864 continue 

865 # Loop with interfaces 

866 for interface_name, interface in area["interfaces"].items(): 

867 # Check our structure is setup 

868 if "areas" not in ret["current"]: 

869 ret["current"]["areas"] = {} 

870 if area_name not in ret["current"]["areas"]: 

871 ret["current"]["areas"][area_name] = {} 

872 if "interfaces" not in ret["current"]["areas"][area_name]: 

873 ret["current"]["areas"][area_name]["interfaces"] = {} 

874 # Link interface 

875 ret["current"]["areas"][area_name]["interfaces"][interface_name] = interface 

876 

877 # Generate the override status as if we were doing a configure 

878 for area_name, area in self.birdconf.protocols.ospf.areas.items(): 

879 for interface_name, interface in area.interfaces.items(): 

880 # Check our structure is setup 

881 if "areas" not in ret["pending"]: 

882 ret["pending"]["areas"] = {} 

883 if area_name not in ret["pending"]["areas"]: 

884 ret["pending"]["areas"][area_name] = {} 

885 if "interfaces" not in ret["pending"]["areas"][area_name]: 

886 ret["pending"]["areas"][area_name]["interfaces"] = {} 

887 if interface_name not in ret["pending"]["areas"][area_name]["interfaces"]: 

888 ret["pending"]["areas"][area_name]["interfaces"][interface_name] = {} 

889 # Add attributes we need 

890 ret["pending"]["areas"][area_name]["interfaces"][interface_name]["cost"] = interface.cost 

891 ret["pending"]["areas"][area_name]["interfaces"][interface_name]["ecmp_weight"] = interface.ecmp_weight 

892 

893 return ret 

894 

895 def _config_global(self) -> None: 

896 """Configure global options.""" 

897 

898 # Check that a router ID was specified 

899 if "router_id" not in self.config: 

900 raise BirdPlanError("The 'router_id' attribute must be specified") 

901 self.birdconf.router_id = self.config["router_id"] 

902 

903 # Check if we have a log_file specified to use 

904 if "log_file" in self.config: 

905 self.birdconf.log_file = self.config["log_file"] 

906 

907 # Check if we're in debugging mode or not 

908 if "debug" in self.config: 

909 self.birdconf.debug = self.config["debug"] 

910 

911 def _config_kernel(self) -> None: 

912 """Configure kernel section.""" 

913 

914 # If we have no rip section, just return 

915 if "kernel" not in self.config: 

916 return 

917 

918 # Check configuration options are supported 

919 for config_item in self.config["kernel"]: 

920 if config_item not in ("vrf", "routing_table"): 

921 raise BirdPlanError(f"The 'kernel' config item '{config_item}' is not supported") 

922 

923 # Check if we have a VRF to use 

924 if "vrf" in self.config["kernel"]: 

925 self.birdconf.vrf = '"' + self.config["kernel"]["vrf"] + '"' 

926 # Make sure we also have a routing talbe 

927 if "routing_table" not in self.config["kernel"]: 

928 raise BirdPlanError("The 'kernel' config item 'vrf' requires that 'routing_table' is also specified") 

929 

930 if "routing_table" in self.config["kernel"]: 

931 self.birdconf.routing_table = self.config["kernel"]["routing_table"] 

932 

933 def _config_static(self) -> None: 

934 """Configure static section.""" 

935 # Static routes 

936 if "static" in self.config: 

937 for route in self.config["static"]: 

938 self.birdconf.protocols.static.add_route(route) 

939 

940 def _config_export_kernel(self) -> None: 

941 """Configure export_kernel section.""" 

942 

943 # Check if we're exporting routes from the master tables to the kernel tables 

944 if "export_kernel" in self.config: 

945 # Loop with export_kernel items 

946 for export, export_config in self.config["export_kernel"].items(): 

947 # Static routes 

948 if export == "static": 

949 self.birdconf.tables.master.route_policy_export.kernel.static = export_config 

950 # RIP routes 

951 elif export == "rip": 

952 self.birdconf.tables.master.route_policy_export.kernel.rip = export_config 

953 # OSPF routes 

954 elif export == "ospf": 

955 self.birdconf.tables.master.route_policy_export.kernel.ospf = export_config 

956 # BGP routes 

957 elif export == "bgp": 

958 self.birdconf.tables.master.route_policy_export.kernel.bgp = export_config 

959 # If we don't understand this 'accept' entry, throw an error 

960 else: 

961 raise BirdPlanError(f"Configuration item '{export}' not understood in 'export_kernel'") 

962 

963 def _config_rip(self) -> None: 

964 """Configure rip section.""" 

965 

966 # If we have no rip section, just return 

967 if "rip" not in self.config: 

968 return 

969 

970 # Check configuration options are supported 

971 for config_item in self.config["rip"]: 

972 if config_item not in ("accept", "redistribute", "interfaces"): 

973 raise BirdPlanError(f"The 'rip' config item '{config_item}' is not supported") 

974 

975 self._config_rip_accept() 

976 self._config_rip_redistribute() 

977 self._config_rip_interfaces() 

978 

979 def _config_rip_accept(self) -> None: 

980 """Configure rip:accept section.""" 

981 

982 # If we don't have an accept section, just return 

983 if "accept" not in self.config["rip"]: 

984 return 

985 

986 # Loop with accept items 

987 for accept, accept_config in self.config["rip"]["accept"].items(): 

988 # Allow accept of the default route 

989 if accept == "default": 

990 self.birdconf.protocols.rip.route_policy_accept.default = accept_config 

991 # If we don't understand this 'accept' entry, throw an error 

992 else: 

993 raise BirdPlanError(f"Configuration item '{accept}' not understood in RIP accept") 

994 

995 def _config_rip_redistribute(self) -> None: # pylint: disable=too-many-branches 

996 """Configure rip:redistribute section.""" 

997 

998 # If we don't have a redistribute section just return 

999 if "redistribute" not in self.config["rip"]: 

1000 return 

1001 

1002 # Loop with redistribution items 

1003 for redistribute, redistribute_config in self.config["rip"]["redistribute"].items(): 

1004 # Add connected route redistribution 

1005 if redistribute == "connected": 

1006 # Set type 

1007 redistribute_connected: Union[bool, List[str]] 

1008 # Check what kind of config we go... 

1009 if isinstance(redistribute_config, bool): 

1010 redistribute_connected = redistribute_config 

1011 # Else if its a dict, we need to treat it a bit differently 

1012 elif isinstance(redistribute_config, dict): 

1013 # Check it has an "interfaces" key 

1014 if "interfaces" not in redistribute_config: 

1015 raise BirdPlanError(f"Configurion item '{redistribute}' has no 'interfaces' option in rip:redistribute") 

1016 # If it does, check that it is a list 

1017 if not isinstance(redistribute_config["interfaces"], list): 

1018 raise BirdPlanError(f"Configurion item '{redistribute}:interfaces' has an invalid type in rip:redistribute") 

1019 # Set redistribute_connected as the interface list 

1020 redistribute_connected = redistribute_config["interfaces"] 

1021 else: 

1022 raise BirdPlanError(f"Configurion item '{redistribute}' has an unsupported value") 

1023 # Add configuration 

1024 self.birdconf.protocols.rip.route_policy_redistribute.connected = redistribute_connected 

1025 # Add kernel route redistribution 

1026 elif redistribute == "kernel": 

1027 self.birdconf.protocols.rip.route_policy_redistribute.kernel = redistribute_config 

1028 # Add kernel default route redistribution 

1029 elif redistribute == "kernel_default": 

1030 self.birdconf.protocols.rip.route_policy_redistribute.kernel_default = redistribute_config 

1031 # Allow redistribution of RIP routes 

1032 elif redistribute == "rip": 

1033 self.birdconf.protocols.rip.route_policy_redistribute.rip = redistribute_config 

1034 # Allow redistribution of RIP default routes 

1035 elif redistribute == "rip_default": 

1036 self.birdconf.protocols.rip.route_policy_redistribute.rip_default = redistribute_config 

1037 # Add static route redistribution 

1038 elif redistribute == "static": 

1039 self.birdconf.protocols.rip.route_policy_redistribute.static = redistribute_config 

1040 # Add static default route redistribution 

1041 elif redistribute == "static_default": 

1042 self.birdconf.protocols.rip.route_policy_redistribute.static_default = redistribute_config 

1043 # If we don't understand this 'redistribute' entry, throw an error 

1044 else: 

1045 raise BirdPlanError(f"Configuration item '{redistribute}' not understood in rip:redistribute") 

1046 

1047 def _config_rip_interfaces(self) -> None: 

1048 """Configure rip:interfaces section.""" 

1049 

1050 # If we don't have interfaces in our rip section, just return 

1051 if "interfaces" not in self.config["rip"]: 

1052 return 

1053 

1054 # Loop with each interface and its config 

1055 for interface_name, interface in self.config["rip"]["interfaces"].items(): 

1056 # See if we have interface config 

1057 interface_config = {} 

1058 # Loop with each config item in the peer 

1059 for config_item, config_value in interface.items(): 

1060 if config_item in ("update-time", "metric"): 

1061 interface_config[config_item] = config_value 

1062 # If we don't understand this 'redistribute' entry, throw an error 

1063 else: 

1064 raise BirdPlanError(f"Configuration item '{config_item}' not understood in RIP area") 

1065 # Add interface 

1066 self.birdconf.protocols.rip.add_interface(interface_name, interface_config) 

1067 

1068 def _config_ospf(self) -> None: 

1069 """Configure OSPF section.""" 

1070 

1071 # If we have no ospf section, just return 

1072 if "ospf" not in self.config: 

1073 return 

1074 

1075 # Check configuration options are supported 

1076 for config_item in self.config["ospf"]: 

1077 if config_item not in ("accept", "redistribute", "areas", "v4version"): 

1078 raise BirdPlanError(f"The 'ospf' config item '{config_item}' is not supported") 

1079 

1080 # Check what version of OSPF we're using for IPv4 

1081 if "v4version" in self.config["ospf"]: 

1082 if isinstance(self.config["ospf"]["v4version"], (int, str)): 

1083 v4version = f"{self.config['ospf']['v4version']}" 

1084 if v4version in ("2", "3"): 

1085 self.birdconf.protocols.ospf.v4version = v4version 

1086 else: 

1087 raise BirdPlanError("The 'ospf' config item 'v4version' has unsupported value") 

1088 else: 

1089 raise BirdPlanError("The 'ospf' config item 'v4version' has unsupported type") 

1090 

1091 self._config_ospf_accept() 

1092 self._config_ospf_redistribute() 

1093 self._config_ospf_areas() 

1094 

1095 def _config_ospf_accept(self) -> None: 

1096 """Configure ospf:accept section.""" 

1097 

1098 # If we don't have an accept section, just return 

1099 if "accept" not in self.config["ospf"]: 

1100 return 

1101 

1102 # Loop with accept items 

1103 for accept, accept_config in self.config["ospf"]["accept"].items(): 

1104 # Allow accept of the default route 

1105 if accept == "default": 

1106 self.birdconf.protocols.ospf.route_policy_accept.default = accept_config 

1107 # If we don't understand this 'accept' entry, throw an error 

1108 else: 

1109 raise BirdPlanError(f"Configuration item '{accept}' not understood in ospf:accept") 

1110 

1111 def _config_ospf_redistribute(self) -> None: # pylint: disable=too-many-branches 

1112 """Configure ospf:redistribute section.""" 

1113 

1114 # If we don't have a redistribute section just return 

1115 if "redistribute" not in self.config["ospf"]: 

1116 return 

1117 

1118 # Loop with redistribution items 

1119 for redistribute, redistribute_config in self.config["ospf"]["redistribute"].items(): 

1120 # Add static route redistribution 

1121 if redistribute == "static": 

1122 self.birdconf.protocols.ospf.route_policy_redistribute.static = redistribute_config 

1123 # Add connected route redistribution 

1124 elif redistribute == "connected": 

1125 # Set type 

1126 redistribute_connected: Union[bool, List[str]] 

1127 # Check what kind of config we go... 

1128 if isinstance(redistribute_config, bool): 

1129 redistribute_connected = redistribute_config 

1130 # Else if its a dict, we need to treat it a bit differently 

1131 elif isinstance(redistribute_config, dict): 

1132 # Check it has an "interfaces" key 

1133 if "interfaces" not in redistribute_config: 

1134 raise BirdPlanError(f"Configurion item '{redistribute}' has no 'interfaces' option in ospf:redistribute") 

1135 # If it does, check that it is a list 

1136 if not isinstance(redistribute_config["interfaces"], list): 

1137 raise BirdPlanError( 

1138 f"Configurion item '{redistribute}:interfaces' has an invalid type in ospf:redistribute" 

1139 ) 

1140 # Set redistribute_connected as the interface list 

1141 redistribute_connected = redistribute_config["interfaces"] 

1142 else: 

1143 raise BirdPlanError(f"Configurion item '{redistribute}' has an unsupported value") 

1144 # Add configuration 

1145 self.birdconf.protocols.ospf.route_policy_redistribute.connected = redistribute_connected 

1146 # Add kernel route redistribution 

1147 elif redistribute == "kernel": 

1148 self.birdconf.protocols.ospf.route_policy_redistribute.kernel = redistribute_config 

1149 # Add kernel default route redistribution 

1150 elif redistribute == "kernel_default": 

1151 self.birdconf.protocols.ospf.route_policy_redistribute.kernel_default = redistribute_config 

1152 # Add static route redistribution 

1153 elif redistribute == "static": 

1154 self.birdconf.protocols.ospf.route_policy_redistribute.static = redistribute_config 

1155 # Add static default route redistribution 

1156 elif redistribute == "static_default": 

1157 self.birdconf.protocols.ospf.route_policy_redistribute.static_default = redistribute_config 

1158 # If we don't understand this 'redistribute' entry, throw an error 

1159 else: 

1160 raise BirdPlanError(f"Configuration item '{redistribute}' not understood in ospf:redistribute") 

1161 

1162 def _config_ospf_areas(self) -> None: # pylint: disable=too-many-branches 

1163 """Configure ospf:interfaces section.""" 

1164 

1165 # If we don't have areas in our ospf section, just return 

1166 if "areas" not in self.config["ospf"]: 

1167 return 

1168 

1169 # Loop with each area and its config 

1170 for area_name, raw_area_config in self.config["ospf"]["areas"].items(): 

1171 # Make sure we have an interface for the area 

1172 if "interfaces" not in raw_area_config: 

1173 raise BirdPlanError(f"OSPF area '{area_name}' must contain 'interfaces'") 

1174 # Loop with each config item 

1175 area_config = {} 

1176 for config_item, raw_config in raw_area_config.items(): 

1177 # Make sure this item is supported 

1178 if config_item not in ("xxxxxxx", "interfaces"): 

1179 raise BirdPlanError( 

1180 f"Configuration item '{config_item}' with value '{raw_config}' not understood in ospf:areas" 

1181 ) 

1182 # Skip over interfaces 

1183 if config_item == "interfaces": 

1184 continue 

1185 # Check for supported config options 

1186 if config_item in ("xxxxx", "yyyy"): 

1187 area_config[config_item] = raw_config 

1188 # If we don't understand this 'redistribute' entry, throw an error 

1189 else: 

1190 raise BirdPlanError(f"Configuration item '{config_item}' not understood in ospf:areas") 

1191 

1192 # Add area 

1193 area = self.birdconf.protocols.ospf.add_area(area_name, area_config) 

1194 

1195 # Loop with interfaces in area 

1196 for interface_name, raw_config in raw_area_config["interfaces"].items(): 

1197 # Start with no special interface configuration 

1198 interface_config: Dict[str, Any] = {} 

1199 # Check what kind of config we've got... 

1200 add_ospf_interface = False 

1201 if isinstance(raw_config, bool): 

1202 add_ospf_interface = raw_config 

1203 # Else if its a dict, we need to treat it a bit differently 

1204 elif isinstance(raw_config, dict): 

1205 add_ospf_interface = True 

1206 for raw_item, raw_value in raw_config.items(): 

1207 if raw_item in ("cost", "ecmp_weight", "hello", "stub", "wait"): 

1208 interface_config[raw_item] = raw_value 

1209 else: 

1210 raise BirdPlanError( 

1211 f"Configuration item '{raw_item}' not understood in OSPF interface '{interface_name}'" 

1212 ) 

1213 else: 

1214 raise BirdPlanError( 

1215 f"Configurion for OSPF interface name '{interface_name}' has an unsupported type: '{type(interface_name)}'" 

1216 ) 

1217 

1218 # Add interface to area 

1219 if add_ospf_interface: 

1220 area.add_interface(interface_name, interface_config) 

1221 

1222 def _config_bgp(self) -> None: 

1223 """Configure bgp section.""" 

1224 

1225 # If we have no rip section, just return 

1226 if "bgp" not in self.config: 

1227 return 

1228 

1229 # Set our ASN 

1230 if "asn" not in self.config["bgp"]: 

1231 raise BirdPlanError('BGP configuration must have an "asn" item defined') 

1232 self.birdconf.protocols.bgp.asn = self.config["bgp"]["asn"] 

1233 

1234 # Check configuration options are supported 

1235 for config_item in self.config["bgp"]: 

1236 if config_item not in ( 

1237 # Globals 

1238 "accept", 

1239 "asn", 

1240 "graceful_shutdown", 

1241 "import", 

1242 "originate", # Origination 

1243 "peers", 

1244 "peertype_constraints", 

1245 "quarantine", 

1246 "rr_cluster_id", 

1247 ): 

1248 raise BirdPlanError(f"The 'bgp' config item '{config_item}' is not supported") 

1249 

1250 self._config_bgp_accept() 

1251 self._config_bgp_globals() 

1252 self._config_bgp_peertype_constraints() 

1253 self._config_bgp_originate() 

1254 

1255 self._config_bgp_import() 

1256 self._config_bgp_peers() 

1257 

1258 def _config_bgp_accept(self) -> None: 

1259 """Configure bgp:accept section.""" 

1260 

1261 # If we don't have an accept section, just return 

1262 if "accept" not in self.config["bgp"]: 

1263 return 

1264 

1265 # Loop with accept items 

1266 for accept, accept_config in self.config["bgp"]["accept"].items(): 

1267 # Check if we need to accept some kinds of routes 

1268 if accept in ( 

1269 "bgp_customer_blackhole", 

1270 "bgp_own_blackhole", 

1271 "bgp_own_default", 

1272 "bgp_transit_default", 

1273 "originated", 

1274 "originated_default", 

1275 ): 

1276 setattr(self.birdconf.protocols.bgp.route_policy_accept, accept, accept_config) 

1277 # If we don't understand this 'accept' entry, throw an error 

1278 else: 

1279 raise BirdPlanError(f"Configuration item '{accept}' not understood in bgp:accept") 

1280 

1281 def _config_bgp_globals(self) -> None: 

1282 """Configure bgp globals.""" 

1283 

1284 # Setup graceful shutdown if specified 

1285 if "graceful_shutdown" in self.config["bgp"]: 

1286 self.birdconf.protocols.bgp.graceful_shutdown = self.config["bgp"]["graceful_shutdown"] 

1287 

1288 # Setup graceful shutdown if specified 

1289 if "quarantine" in self.config["bgp"]: 

1290 self.birdconf.protocols.bgp.quarantine = self.config["bgp"]["quarantine"] 

1291 

1292 # Set our route reflector cluster id 

1293 if "rr_cluster_id" in self.config["bgp"]: 

1294 self.birdconf.protocols.bgp.rr_cluster_id = self.config["bgp"]["rr_cluster_id"] 

1295 

1296 def _config_bgp_peertype_constraints(self) -> None: 

1297 """Configure bgp:peertype_constraints section.""" 

1298 

1299 # If we don't have a peertype_constraints section, just return 

1300 if "peertype_constraints" not in self.config["bgp"]: 

1301 return 

1302 

1303 for peer_type in self.config["bgp"]["peertype_constraints"]: 

1304 # Make sure we have a valid peer type 

1305 if peer_type not in ( 

1306 "customer", 

1307 "customer.private", 

1308 "internal", 

1309 "peer", 

1310 "routecollector", 

1311 "routeserver", 

1312 "rrclient", 

1313 "rrserver", 

1314 "rrserver-rrserver", 

1315 "transit", 

1316 ): 

1317 raise BirdPlanError(f"The 'bgp:peertype_constraints' config item '{peer_type}' is not supported") 

1318 # Loop with constraint items 

1319 for constraint_name in self.config["bgp"]["peertype_constraints"][peer_type]: 

1320 # Make sure we have a valid constraint to set 

1321 if constraint_name not in ( 

1322 "blackhole_import_maxlen4", 

1323 "blackhole_import_minlen4", 

1324 "blackhole_export_maxlen4", 

1325 "blackhole_export_minlen4", 

1326 "blackhole_import_maxlen6", 

1327 "blackhole_import_minlen6", 

1328 "blackhole_export_maxlen6", 

1329 "blackhole_export_minlen6", 

1330 "import_maxlen4", 

1331 "export_maxlen4", 

1332 "import_minlen4", 

1333 "export_minlen4", 

1334 "import_maxlen6", 

1335 "export_maxlen6", 

1336 "import_minlen6", 

1337 "export_minlen6", 

1338 "aspath_import_maxlen", 

1339 "aspath_import_minlen", 

1340 "community_import_maxlen", 

1341 "extended_community_import_maxlen", 

1342 "large_community_import_maxlen", 

1343 ): 

1344 raise BirdPlanError( 

1345 f"The 'bgp:peertype_constraints:{peer_type}' config item '{constraint_name}' is not supported" 

1346 ) 

1347 # Make sure this peer supports blackhole imports 

1348 if constraint_name.startswith("blackhole_import_"): # noqa: SIM102 

1349 if peer_type not in ( 

1350 "customer", 

1351 "internal", 

1352 "rrclient", 

1353 "rrserver", 

1354 "rrserver-rrserver", 

1355 ): 

1356 raise BirdPlanError( 

1357 f"Having 'peertype_constraints:{constraint_name}' specified for peer type '{peer_type}' " 

1358 "makes no sense" 

1359 ) 

1360 # Make sure this peer accepts blackhole exports 

1361 if constraint_name.startswith("blackhole_export_"): # noqa: SIM102 

1362 if peer_type not in ( 

1363 "internal", 

1364 "routeserver", 

1365 "routecollector", 

1366 "rrclient", 

1367 "rrserver", 

1368 "rrserver-rrserver", 

1369 "transit", 

1370 ): 

1371 raise BirdPlanError( 

1372 f"Having 'peertype_constraints:{constraint_name}' specified for peer type '{peer_type}' " 

1373 "makes no sense" 

1374 ) 

1375 # Make sure this peer supports imports 

1376 if "import" in constraint_name: # noqa: SIM102 

1377 if peer_type not in ( 

1378 "customer", 

1379 "customer.private", 

1380 "internal", 

1381 "peer", 

1382 "routeserver", 

1383 "rrclient", 

1384 "rrserver", 

1385 "rrserver-rrserver", 

1386 "transit", 

1387 ): 

1388 raise BirdPlanError( 

1389 f"Having 'peertype_constraints:{constraint_name}' specified for peer type '{peer_type}' " 

1390 "makes no sense" 

1391 ) 

1392 # Finally set the constraint item 

1393 setattr( 

1394 self.birdconf.protocols.bgp.constraints(peer_type), 

1395 constraint_name, 

1396 self.config["bgp"]["peertype_constraints"][peer_type][constraint_name], 

1397 ) 

1398 

1399 def _config_bgp_originate(self) -> None: 

1400 """Configure bgp:originate section.""" 

1401 

1402 # If we don't have an accept section, just return 

1403 if "originate" not in self.config["bgp"]: 

1404 return 

1405 

1406 # Add origination routes 

1407 for route in self.config["bgp"]["originate"]: 

1408 self.birdconf.protocols.bgp.add_originated_route(route) 

1409 

1410 def _config_bgp_import(self) -> None: # pylint: disable=too-many-branches 

1411 """Configure bgp:import section.""" 

1412 

1413 # If we don't have the option then just return 

1414 if "import" not in self.config["bgp"]: 

1415 return 

1416 

1417 # Loop with redistribution items 

1418 for import_type, import_config in self.config["bgp"]["import"].items(): 

1419 # Import connected routes into the main BGP table 

1420 if import_type == "connected": 

1421 # Set type 

1422 import_connected: Union[bool, List[str]] 

1423 # Check what kind of config we go... 

1424 if isinstance(import_config, bool): 

1425 import_connected = import_config 

1426 # Else if its a dict, we need to treat it a bit differently 

1427 elif isinstance(import_config, dict): 

1428 # Check it has an "interfaces" key 

1429 if "interfaces" not in import_config: 

1430 raise BirdPlanError(f"Configurion item '{import_type}' has no 'interfaces' option in bgp:import") 

1431 # If it does, check that it is a list 

1432 if not isinstance(import_config["interfaces"], list): 

1433 raise BirdPlanError(f"Configurion item '{import_type}:interfaces' has an invalid type in bgp:import") 

1434 # Set import_connected as the interface list 

1435 import_connected = import_config["interfaces"] 

1436 else: 

1437 raise BirdPlanError(f"Configurion item '{import_type}' has an unsupported value") 

1438 # Add configuration 

1439 self.birdconf.protocols.bgp.route_policy_import.connected = import_connected 

1440 

1441 # Import kernel routes into the main BGP table 

1442 elif import_type == "kernel": 

1443 self.birdconf.protocols.bgp.route_policy_import.kernel = import_config 

1444 # Import kernel blackhole routes into the main BGP table 

1445 elif import_type == "kernel_blackhole": 

1446 self.birdconf.protocols.bgp.route_policy_import.kernel_blackhole = import_config 

1447 # Import kernel default routes into the main BGP table 

1448 elif import_type == "kernel_default": 

1449 self.birdconf.protocols.bgp.route_policy_import.kernel_default = import_config 

1450 

1451 # Import static routes into the main BGP table 

1452 elif import_type == "static": 

1453 self.birdconf.protocols.bgp.route_policy_import.static = import_config 

1454 # Import static blackhole routes into the main BGP table 

1455 elif import_type == "static_blackhole": 

1456 self.birdconf.protocols.bgp.route_policy_import.static_blackhole = import_config 

1457 # Import static default routes into the main BGP table 

1458 elif import_type == "static_default": 

1459 self.birdconf.protocols.bgp.route_policy_import.static_default = import_config 

1460 

1461 # If we don't understand this 'redistribute' entry, throw an error 

1462 else: 

1463 raise BirdPlanError(f"Configuration item '{import_type}' not understood in bgp:import") 

1464 

1465 def _config_bgp_peers(self) -> None: 

1466 """Configure bgp:peers section.""" 

1467 

1468 if "peers" not in self.config["bgp"]: 

1469 return 

1470 

1471 # Loop with peer ASN and config 

1472 peer_count = len(self.config["bgp"]["peers"]) 

1473 peer_cur: int = 1 

1474 for peer_name, peer_config in self.config["bgp"]["peers"].items(): 

1475 # Make sure peer name is valid 

1476 if not re.match(r"^[a-z0-9]+$", peer_name): 

1477 raise BirdPlanError(f"The peer name '{peer_name}' specified in 'bgp:peers' is not valid, use [a-z0-9]") 

1478 

1479 # Log completion 

1480 if not self.birdconf.birdconfig_globals.suppress_info: 

1481 percentage_complete = (peer_cur / peer_count) * 100 

1482 logging.info( 

1483 colored("Processing BGP peer %s/%s (%.2f%%): %s", "blue"), peer_cur, peer_count, percentage_complete, peer_name 

1484 ) 

1485 

1486 # Configure peer 

1487 self._config_bgp_peers_peer(peer_name, peer_config) 

1488 

1489 # Bump current peer 

1490 peer_cur += 1 

1491 

1492 def _config_bgp_peers_peer( # noqa: CFQ001 # pylint: disable=too-many-branches,too-many-locals,too-many-statements 

1493 self, peer_name: str, peer_config: Dict[str, Any] 

1494 ) -> None: 

1495 """Configure bgp:peers single peer.""" 

1496 

1497 # Start with no peer config 

1498 peer = {} 

1499 

1500 # Loop with each config item in the peer 

1501 for config_item, config_value in peer_config.items(): 

1502 if config_item in ( 

1503 "asn", 

1504 "description", 

1505 "type", 

1506 "neighbor4", 

1507 "neighbor6", 

1508 "source_address4", 

1509 "source_address6", 

1510 "connect_retry_time", 

1511 "connect_delay_time", 

1512 "error_wait_time", 

1513 "multihop", 

1514 "passive", 

1515 "password", 

1516 "ttl_security", 

1517 "prefix_limit_action", 

1518 "prefix_limit4", 

1519 "prefix_limit6", 

1520 "quarantine", 

1521 "replace_aspath", 

1522 "incoming_large_communities", 

1523 "cost", 

1524 "graceful_shutdown", 

1525 "blackhole_community", 

1526 ): 

1527 peer[config_item] = config_value 

1528 # Peer add_paths configuration 

1529 elif config_item == "add_paths": 

1530 peer["add_paths"] = None 

1531 if isinstance(config_value, bool): 

1532 if config_value: 

1533 peer["add_paths"] = "on" 

1534 elif isinstance(config_value, str): 

1535 if config_value not in ("tx", "rx", "on"): 

1536 raise BirdPlanError( 

1537 f"Configuration value '{config_value}' not understood in bgp:peers:{peer_name}:add_paths, valid values" 

1538 "are 'tx', 'rx', 'on'" 

1539 ) 

1540 peer["add_paths"] = config_value 

1541 else: 

1542 raise BirdPlanError( 

1543 f"Configuration item has invalid type '{type(config_value)}' in bgp:peers:{peer_name}:add_paths" 

1544 ) 

1545 # Peer location configuration 

1546 elif config_item == "location": 

1547 peer["location"] = {} 

1548 # Loop with location configuration items 

1549 for location_type, location_config in config_value.items(): 

1550 if location_type not in ("iso3166", "unm49"): 

1551 raise BirdPlanError( 

1552 f"Configuration item '{location_type}' not understood in bgp:peers:{peer_name}:location" 

1553 ) 

1554 peer["location"][location_type] = location_config 

1555 # Work out redistribution 

1556 elif config_item == "redistribute": 

1557 peer["redistribute"] = {} 

1558 # Loop with redistribution items 

1559 for redistribute_type, redistribute_config in config_value.items(): 

1560 if redistribute_type not in ( 

1561 "connected", 

1562 "kernel", 

1563 "kernel_blackhole", 

1564 "kernel_default", 

1565 "static", 

1566 "static_blackhole", 

1567 "static_default", 

1568 "originated", 

1569 "originated_default", 

1570 "bgp", 

1571 "bgp_customer", 

1572 "bgp_customer_blackhole", 

1573 "bgp_own", 

1574 "bgp_own_blackhole", 

1575 "bgp_own_default", 

1576 "bgp_peering", 

1577 "bgp_transit", 

1578 "bgp_transit_default", 

1579 ): 

1580 raise BirdPlanError( 

1581 f"Configuration item '{redistribute_type}' not understood in bgp:peers:{peer_name} redistribute" 

1582 ) 

1583 peer["redistribute"][redistribute_type] = redistribute_config 

1584 

1585 # Work out acceptance of routes 

1586 elif config_item == "accept": 

1587 peer["accept"] = {} 

1588 # Loop with acceptance items 

1589 for accept, accept_config in config_value.items(): 

1590 if accept not in ( 

1591 "bgp_customer_blackhole", 

1592 "bgp_own_blackhole", 

1593 "bgp_own_default", 

1594 "bgp_transit_default", 

1595 ): 

1596 raise BirdPlanError(f"Configuration item '{accept}' not understood in bgp:peers:{peer_name}:accept") 

1597 peer["accept"][accept] = accept_config 

1598 # Add import filters 

1599 elif config_item in ("import_filter", "filter"): 

1600 peer["import_filter"] = {} 

1601 # Loop with filter configuration items 

1602 for filter_type, filter_config in config_value.items(): 

1603 if filter_type not in ("as_sets", "aspath_asns", "origin_asns", "peer_asns", "prefixes"): 

1604 raise BirdPlanError( 

1605 f"Configuration item '{filter_type}' not understood in bgp:peers:{peer_name}:import_filter" 

1606 ) 

1607 peer["import_filter"][filter_type] = filter_config 

1608 # Add import filters 

1609 elif config_item == "import_filter_deny": 

1610 peer["import_filter_deny"] = {} 

1611 # Loop with filter configuration items 

1612 for filter_type, filter_config in config_value.items(): 

1613 if filter_type not in ("aspath_asns", "origin_asns", "prefixes"): 

1614 raise BirdPlanError( 

1615 f"Configuration item '{filter_type}' not understood in bgp:peers:{peer_name}:import_filter_deny" 

1616 ) 

1617 peer["import_filter_deny"][filter_type] = filter_config 

1618 # Add import filters 

1619 elif config_item == "export_filter": 

1620 peer["export_filter"] = {} 

1621 # Loop with filter configuration items 

1622 for filter_type, filter_config in config_value.items(): 

1623 if filter_type not in ("origin_asns", "prefixes"): 

1624 raise BirdPlanError( 

1625 f"Configuration item '{filter_type}' not understood in bgp:peers:{peer_name}:export_filter" 

1626 ) 

1627 peer["export_filter"][filter_type] = filter_config 

1628 

1629 # Work out outgoing community options 

1630 elif config_item == "outgoing_communities": 

1631 if isinstance(config_value, dict): 

1632 # Loop with outgoing community configuration items 

1633 for lc_type, lc_config in config_value.items(): 

1634 if lc_type not in ( 

1635 "blackhole", 

1636 "default", 

1637 "connected", 

1638 "kernel", 

1639 "kernel_blackhole", 

1640 "kernel_default", 

1641 "static", 

1642 "static_blackhole", 

1643 "static_default", 

1644 "originated", 

1645 "originated_default", 

1646 "bgp", 

1647 "bgp_own", 

1648 "bgp_own_blackhole", 

1649 "bgp_own_default", 

1650 "bgp_customer", 

1651 "bgp_customer_blackhole", 

1652 "bgp_peering", 

1653 "bgp_transit", 

1654 "bgp_transit_default", 

1655 "bgp_blackhole", 

1656 "bgp_default", 

1657 ): 

1658 raise BirdPlanError( 

1659 f"Configuration item '{lc_type}' not understood in bgp:peers:{peer_name}:outgoing_communities" 

1660 ) 

1661 # Make sure we have a prepend key 

1662 if "outgoing_communities" not in peer: 

1663 peer["outgoing_communities"] = {} 

1664 # Then add the config... 

1665 peer["outgoing_communities"][lc_type] = lc_config 

1666 

1667 # Check in the case it is a list 

1668 elif isinstance(config_value, list): 

1669 peer["outgoing_communities"] = config_value 

1670 

1671 # Else if we don't know what it is, then throw an exception 

1672 else: 

1673 raise BirdPlanError(f"Configuration item bgp:peers:{peer_name}:outgoing_communities has incorrect type") 

1674 

1675 # Work out outgoing large community options 

1676 elif config_item == "outgoing_large_communities": 

1677 if isinstance(config_value, dict): 

1678 # Loop with outgoing large community configuration items 

1679 for lc_type, lc_config in config_value.items(): 

1680 if lc_type not in ( 

1681 "blackhole", 

1682 "default", 

1683 "connected", 

1684 "kernel", 

1685 "kernel_blackhole", 

1686 "kernel_default", 

1687 "static", 

1688 "static_blackhole", 

1689 "static_default", 

1690 "originated", 

1691 "originated_default", 

1692 "bgp", 

1693 "bgp_own", 

1694 "bgp_own_blackhole", 

1695 "bgp_own_default", 

1696 "bgp_customer", 

1697 "bgp_customer_blackhole", 

1698 "bgp_peering", 

1699 "bgp_transit", 

1700 "bgp_transit_default", 

1701 "bgp_blackhole", 

1702 "bgp_default", 

1703 ): 

1704 raise BirdPlanError( 

1705 f"Configuration item '{lc_type}' not understood in bgp:peers:{peer_name}:outgoing_large_communities" 

1706 ) 

1707 # Make sure we have a prepend key 

1708 if "outgoing_large_communities" not in peer: 

1709 peer["outgoing_large_communities"] = {} 

1710 # Then add the config... 

1711 peer["outgoing_large_communities"][lc_type] = lc_config 

1712 

1713 # Check in the case it is a list 

1714 elif isinstance(config_value, list): 

1715 peer["outgoing_large_communities"] = config_value 

1716 

1717 # Else if we don't know what it is, then throw an exception 

1718 else: 

1719 raise BirdPlanError(f"Configuration item bgp:peers:{peer_name}:outgoing_large_communities has incorrect type") 

1720 

1721 # Work out prepending options 

1722 elif config_item == "prepend": 

1723 if isinstance(config_value, dict): 

1724 # Loop with prepend configuration items 

1725 for prepend_type, prepend_config in config_value.items(): 

1726 if prepend_type not in ( 

1727 "blackhole", 

1728 "default", 

1729 "connected", 

1730 "kernel", 

1731 "kernel_blackhole", 

1732 "kernel_default", 

1733 "static", 

1734 "static_blackhole", 

1735 "static_default", 

1736 "originated", 

1737 "originated_default", 

1738 "bgp", 

1739 "bgp_own", 

1740 "bgp_own_blackhole", 

1741 "bgp_own_default", 

1742 "bgp_customer", 

1743 "bgp_customer_blackhole", 

1744 "bgp_peering", 

1745 "bgp_transit", 

1746 "bgp_transit_default", 

1747 "bgp_blackhole", 

1748 "bgp_default", 

1749 ): 

1750 raise BirdPlanError( 

1751 f"Configuration item '{prepend_type}' not understood in bgp:peers:{peer_name}:prepend" 

1752 ) 

1753 # Make sure we have a prepend key 

1754 if "prepend" not in peer: 

1755 peer["prepend"] = {} 

1756 # Then add the config... 

1757 peer["prepend"][prepend_type] = prepend_config 

1758 else: 

1759 peer["prepend"] = config_value 

1760 

1761 # Work out our constraints 

1762 elif config_item == "constraints": 

1763 # Loop with constraint configuration items 

1764 for constraint_name, constraint_value in config_value.items(): 

1765 if constraint_name not in ( 

1766 "blackhole_import_maxlen4", 

1767 "blackhole_import_minlen4", 

1768 "blackhole_export_maxlen4", 

1769 "blackhole_export_minlen4", 

1770 "blackhole_import_maxlen6", 

1771 "blackhole_import_minlen6", 

1772 "blackhole_export_maxlen6", 

1773 "blackhole_export_minlen6", 

1774 "import_maxlen4", 

1775 "export_maxlen4", 

1776 "import_minlen4", 

1777 "export_minlen4", 

1778 "import_maxlen6", 

1779 "export_maxlen6", 

1780 "import_minlen6", 

1781 "export_minlen6", 

1782 "aspath_import_maxlen", 

1783 "aspath_import_minlen", 

1784 "community_import_maxlen", 

1785 "extended_community_import_maxlen", 

1786 "large_community_import_maxlen", 

1787 ): 

1788 raise BirdPlanError( 

1789 f"Configuration item '{constraint_name}' not understood in bgp:peers:{peer_name}:prepend" 

1790 ) 

1791 # Make sure we have a prepend key 

1792 if "constraints" not in peer: 

1793 peer["constraints"] = {} 

1794 # Then add the config... 

1795 peer["constraints"][constraint_name] = constraint_value 

1796 

1797 else: 

1798 raise BirdPlanError(f"Configuration item '{config_item}' not understood in bgp:peers:{peer_name}") 

1799 

1800 # Check items we need 

1801 for required_item in ["asn", "description", "type"]: 

1802 if required_item not in peer: 

1803 raise BirdPlanError(f"Configuration item '{required_item}' missing in bgp:peers:{peer_name}") 

1804 

1805 # Check the peer type is valid 

1806 if peer["type"] not in ( 

1807 "customer", 

1808 "internal", 

1809 "peer", 

1810 "routecollector", 

1811 "routeserver", 

1812 "rrclient", 

1813 "rrserver", 

1814 "rrserver-rrserver", 

1815 "transit", 

1816 ): 

1817 raise BirdPlanError(f"Configuration item 'type' for BGP peer '{peer_name}' has invalid value '%s'" % peer["type"]) 

1818 

1819 # Check that if we have a peer type of rrclient, that we have rr_cluster_id too... 

1820 if (peer["type"] == "rrclient") and ("rr_cluster_id" not in self.config["bgp"]): 

1821 raise BirdPlanError(f"Configuration for BGP peer '{peer_name}' is missing 'rr_cluster_id' when having 'rrclient' peers") 

1822 # If we are a customer type, we must have filters defined 

1823 if (peer["type"] == "customer") and ("import_filter" not in peer) and ("filter" not in peer): 

1824 raise BirdPlanError(f"Configuration for BGP peer '{peer_name}' is missing 'import_filter' when type is 'customer'") 

1825 # We must have a neighbor4 or neighbor6 

1826 if ("neighbor4" not in peer) and ("neighbor6" not in peer): 

1827 raise BirdPlanError(f"Configuration for BGP peer '{peer_name}' is missing 'neighbor4' or 'neighbor6' config") 

1828 # We must have a source_address4 for neighbor4 

1829 if ("neighbor4" in peer) and ("source_address4" not in peer): 

1830 raise BirdPlanError(f"Configuration for BGP peer '{peer_name}' must have a 'source_address4'") 

1831 # We must have a source_address6 for neighbor6 

1832 if ("neighbor6" in peer) and ("source_address6" not in peer): 

1833 raise BirdPlanError(f"Configuration for BGP peer '{peer_name}' must have a 'source_address6'") 

1834 

1835 # Make sure we have items we need 

1836 self.birdconf.protocols.bgp.add_peer(peer_name, peer) 

1837 

1838 @property 

1839 def birdconf(self) -> BirdConfig: 

1840 """Return the BirdConfig object.""" 

1841 return self._birdconf 

1842 

1843 @property 

1844 def config(self) -> Dict[str, Any]: 

1845 """Return our config.""" 

1846 return self._config 

1847 

1848 @config.setter 

1849 def config(self, config: Dict[str, Any]) -> None: 

1850 """Set our configuration.""" 

1851 self._config = config 

1852 

1853 @property 

1854 def state(self) -> Dict[str, Any]: 

1855 """Return our state.""" 

1856 return self.birdconf.state 

1857 

1858 @state.setter 

1859 def state(self, state: Dict[str, Any]) -> None: 

1860 """Set our state.""" 

1861 self.birdconf.state = state 

1862 

1863 @property 

1864 def state_file(self) -> Optional[str]: 

1865 """State file we're using.""" 

1866 return self._state_file 

1867 

1868 @state_file.setter 

1869 def state_file(self, state_file: Optional[str]) -> None: 

1870 """Set our state file.""" 

1871 self._state_file = state_file 

1872 

1873 @property 

1874 def yaml(self) -> YAML: 

1875 """Return our YAML parser.""" 

1876 return self._yaml