Coverage for src/birdplan/bird_config/sections/protocols/ospf/ospf_config_parser.py: 79%

87 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-25 07:38 +0000

1# 

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

3# 

4# Copyright (c) 2019-2025, AllWorldIT 

5# 

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

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

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

9# (at your option) any later version. 

10# 

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

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

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

14# GNU General Public License for more details. 

15# 

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

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

18 

19"""BIRD OSPF configuration parser.""" 

20 

21from typing import Any 

22 

23from .....exceptions import BirdPlanConfigError 

24from .... import BirdConfig 

25from ....config_parser import ConfigParser 

26 

27__all__ = ["OSPFConfigParser"] 

28 

29 

30class OSPFConfigParser(ConfigParser): 

31 """OSPF configuration parser.""" 

32 

33 _birdconf: BirdConfig 

34 

35 def parse(self, config: dict[str, Any]) -> None: 

36 """Configure OSPF protocol.""" 

37 

38 self._config_ospf(config) 

39 

40 def _config_ospf(self, config: dict[str, Any]) -> None: 

41 """Configure OSPF section.""" 

42 

43 # If we have no ospf section, just return 

44 if "ospf" not in config: 

45 return 

46 

47 # Check configuration options are supported 

48 for config_item in config["ospf"]: 

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

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

51 

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

53 if "v4version" in config["ospf"]: 

54 if isinstance(config["ospf"]["v4version"], int | str): 

55 v4version = f"{config['ospf']['v4version']}" 

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

57 self.birdconf.protocols.ospf.v4version = v4version 

58 else: 

59 raise BirdPlanConfigError("The 'ospf' config item 'v4version' has unsupported value") 

60 else: 

61 raise BirdPlanConfigError("The 'ospf' config item 'v4version' has unsupported type") 

62 

63 self._config_ospf_accept(config) 

64 self._config_ospf_redistribute(config) 

65 self._config_ospf_areas(config) 

66 

67 def _config_ospf_accept(self, config: dict[str, Any]) -> None: 

68 """Configure ospf:accept section.""" 

69 

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

71 if "accept" not in config["ospf"]: 

72 return 

73 

74 # Loop with accept items 

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

76 # Allow accept of the default route 

77 if accept == "default": 

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

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

80 else: 

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

82 

83 def _config_ospf_redistribute(self, config: dict[str, Any]) -> None: # noqa: C901,PLR0912 

84 """Configure ospf:redistribute section.""" 

85 

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

87 if "redistribute" not in config["ospf"]: 

88 return 

89 

90 # Loop with redistribution items 

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

92 # Add static route redistribution 

93 if redistribute == "static": 

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

95 # Add connected route redistribution 

96 elif redistribute == "connected": 

97 # Set type 

98 redistribute_connected: bool | list[str] 

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

100 if isinstance(redistribute_config, bool): 

101 redistribute_connected = redistribute_config 

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

103 elif isinstance(redistribute_config, dict): 

104 # Check it has an "interfaces" key 

105 if "interfaces" not in redistribute_config: 

106 raise BirdPlanConfigError( 

107 f"Configurion item '{redistribute}' has no 'interfaces' option in ospf:redistribute" 

108 ) 

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

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

111 raise BirdPlanConfigError( 

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

113 ) 

114 # Set redistribute_connected as the interface list 

115 redistribute_connected = redistribute_config["interfaces"] 

116 else: 

117 raise BirdPlanConfigError(f"Configurion item '{redistribute}' has an unsupported value") 

118 # Add configuration 

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

120 # Add kernel route redistribution 

121 elif redistribute == "kernel": 

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

123 # Add kernel default route redistribution 

124 elif redistribute == "kernel_default": 

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

126 # Add static route redistribution 

127 elif redistribute == "static": 

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

129 # Add static default route redistribution 

130 elif redistribute == "static_default": 

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

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

133 else: 

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

135 

136 def _config_ospf_areas(self, config: dict[str, Any]) -> None: # noqa: C901,PLR0912 

137 """Configure ospf:interfaces section.""" 

138 

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

140 if "areas" not in config["ospf"]: 

141 return 

142 

143 # Loop with each area and its config 

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

145 # Make sure we have an interface for the area 

146 if "interfaces" not in raw_area_config: 

147 raise BirdPlanConfigError(f"OSPF area '{area_name}' must contain 'interfaces'") 

148 # Loop with each config item 

149 area_config = {} 

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

151 # Make sure this item is supported 

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

153 raise BirdPlanConfigError( 

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

155 ) 

156 # Skip over interfaces 

157 if config_item == "interfaces": 

158 continue 

159 # Check for supported config options 

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

161 area_config[config_item] = raw_config 

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

163 else: 

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

165 

166 # Add area 

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

168 

169 # Loop with interfaces in area 

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

171 # Start with no special interface configuration 

172 interface_config: dict[str, Any] = {} 

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

174 add_ospf_interface = False 

175 if isinstance(raw_config, bool): 

176 add_ospf_interface = raw_config 

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

178 elif isinstance(raw_config, dict): 

179 add_ospf_interface = True 

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

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

182 interface_config[raw_item] = raw_value 

183 else: 

184 raise BirdPlanConfigError( 

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

186 ) 

187 else: 

188 raise BirdPlanConfigError( 

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

190 ) 

191 

192 # Add interface to area 

193 if add_ospf_interface: 

194 area.add_interface(interface_name, interface_config)