Coverage for manila/network/standalone_network_plugin.py: 100%

142 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2026-02-18 22:19 +0000

1# Copyright 2015 Mirantis, Inc. 

2# All Rights Reserved. 

3# 

4# Licensed under the Apache License, Version 2.0 (the "License"); you may 

5# not use this file except in compliance with the License. You may obtain 

6# a copy of the License at 

7# 

8# http://www.apache.org/licenses/LICENSE-2.0 

9# 

10# Unless required by applicable law or agreed to in writing, software 

11# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 

12# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 

13# License for the specific language governing permissions and limitations 

14# under the License. 

15 

16import netaddr 

17from oslo_config import cfg 

18from oslo_log import log 

19 

20from manila.common import constants 

21from manila import exception 

22from manila.i18n import _ 

23from manila import network 

24from manila import utils 

25 

26standalone_network_plugin_opts = [ 

27 cfg.StrOpt( 

28 'standalone_network_plugin_gateway', 

29 help="Gateway address that should be used. Required."), 

30 cfg.StrOpt( 

31 'standalone_network_plugin_mask', 

32 help="Network mask that will be used. Can be either decimal " 

33 "like '24' or binary like '255.255.255.0'. Required."), 

34 cfg.StrOpt( 

35 'standalone_network_plugin_network_type', 

36 help="Network type, such as 'flat', 'vlan', 'vxlan' or 'gre'. " 

37 "Empty value is alias for 'flat'. " 

38 "It will be assigned to share-network and share drivers will be " 

39 "able to use this for network interfaces within provisioned " 

40 "share servers. Optional.", 

41 choices=['flat', 'vlan', 'vxlan', 'gre']), 

42 cfg.IntOpt( 

43 'standalone_network_plugin_segmentation_id', 

44 help="Set it if network has segmentation (VLAN, VXLAN, etc...). " 

45 "It will be assigned to share-network and share drivers will be " 

46 "able to use this for network interfaces within provisioned " 

47 "share servers. Optional. Example: 1001"), 

48 cfg.ListOpt( 

49 'standalone_network_plugin_allowed_ip_ranges', 

50 help="Can be IP address, range of IP addresses or list of addresses " 

51 "or ranges. Contains addresses from IP network that are allowed " 

52 "to be used. If empty, then will be assumed that all host " 

53 "addresses from network can be used. Optional. " 

54 "Examples: 10.0.0.10 or 10.0.0.10-10.0.0.20 or " 

55 "10.0.0.10-10.0.0.20,10.0.0.30-10.0.0.40,10.0.0.50"), 

56 cfg.IntOpt( 

57 'standalone_network_plugin_mtu', 

58 default=1500, 

59 help="Maximum Transmission Unit (MTU) value of the network. Default " 

60 "value is 1500."), 

61] 

62 

63CONF = cfg.CONF 

64LOG = log.getLogger(__name__) 

65 

66 

67class StandaloneNetworkPlugin(network.NetworkBaseAPI): 

68 """Standalone network plugin for share drivers. 

69 

70 This network plugin can be used with any network platform. 

71 It can serve flat networks as well as segmented. 

72 It does not require some specific network services in OpenStack like 

73 the Neutron plugin. 

74 The only thing that plugin does is reservation and release of IP addresses 

75 from some network. 

76 """ 

77 

78 def __init__(self, config_group_name=None, db_driver=None, label='user'): 

79 self.config_group_name = config_group_name or 'DEFAULT' 

80 super(StandaloneNetworkPlugin, 

81 self).__init__(config_group_name=self.config_group_name, 

82 db_driver=db_driver) 

83 CONF.register_opts( 

84 standalone_network_plugin_opts, 

85 group=self.config_group_name) 

86 self.configuration = getattr(CONF, self.config_group_name, CONF) 

87 self._set_persistent_network_data() 

88 self._label = label 

