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
« 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.
16import netaddr
17from oslo_config import cfg
18from oslo_log import log
20from manila.common import constants
21from manila import exception
22from manila.i18n import _
23from manila import network
24from manila import utils
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]
63CONF = cfg.CONF
64LOG = log.getLogger(__name__)
67class StandaloneNetworkPlugin(network.NetworkBaseAPI):
68 """Standalone network plugin for share drivers.
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 """
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))
111 @property
112 def label(self):
113 return self._label
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)
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.")
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
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)
169 def _get_list_of_allowed_addresses(self):
170 """Returns list of CIDRs that can be used for getting IP addresses.
172 Reads information provided via configuration, such as gateway,
173 netmask, segmentation ID and allowed IP ranges, then performs
174 validation of provided data.
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)
205 range_instance = netaddr.IPRange(ip_range_start, ip_range_end)
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)
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))
230 return cidrs
232 def _get_available_ips(self, context, amount):
233 """Returns IP addresses from allowed IP range if there are unused IPs.
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)
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)
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)
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.
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
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'])
325 def unmanage_network_allocations(self, context, share_server_id):
326 self.deallocate_network(context, share_server_id)
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)
338 # We begin matching the allocations to known neutron ports and
339 # finally return the non-consumed allocations
340 remaining_allocations = list(allocations)
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 = []
347 for ip in ips:
348 if any(ip in cidr for cidr in cidrs):
349 allocation = str(ip)
350 selected_allocations.append(allocation)
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)
371 return remaining_allocations