Coverage for birdplan/__init__.py: 78%
692 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 03:27 +0000
« 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/>.
19"""BirdPlan package."""
21# pylint: disable=too-many-lines
23import grp
24import json
25import logging
26import os
27import pathlib
28import pwd
29import re
30from typing import Any, Dict, List, Optional, Union
32import birdclient
33import jinja2
34import packaging.version
36from .bird_config import BirdConfig
37from .console.colors import colored
38from .exceptions import BirdPlanError
39from .version import __version__
40from .yaml import YAML, YAMLError
42__all__ = [
43 "BirdPlan",
44 "__version__",
45]
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]]
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")
60class BirdPlan: # pylint: disable=too-many-public-methods
61 """Main BirdPlan class."""
63 _birdconf: BirdConfig
64 _config: Dict[str, Any]
65 _state_file: Optional[str]
66 _yaml: YAML
68 def __init__(self, test_mode: bool = False) -> None:
69 """Initialize object."""
71 self._birdconf = BirdConfig(test_mode=test_mode)
72 self._config = {}
73 self._state_file = None
74 self._yaml = YAML()
76 def load(self, **kwargs: Any) -> None:
77 """
78 Initialize object.
80 Parameters
81 ----------
82 plan_file : str
83 Source plan file to generate configuration from.
85 state_file : Optional[str]
86 Optional state file, used for commands like BGP graceful shutdown.
88 ignore_irr_changes : bool
89 Optional parameter to ignore IRR lookups during configuration load.
91 ignore_peeringdb_changes : bool
92 Optional parameter to ignore peering DB lookups during configuraiton load.
94 use_cached : bool
95 Optional parameter to use cached values from state during configuration load.
97 """
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)
106 # Make sure we have the parameters we need
107 if not plan_file:
108 raise BirdPlanError("Required parameter 'plan_file' not found")
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)
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 )
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
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
134 # Set our state file and load state
135 self.state_file = state_file
136 self.load_state()
138 # Make sure we have configuration...
139 if not self.config:
140 raise BirdPlanError("No configuration found")
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")
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
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()
161 def configure(self) -> str:
162 """
163 Create BIRD configuration.
165 Returns
166 -------
167 str : Bird configuration as a string.
169 """
170 return "\n".join(self.birdconf.get_config())
172 def commit_state(self) -> None:
173 """Commit our current state."""
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")
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
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
201 def load_state(self) -> None:
202 """Load our state."""
204 # Clear state
205 self.state = {}
207 # Skip if we don't have a state file
208 if not self.state_file:
209 return
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
222 def state_ospf_summary(self, bird_socket: Optional[str] = None) -> BirdPlanOSPFSummary:
223 """
224 Return OSPF summary.
226 Returns
227 -------
228 BirdPlanOSPFSummary
229 Dictionary containing the OSPF summary.
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 }
252 """ # noqa: RST201,RST203,RST301
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")
258 # Initialize our return structure
259 ret: BirdPlanOSPFSummary = {}
261 # Return if we don't have any BGP state
262 if "ospf" not in self.state:
263 return ret
265 # Query bird client for the current protocols
266 birdc = birdclient.BirdClient(control_socket=bird_socket)
267 bird_protocols = birdc.show_protocols()
269 for name, data in bird_protocols.items():
270 if data["proto"] != "OSPF":
271 continue
272 ret[name] = data
274 return ret
276 def state_bgp_peer_summary(self, bird_socket: Optional[str] = None) -> BirdPlanBGPPeerSummary:
277 """
278 Return BGP peer summary.
280 Returns
281 -------
282 BirdPlanBGPPeerStatus
283 Dictionary containing the BGP peer summary.
285 eg.
286 {
287 'peer1': {
288 'name': ...,
289 'asn': ...,
290 'description': ...,
291 'protocols': {
292 'ipv4': ...,
293 'ipv6': ...,
294 }
295 }
296 'peer2': {
297 ...,
298 }
299 }
301 """ # noqa: RST201,RST203,RST301
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")
307 # Initialize our return structure
308 ret: BirdPlanBGPPeerSummary = {}
310 # Return if we don't have any BGP state
311 if "bgp" not in self.state:
312 return ret
314 # Query bird client for the current protocols
315 birdc = birdclient.BirdClient(control_socket=bird_socket)
316 bird_protocols = birdc.show_protocols()
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 }
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"]]
340 return ret
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.
346 Returns
347 -------
348 BirdPlanBGPPeerShow
349 Dictionary containing the status of a BGP peer.
351 eg.
352 {
353 'asn': ...,
354 'description': ...,
355 'protocols': {
356 'ipv4': {
357 ...,
358 'status': ...,
359 }
360 'ipv6': ...,
361 },
362 }
364 """ # noqa: RST201,RST203,RST301
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")
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")
377 # Make things easier below
378 configured = self.state["bgp"]["peers"][peer]
380 # Set our peer info to the configured state
381 ret: BirdPlanBGPPeerShow = configured
383 # Add peer name
384 ret["name"] = peer
386 # Query bird client for the current protocols
387 birdc = birdclient.BirdClient(control_socket=bird_socket)
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
398 return ret
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.
404 Parameters
405 ----------
406 peer : str
407 Peer name to set to BGP graceful shutdown state for.
408 Pattern matches can be specified with '*'.
410 value : bool
411 State of the graceful shutdown option for this peer.
413 """
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")
419 # Prepare the state structure if its not got what we need
420 if "bgp" not in self.state:
421 self.state["bgp"] = {}
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
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.
433 Parameters
434 ----------
435 peer : str
436 Peer name or pattern to remove the BGP graceful shutdown override flag from.
438 """
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")
444 # Prepare the state structure if its not got what we need
445 if "bgp" not in self.state:
446 return
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"]
459 def state_bgp_peer_graceful_shutdown_status(self) -> BirdPlanBGPPeerGracefulShutdownStatus:
460 """
461 Return the status of BGP peer graceful shutdown.
463 Returns
464 -------
465 BirdPlanBGPPeerGracefulShutdownStatus
466 Dictionary containing the status of overrides and peers.
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 }
482 """ # noqa: RST201,RST203,RST301
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")
488 # Initialize our return structure
489 ret: BirdPlanBGPPeerGracefulShutdownStatus = {
490 "overrides": {},
491 "current": {},
492 "pending": {},
493 }
495 # Return if we don't have any BGP state
496 if "bgp" not in self.state:
497 return ret
499 # Pull in any overrides we may have
500 if "+graceful_shutdown" in self.state["bgp"]:
501 ret["overrides"] = self.state["bgp"]["+graceful_shutdown"]
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)
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
514 return ret
516 def state_bgp_peer_quarantine_set(self, peer: str, value: bool) -> None:
517 """
518 Set the BGP quarantine override state for a peer.
520 Parameters
521 ----------
522 peer : str
523 Peer name to set to BGP quarantine state for.
524 Pattern matches can be specified with '*'.
526 value : bool
527 State of the quarantine option for this peer.
529 """
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")
535 # Prepare the state structure if its not got what we need
536 if "bgp" not in self.state:
537 self.state["bgp"] = {}
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
545 def state_bgp_peer_quarantine_remove(self, peer: str) -> None:
546 """
547 Remove a BGP quarantine override flag from a peer or pattern.
549 Parameters
550 ----------
551 peer : str
552 Peer name or pattern to remove the BGP quarantine override flag from.
554 """
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")
560 # Prepare the state structure if its not got what we need
561 if "bgp" not in self.state:
562 return
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")
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"]
574 def state_bgp_peer_quarantine_status(self) -> BirdPlanBGPPeerQuarantineStatus:
575 """
576 Return the status of BGP peer quarantine.
578 Returns
579 -------
580 BirdPlanBGPPeerQuarantineStatus
581 Dictionary containing the status of overrides and peers.
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 }
597 """ # noqa: RST201,RST203,RST301
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")
603 # Initialize our return structure
604 ret: BirdPlanBGPPeerQuarantineStatus = {
605 "overrides": {},
606 "current": {},
607 "pending": {},
608 }
610 # Return if we don't have any BGP state
611 if "bgp" not in self.state:
612 return ret
614 # Pull in any overrides we may have
615 if "+quarantine" in self.state["bgp"]:
616 ret["overrides"] = self.state["bgp"]["+quarantine"]
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)
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
629 return ret
631 def state_ospf_set_interface_cost(self, area: str, interface: str, cost: int) -> None:
632 """
633 Set an OSPF interface cost override.
635 Parameters
636 ----------
637 area : str
638 Interface to set the OSPF cost for.
640 interface : str
641 Interface to set the OSPF cost for.
643 cost : int
644 OSPF interface cost.
646 """
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")
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] = {}
664 # Set the interface cost value
665 self.state["ospf"]["areas"][area]["+interfaces"][interface]["cost"] = cost
667 def state_ospf_remove_interface_cost(self, area: str, interface: str) -> None:
668 """
669 Remove an OSPF interface cost override.
671 Parameters
672 ----------
673 area : str
674 OSPF area which contains the interface.
676 interface : str
677 Interface to remove the OSPF cost for.
679 """
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")
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")
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"]
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.
708 Parameters
709 ----------
710 area : str
711 OSPF area which contains the interface.
713 interface : str
714 Interface to set the OSPF ECMP weight for.
716 ecmp_weight : int
717 OSPF interface ECMP weight.
719 """
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")
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] = {}
737 # Set the interface ecmp_weight value
738 self.state["ospf"]["areas"][area]["+interfaces"][interface]["ecmp_weight"] = ecmp_weight
740 def state_ospf_remove_interface_ecmp_weight(self, area: str, interface: str) -> None:
741 """
742 Remove an OSPF interface ECMP weight override.
744 Parameters
745 ----------
746 area : str
747 OSPF area which contains the interface.
749 interface : str
750 Interface to remove the OSPF ECMP weight for.
752 """
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")
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")
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"]
777 def state_ospf_interface_status(self) -> BirdPlanOSPFInterfaceStatus: # pylint: disable=too-many-branches
778 """
779 Return the status of OSPF interfaces.
781 Returns
782 -------
783 BirdPlanOSPFInterfaceStatus
784 Dictionary containing the status of overrides and peers.
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 }
826 """ # noqa: RST201,RST203,RST301
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")
832 # Initialize our return structure
833 ret: BirdPlanOSPFInterfaceStatus = {
834 "overrides": {},
835 "current": {},
836 "pending": {},
837 }
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
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
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
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
893 return ret
895 def _config_global(self) -> None:
896 """Configure global options."""
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"]
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"]
907 # Check if we're in debugging mode or not
908 if "debug" in self.config:
909 self.birdconf.debug = self.config["debug"]
911 def _config_kernel(self) -> None:
912 """Configure kernel section."""
914 # If we have no rip section, just return
915 if "kernel" not in self.config:
916 return
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")
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")
930 if "routing_table" in self.config["kernel"]:
931 self.birdconf.routing_table = self.config["kernel"]["routing_table"]
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)
940 def _config_export_kernel(self) -> None:
941 """Configure export_kernel section."""
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'")
963 def _config_rip(self) -> None:
964 """Configure rip section."""
966 # If we have no rip section, just return
967 if "rip" not in self.config:
968 return
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")
975 self._config_rip_accept()
976 self._config_rip_redistribute()
977 self._config_rip_interfaces()
979 def _config_rip_accept(self) -> None:
980 """Configure rip:accept section."""
982 # If we don't have an accept section, just return
983 if "accept" not in self.config["rip"]:
984 return
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")
995 def _config_rip_redistribute(self) -> None: # pylint: disable=too-many-branches
996 """Configure rip:redistribute section."""
998 # If we don't have a redistribute section just return
999 if "redistribute" not in self.config["rip"]:
1000 return
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")
1047 def _config_rip_interfaces(self) -> None:
1048 """Configure rip:interfaces section."""
1050 # If we don't have interfaces in our rip section, just return
1051 if "interfaces" not in self.config["rip"]:
1052 return
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)
1068 def _config_ospf(self) -> None:
1069 """Configure OSPF section."""
1071 # If we have no ospf section, just return
1072 if "ospf" not in self.config:
1073 return
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")
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")
1091 self._config_ospf_accept()
1092 self._config_ospf_redistribute()
1093 self._config_ospf_areas()
1095 def _config_ospf_accept(self) -> None:
1096 """Configure ospf:accept section."""
1098 # If we don't have an accept section, just return
1099 if "accept" not in self.config["ospf"]:
1100 return
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")
1111 def _config_ospf_redistribute(self) -> None: # pylint: disable=too-many-branches
1112 """Configure ospf:redistribute section."""
1114 # If we don't have a redistribute section just return
1115 if "redistribute" not in self.config["ospf"]:
1116 return
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")
1162 def _config_ospf_areas(self) -> None: # pylint: disable=too-many-branches
1163 """Configure ospf:interfaces section."""
1165 # If we don't have areas in our ospf section, just return
1166 if "areas" not in self.config["ospf"]:
1167 return
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")
1192 # Add area
1193 area = self.birdconf.protocols.ospf.add_area(area_name, area_config)
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 )
1218 # Add interface to area
1219 if add_ospf_interface:
1220 area.add_interface(interface_name, interface_config)
1222 def _config_bgp(self) -> None:
1223 """Configure bgp section."""
1225 # If we have no rip section, just return
1226 if "bgp" not in self.config:
1227 return
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"]
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")
1250 self._config_bgp_accept()
1251 self._config_bgp_globals()
1252 self._config_bgp_peertype_constraints()
1253 self._config_bgp_originate()
1255 self._config_bgp_import()
1256 self._config_bgp_peers()
1258 def _config_bgp_accept(self) -> None:
1259 """Configure bgp:accept section."""
1261 # If we don't have an accept section, just return
1262 if "accept" not in self.config["bgp"]:
1263 return
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")
1281 def _config_bgp_globals(self) -> None:
1282 """Configure bgp globals."""
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"]
1288 # Setup graceful shutdown if specified
1289 if "quarantine" in self.config["bgp"]:
1290 self.birdconf.protocols.bgp.quarantine = self.config["bgp"]["quarantine"]
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"]
1296 def _config_bgp_peertype_constraints(self) -> None:
1297 """Configure bgp:peertype_constraints section."""
1299 # If we don't have a peertype_constraints section, just return
1300 if "peertype_constraints" not in self.config["bgp"]:
1301 return
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 )
1399 def _config_bgp_originate(self) -> None:
1400 """Configure bgp:originate section."""
1402 # If we don't have an accept section, just return
1403 if "originate" not in self.config["bgp"]:
1404 return
1406 # Add origination routes
1407 for route in self.config["bgp"]["originate"]:
1408 self.birdconf.protocols.bgp.add_originated_route(route)
1410 def _config_bgp_import(self) -> None: # pylint: disable=too-many-branches
1411 """Configure bgp:import section."""
1413 # If we don't have the option then just return
1414 if "import" not in self.config["bgp"]:
1415 return
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
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
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
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")
1465 def _config_bgp_peers(self) -> None:
1466 """Configure bgp:peers section."""
1468 if "peers" not in self.config["bgp"]:
1469 return
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]")
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 )
1486 # Configure peer
1487 self._config_bgp_peers_peer(peer_name, peer_config)
1489 # Bump current peer
1490 peer_cur += 1
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."""
1497 # Start with no peer config
1498 peer = {}
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
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
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
1667 # Check in the case it is a list
1668 elif isinstance(config_value, list):
1669 peer["outgoing_communities"] = config_value
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")
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
1713 # Check in the case it is a list
1714 elif isinstance(config_value, list):
1715 peer["outgoing_large_communities"] = config_value
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")
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
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
1797 else:
1798 raise BirdPlanError(f"Configuration item '{config_item}' not understood in bgp:peers:{peer_name}")
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}")
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"])
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'")
1835 # Make sure we have items we need
1836 self.birdconf.protocols.bgp.add_peer(peer_name, peer)
1838 @property
1839 def birdconf(self) -> BirdConfig:
1840 """Return the BirdConfig object."""
1841 return self._birdconf
1843 @property
1844 def config(self) -> Dict[str, Any]:
1845 """Return our config."""
1846 return self._config
1848 @config.setter
1849 def config(self, config: Dict[str, Any]) -> None:
1850 """Set our configuration."""
1851 self._config = config
1853 @property
1854 def state(self) -> Dict[str, Any]:
1855 """Return our state."""
1856 return self.birdconf.state
1858 @state.setter
1859 def state(self, state: Dict[str, Any]) -> None:
1860 """Set our state."""
1861 self.birdconf.state = state
1863 @property
1864 def state_file(self) -> Optional[str]:
1865 """State file we're using."""
1866 return self._state_file
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
1873 @property
1874 def yaml(self) -> YAML:
1875 """Return our YAML parser."""
1876 return self._yaml