89 LOG.debug( 

90 "\nStandalone network plugin data for config group " 

91 "'%(config_group)s': \n" 

92 "IP version - %(ip_version)s\n" 

93 "Used network - %(net)s\n" 

94 "Used gateway - %(gateway)s\n" 

95 "Used network type - %(network_type)s\n" 

96 "Used segmentation ID - %(segmentation_id)s\n" 

97 "Allowed CIDRs - %(cidrs)s\n" 

98 "Original allowed IP ranges - %(ip_ranges)s\n" 

99 "Reserved IP addresses - %(reserved)s\n", 

100 dict( 

101 config_group=self.config_group_name, 

102 ip_version=self.ip_version, 

103 net=str(self.net), 

104 gateway=self.gateway, 

105 network_type=self.network_type, 

106 segmentation_id=self.segmentation_id, 

107 cidrs=self.allowed_cidrs, 

108 ip_ranges=self.allowed_ip_ranges, 

109 reserved=self.reserved_addresses)) 

110 

111 @property 

112 def label(self): 

113 return self._label 

114 

115 def _set_persistent_network_data(self): 

116 """Sets persistent data for whole plugin.""" 

117 # NOTE(tommylikehu): Standalone plugin could only support 

118 # either IPv4 or IPv6, so if both network_plugin_ipv4_enabled 

119 # and network_plugin_ipv6_enabled are configured True 

120 # we would only support IPv6. 

121 ipv4_enabled = getattr(self.configuration, 

122 'network_plugin_ipv4_enabled', None) 

123 ipv6_enabled = getattr(self.configuration, 

124 'network_plugin_ipv6_enabled', None) 

125 

126 if ipv4_enabled: 

127 ip_version = 4 

128 if ipv6_enabled: 

129 ip_version = 6 

130 if ipv4_enabled and ipv6_enabled: 

131 LOG.warning("Only IPv6 is enabled, although both " 

132 "'network_plugin_ipv4_enabled' and " 

133 "'network_plugin_ipv6_enabled' are " 

134 "configured True.") 

135 

136 self.network_type = ( 

137 self.configuration.standalone_network_plugin_network_type) 

138 self.segmentation_id = ( 

139 self.configuration.standalone_network_plugin_segmentation_id) 

140 self.gateway = self.configuration.standalone_network_plugin_gateway 

141 self.mask = self.configuration.standalone_network_plugin_mask 

142 self.allowed_ip_ranges = ( 

143 self.configuration.standalone_network_plugin_allowed_ip_ranges) 

144 self.ip_version = ip_version 

145 self.net = self._get_network() 

146 self.allowed_cidrs = self._get_list_of_allowed_addresses() 

147 self.reserved_addresses = ( 

148 str(self.net.network), 

149 self.gateway, 

150 str(self.net.broadcast)) 

151 self.mtu = self.configuration.standalone_network_plugin_mtu 

152 

153 def _get_network(self): 

154 """Returns IPNetwork object calculated from gateway and netmask.""" 

155 if not isinstance(self.gateway, str): 

156 raise exception.NetworkBadConfigurationException( 

157 _("Configuration option 'standalone_network_plugin_gateway' " 

158 "is required and has improper value '%s'.") % self.gateway) 

159 if not isinstance(self.mask, str): 

160 raise exception.NetworkBadConfigurationException( 

161 _("Configuration option 'standalone_network_plugin_mask' is " 

162 "required and has improper value '%s'.") % self.mask) 

163 try: 

164 return netaddr.IPNetwork(self.gateway + '/' + self.mask) 

165 except netaddr.AddrFormatError as e: 

166 raise exception.NetworkBadConfigurationException( 

167 reason=e) 

168 

169 def _get_list_of_allowed_addresses(self): 

170 """Returns list of CIDRs that can be used for getting IP addresses. 

171 

172 Reads information provided via configuration, such as gateway, 

173 netmask, segmentation ID and allowed IP ranges, then performs 

174 validation of provided data. 

175 

176 :returns: list of CIDRs as text types. 

177 :raises: exception.NetworkBadConfigurationException 

178 """ 

179 cidrs = [] 

180 if self.allowed_ip_ranges: 

181 for ip_range in self.allowed_ip_ranges: 

182 ip_range_start = ip_range_end = None 

183 if utils.is_valid_ip_address(ip_range, self.ip_version): 

184 ip_range_start = ip_range_end = ip_range 

185 elif '-' in ip_range: 

186 ip_range_list = ip_range.split('-') 

187 if len(ip_range_list) == 2: 

188 ip_range_start = ip_range_list[0] 

189 ip_range_end = ip_range_list[1] 

190 for ip in ip_range_list: 

191 utils.is_valid_ip_address(ip, self.ip_version) 

192 else: 

193 msg = _("Wrong value for IP range " 

194 "'%s' was provided.") % ip_range 

195 raise exception.NetworkBadConfigurationException( 

196 reason=msg) 

197 else: 

198 msg = _("Config option " 

199 "'standalone_network_plugin_allowed_ip_ranges' " 

200 "has incorrect value " 

201 "'%s'.") % self.allowed_ip_ranges 

202 raise exception.NetworkBadConfigurationException( 

203 reason=msg) 

204 

205 range_instance = netaddr.IPRange(ip_range_start, ip_range_end) 

206 

207 if range_instance not in self.net: 

208 data = dict( 

209 range=str(range_instance), 

210 net=str(self.net), 

211 gateway=self.gateway, 

212 netmask=self.net.netmask) 

213 msg = _("One of provided allowed IP ranges ('%(range)s') " 

214 "does not fit network '%(net)s' combined from " 

215 "gateway '%(gateway)s' and netmask " 

216 "'%(netmask)s'.") % data 

217 raise exception.NetworkBadConfigurationException( 

218 reason=msg) 

219 

220 cidrs.extend( 

221 str(cidr) for cidr in range_instance.cidrs()) 

222 else: 

223 if self.net.version != self.ip_version: 

224 msg = _("Configured invalid IP version '%(conf_v)s', network " 

225 "has version ""'%(net_v)s'") % dict( 

226 conf_v=self.ip_version, net_v=self.net.version) 

227 raise exception.NetworkBadConfigurationException(reason=msg) 

228 cidrs.append(str(self.net)) 

229 

230 return cidrs 

231 

232 def _get_available_ips(self, context, amount): 

233 """Returns IP addresses from allowed IP range if there are unused IPs. 

234 

235 :returns: IP addresses as list of text types 

236 :raises: exception.NetworkBadConfigurationException 

237 """ 

238 ips = [] 

239 if amount < 1: 

240 return ips 

241 iterator = netaddr.iter_unique_ips(*self.allowed_cidrs) 

242 for ip in iterator: 

243 ip = str(ip) 

244 if (ip in self.reserved_addresses or 

245 self.db.network_allocations_get_by_ip_address(context, 

246 ip)): 

247 continue 

248 else: 

249 ips.append(ip) 

250 if len(ips) == amount: 

251 return ips 

252 msg = _("No available IP addresses left in CIDRs %(cidrs)s. " 

253 "Requested amount of IPs to be provided '%(amount)s', " 

254 "available only '%(available)s'.") % { 

255 'cidrs': self.allowed_cidrs, 

256 'amount': amount, 

257 'available': len(ips)} 

258 raise exception.NetworkBadConfigurationException(reason=msg) 

259 

260 def include_network_info(self, share_network_subnet): 

261 """Includes share-network-subnet with plugin specific data.""" 

262 self._save_network_info(None, share_network_subnet, save_db=False) 

263 

264 def _save_network_info(self, context, share_network_subnet, save_db=True): 

265 """Update share-network-subnet with plugin specific data.""" 

266 data = { 

267 'network_type': self.network_type, 

268 'segmentation_id': self.segmentation_id, 

269 'cidr': str(self.net.cidr), 

270 'gateway': str(self.gateway), 

271 'ip_version': self.ip_version, 

272 'mtu': self.mtu, 

273 } 

274 share_network_subnet.update(data) 

275 if self.label != 'admin' and save_db: 

276 self.db.share_network_subnet_update( 

277 context, share_network_subnet['id'], data) 

278 

279 @utils.synchronized( 

280 "allocate_network_for_standalone_network_plugin", external=True) 

281 def allocate_network(self, context, share_server, share_network=None, 

282 share_network_subnet=None, **kwargs): 

283 """Allocate network resources using one dedicated network. 

284 

285 This one has interprocess lock to avoid concurrency in creation of 

286 share servers with same IP addresses using different share-networks. 

287 """ 

288 allocation_count = kwargs.get('count', 1) 

289 if self.label != 'admin': 

290 self._verify_share_network(share_server['id'], 

291 share_network_subnet) 

292 else: 

293 share_network_subnet = share_network_subnet or {} 

294 self._save_network_info(context, share_network_subnet) 

295 allocations = [] 

296 ip_addresses = self._get_available_ips(context, allocation_count) 

297 for ip_address in ip_addresses: 

298 data = { 

299 'share_server_id': share_server['id'], 

300 'ip_address': ip_address, 

301 'status': constants.STATUS_ACTIVE, 

302 'label': self.label, 

303 'network_type': share_network_subnet['network_type'], 

304 'segmentation_id': share_network_subnet['segmentation_id'], 

305 'cidr': share_network_subnet['cidr'], 

306 'gateway': share_network_subnet['gateway'], 

307 'ip_version': share_network_subnet['ip_version'], 

308 'mtu': share_network_subnet['mtu'], 

309 } 

310 if self.label != 'admin': 

311 data['share_network_subnet_id'] = ( 

312 share_network_subnet['id']) 

313 allocations.append( 

314 self.db.network_allocation_create(context, data)) 

315 return allocations 

316 

317 def deallocate_network(self, context, share_server_id, 

318 share_network=None, share_network_subnet=None): 

319 """Deallocate network resources for share server.""" 

320 allocations = self.db.network_allocations_get_for_share_server( 

321 context, share_server_id) 

322 for allocation in allocations: 

323 self.db.network_allocation_delete(context, allocation['id']) 

324 

325 def unmanage_network_allocations(self, context, share_server_id): 

326 self.deallocate_network(context, share_server_id) 

327 

328 def manage_network_allocations(self, context, allocations, share_server, 

329 share_network=None, 

330 share_network_subnet=None): 

331 if self.label != 'admin': 

332 self._verify_share_network_subnet(share_server['id'], 

333 share_network_subnet) 

334 else: 

335 share_network_subnet = share_network_subnet or {} 

336 self._save_network_info(context, share_network_subnet) 

337 

338 # We begin matching the allocations to known neutron ports and 

339 # finally return the non-consumed allocations 

340 remaining_allocations = list(allocations) 

341 

342 ips = [netaddr.IPAddress(allocation) for allocation 

343 in remaining_allocations] 

344 cidrs = [netaddr.IPNetwork(cidr) for cidr in self.allowed_cidrs] 

345 selected_allocations = [] 

346 

347 for ip in ips: 

348 if any(ip in cidr for cidr in cidrs): 

349 allocation = str(ip) 

350 selected_allocations.append(allocation) 

351 

352 for allocation in selected_allocations: 

353 data = { 

354 'share_server_id': share_server['id'], 

355 'ip_address': allocation, 

356 'status': constants.STATUS_ACTIVE, 

357 'label': self.label, 

358 'network_type': share_network_subnet['network_type'], 

359 'segmentation_id': share_network_subnet['segmentation_id'], 

360 'cidr': share_network_subnet['cidr'], 

361 'gateway': share_network_subnet['gateway'], 

362 'ip_version': share_network_subnet['ip_version'], 

363 'mtu': share_network_subnet['mtu'], 

364 } 

365 if self.label != 'admin': 

366 data['share_network_subnet_id'] = ( 

367 share_network_subnet['id']) 

368 self.db.network_allocation_create(context, data) 

369 remaining_allocations.remove(allocation) 

370 

371 return remaining_allocations