Browse Source

Merge "Enable IPv6 in manila(network plugins and drivers)"

Jenkins 1 year ago
parent
commit
2f1536aec4
29 changed files with 686 additions and 85 deletions
  1. 35
    1
      manila/network/__init__.py
  2. 26
    2
      manila/network/neutron/neutron_network_plugin.py
  3. 40
    6
      manila/network/standalone_network_plugin.py
  4. 12
    0
      manila/scheduler/host_manager.py
  5. 2
    0
      manila/scheduler/utils.py
  6. 23
    0
      manila/share/access.py
  7. 62
    0
      manila/share/driver.py
  8. 37
    11
      manila/share/drivers/helpers.py
  9. 30
    1
      manila/share/drivers/lvm.py
  10. 9
    9
      manila/share/drivers/service_instance.py
  11. 39
    1
      manila/tests/network/neutron/test_neutron_plugin.py
  12. 12
    7
      manila/tests/network/test_standalone_network_plugin.py
  13. 31
    5
      manila/tests/scheduler/test_host_manager.py
  14. 2
    0
      manila/tests/share/drivers/cephfs/test_driver.py
  15. 2
    0
      manila/tests/share/drivers/container/test_driver.py
  16. 2
    2
      manila/tests/share/drivers/dell_emc/test_driver.py
  17. 2
    0
      manila/tests/share/drivers/glusterfs/test_glusterfs_native.py
  18. 3
    0
      manila/tests/share/drivers/hdfs/test_hdfs_native.py
  19. 6
    0
      manila/tests/share/drivers/hpe/test_hpe_3par_driver.py
  20. 2
    0
      manila/tests/share/drivers/huawei/test_huawei_nas.py
  21. 66
    18
      manila/tests/share/drivers/test_helpers.py
  22. 23
    1
      manila/tests/share/drivers/test_lvm.py
  23. 48
    16
      manila/tests/share/drivers/test_service_instance.py
  24. 2
    0
      manila/tests/share/drivers/zfsonlinux/test_driver.py
  25. 53
    0
      manila/tests/share/test_access.py
  26. 69
    0
      manila/tests/share/test_driver.py
  27. 27
    0
      manila/tests/test_network.py
  28. 13
    5
      manila/utils.py
  29. 8
    0
      releasenotes/notes/support-ipv6-in-drivers-and-network-plugins-1833121513edb13d.yaml

+ 35
- 1
manila/network/__init__.py View File

@@ -32,6 +32,19 @@ network_opts = [
32 32
         help='The full class name of the Networking API class to use.'),
33 33
 ]
34 34
 
35
+network_base_opts = [
36
+    cfg.BoolOpt(
37
+        'network_plugin_ipv4_enabled',
38
+        default=True,
39
+        help="Whether to support IPv4 network resource, Default=True."),
40
+    cfg.BoolOpt(
41
+        'network_plugin_ipv6_enabled',
42
+        default=False,
43
+        help="Whether to support IPv6 network resource, Default=False. "
44
+             "If this option is True, the value of "
45
+             "'network_plugin_ipv4_enabled' will be ignored."),
46
+]
47
+
35 48
 CONF = cfg.CONF
36 49
 
37 50
 
@@ -55,7 +68,14 @@ def API(config_group_name=None, label='user'):
55 68
 class NetworkBaseAPI(db_base.Base):
56 69
     """User network plugin for setting up main net interfaces."""
57 70
 
58
-    def __init__(self, db_driver=None):
71
+    def __init__(self, config_group_name=None, db_driver=None):
72
+        if config_group_name:
73
+            CONF.register_opts(network_base_opts,
74
+                               group=config_group_name)
75
+        else:
76
+            CONF.register_opts(network_base_opts)
77
+        self.configuration = getattr(CONF,
78
+                                     six.text_type(config_group_name), CONF)
59 79
         super(NetworkBaseAPI, self).__init__(db_driver=db_driver)
60 80
 
61 81
     def _verify_share_network(self, share_server_id, share_network):
@@ -84,3 +104,17 @@ class NetworkBaseAPI(db_base.Base):
84 104
     @abc.abstractmethod
85 105
     def deallocate_network(self, context, share_server_id):
86 106
         pass
107
+
108
+    @property
109
+    def enabled_ip_version(self):
110
+        if not hasattr(self, '_enabled_ip_version'):
111
+            if self.configuration.network_plugin_ipv6_enabled:
112
+                self._enabled_ip_version = 6
113
+            elif self.configuration.network_plugin_ipv4_enabled:
114
+                self._enabled_ip_version = 4
115
+            else:
116
+                msg = _("Either 'network_plugin_ipv4_enabled' or "
117
+                        "'network_plugin_ipv6_enabled' "
118
+                        "should be configured to 'True'.")
119
+                raise exception.NetworkBadConfigurationException(reason=msg)
120
+        return self._enabled_ip_version

+ 26
- 2
manila/network/neutron/neutron_network_plugin.py View File

@@ -14,6 +14,8 @@
14 14
 #    License for the specific language governing permissions and limitations
15 15
 #    under the License.
16 16
 
17
+import ipaddress
18
+import six
17 19
 import socket
18 20
 
19 21
 from oslo_config import cfg
@@ -99,7 +101,10 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI):
99 101
 
100 102
     def __init__(self, *args, **kwargs):
101 103
         db_driver = kwargs.pop('db_driver', None)
102
-        super(NeutronNetworkPlugin, self).__init__(db_driver=db_driver)
104
+        config_group_name = kwargs.get('config_group_name', 'DEFAULT')
105
+        super(NeutronNetworkPlugin,
106
+              self).__init__(config_group_name=config_group_name,
107
+                             db_driver=db_driver)
103 108
         self._neutron_api = None
104 109
         self._neutron_api_args = args
105 110
         self._neutron_api_kwargs = kwargs
@@ -156,6 +161,23 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI):
156 161
 
157 162
         return ports
158 163
 
164
+    def _get_matched_ip_address(self, fixed_ips, ip_version):
165
+        """Get first ip address which matches the specified ip_version."""
166
+
167
+        for ip in fixed_ips:
168
+            try:
169
+                address = ipaddress.ip_address(six.text_type(ip['ip_address']))
170
+                if address.version == ip_version:
171
+                    return ip['ip_address']
172
+            except ValueError:
173
+                LOG.error("%(address)s isn't a valid ip "
174
+                          "address, omitted."), {'address':
175
+                                                 ip['ip_address']}
176
+        msg = _("Can not find any IP address with configured IP "
177
+                "version %(version)s in share-network.") % {'version':
178
+                                                            ip_version}
179
+        raise exception.NetworkBadConfigurationException(reason=msg)
180
+
159 181
     def deallocate_network(self, context, share_server_id):
160 182
         """Deallocate neutron network resources for the given share server.
161 183
 
@@ -188,10 +210,12 @@ class NeutronNetworkPlugin(network.NetworkBaseAPI):
188 210
         port = self.neutron_api.create_port(
189 211
             share_network['project_id'], **create_args)
190 212
 
213
+        ip_address = self._get_matched_ip_address(port['fixed_ips'],
214
+                                                  share_network['ip_version'])
191 215
         port_dict = {
192 216
             'id': port['id'],
193 217
             'share_server_id': share_server['id'],
194
-            'ip_address': port['fixed_ips'][0]['ip_address'],
218
+            'ip_address': ip_address,
195 219
             'gateway': share_network['gateway'],
196 220
             'mac_address': port['mac_address'],
197 221
             'status': constants.STATUS_ACTIVE,

+ 40
- 6
manila/network/standalone_network_plugin.py View File

@@ -27,7 +27,7 @@ from manila import utils
27 27
 standalone_network_plugin_opts = [
28 28
     cfg.StrOpt(
29 29
         'standalone_network_plugin_gateway',
30
-        help="Gateway IPv4 address that should be used. Required.",
30
+        help="Gateway address that should be used. Required.",
31 31
         deprecated_group='DEFAULT'),
32 32
     cfg.StrOpt(
33 33
         'standalone_network_plugin_mask',
@@ -63,7 +63,12 @@ standalone_network_plugin_opts = [
63 63
         'standalone_network_plugin_ip_version',
64 64
         default=4,
65 65
         help="IP version of network. Optional."
66
-             "Allowed values are '4' and '6'. Default value is '4'.",
66
+             "Allowed values are '4' and '6'. Default value is '4'. "
67
+             "Note: This option is no longer used and has no effect",
68
+        deprecated_for_removal=True,
69
+        deprecated_reason="This option has been replaced by "
70
+                          "'network_plugin_ipv4_enabled' and "
71
+                          "'network_plugin_ipv6_enabled' options.",
67 72
         deprecated_group='DEFAULT'),
68 73
     cfg.IntOpt(
69 74
         'standalone_network_plugin_mtu',
@@ -89,8 +94,10 @@ class StandaloneNetworkPlugin(network.NetworkBaseAPI):
89 94
     """
90 95
 
91 96
     def __init__(self, config_group_name=None, db_driver=None, label='user'):
92
-        super(StandaloneNetworkPlugin, self).__init__(db_driver=db_driver)
93 97
         self.config_group_name = config_group_name or 'DEFAULT'
98
+        super(StandaloneNetworkPlugin,
99
+              self).__init__(config_group_name=self.config_group_name,
100
+                             db_driver=db_driver)
94 101
         CONF.register_opts(
95 102
             standalone_network_plugin_opts,
96 103
             group=self.config_group_name)
@@ -125,6 +132,34 @@ class StandaloneNetworkPlugin(network.NetworkBaseAPI):
125 132
 
126 133
     def _set_persistent_network_data(self):
127 134
         """Sets persistent data for whole plugin."""
135
+        # NOTE(tommylikehu): Standalone plugin could only support
136
+        # either IPv4 or IPv6, so if both network_plugin_ipv4_enabled
137
+        # and network_plugin_ipv6_enabled are configured True
138
+        # we would only support IPv6.
139
+        ipv4_enabled = getattr(self.configuration,
140
+                               'network_plugin_ipv4_enabled', None)
141
+        ipv6_enabled = getattr(self.configuration,
142
+                               'network_plugin_ipv6_enabled', None)
143
+
144
+        if ipv4_enabled:
145
+            ip_version = 4
146
+        if ipv6_enabled:
147
+            ip_version = 6
148
+        if ipv4_enabled and ipv6_enabled:
149
+            LOG.warning("Only IPv6 is enabled, although both "
150
+                        "'network_plugin_ipv4_enabled' and "
151
+                        "'network_plugin_ipv6_enabled' are "
152
+                        "configured True.")
153
+
154
+        if not (ipv4_enabled or ipv6_enabled):
155
+            ip_version = int(
156
+                self.configuration.standalone_network_plugin_ip_version)
157
+            LOG.warning("You're using a deprecated option that may"
158
+                        " be removed and silently ignored in the future. "
159
+                        "Please use 'network_plugin_ipv4_enabled' or "
160
+                        "'network_plugin_ipv6_enabled' instead of "
161
+                        "'standalone_network_plugin_ip_version'.")
162
+
128 163
         self.network_type = (
129 164
             self.configuration.standalone_network_plugin_network_type)
130 165
         self.segmentation_id = (
@@ -133,8 +168,7 @@ class StandaloneNetworkPlugin(network.NetworkBaseAPI):
133 168
         self.mask = self.configuration.standalone_network_plugin_mask
134 169
         self.allowed_ip_ranges = (
135 170
             self.configuration.standalone_network_plugin_allowed_ip_ranges)
136
-        self.ip_version = int(
137
-            self.configuration.standalone_network_plugin_ip_version)
171
+        self.ip_version = ip_version
138 172
         self.net = self._get_network()
139 173
         self.allowed_cidrs = self._get_list_of_allowed_addresses()
140 174
         self.reserved_addresses = (
@@ -191,7 +225,7 @@ class StandaloneNetworkPlugin(network.NetworkBaseAPI):
191 225
                     msg = _("Config option "
192 226
                             "'standalone_network_plugin_allowed_ip_ranges' "
193 227
                             "has incorrect value "
194
-                            "'%s'") % self.allowed_ip_ranges
228
+                            "'%s'.") % self.allowed_ip_ranges
195 229
                     raise exception.NetworkBadConfigurationException(
196 230
                         reason=msg)
197 231
 

+ 12
- 0
manila/scheduler/host_manager.py View File

@@ -143,6 +143,8 @@ class HostState(object):
143 143
         self.compression = False
144 144
         self.replication_type = None
145 145
         self.replication_domain = None
146
+        self.ipv4_support = None
147
+        self.ipv6_support = None
146 148
 
147 149
         # PoolState for all pools
148 150
         self.pools = {}
@@ -332,6 +334,12 @@ class HostState(object):
332 334
             pool_cap['sg_consistent_snapshot_support'] = (
333 335
                 self.sg_consistent_snapshot_support)
334 336
 
337
+        if self.ipv4_support is not None:
338
+            pool_cap['ipv4_support'] = self.ipv4_support
339
+
340
+        if self.ipv6_support is not None:
341
+            pool_cap['ipv6_support'] = self.ipv6_support
342
+
335 343
     def update_backend(self, capability):
336 344
         self.share_backend_name = capability.get('share_backend_name')
337 345
         self.vendor_name = capability.get('vendor_name')
@@ -351,6 +359,10 @@ class HostState(object):
351 359
         self.replication_domain = capability.get('replication_domain')
352 360
         self.sg_consistent_snapshot_support = capability.get(
353 361
             'share_group_stats', {}).get('consistent_snapshot_support')
362
+        if capability.get('ipv4_support') is not None:
363
+            self.ipv4_support = capability['ipv4_support']
364
+        if capability.get('ipv6_support') is not None:
365
+            self.ipv6_support = capability['ipv6_support']
354 366
 
355 367
     def consume_from_share(self, share):
356 368
         """Incrementally update host state from an share."""

+ 2
- 0
manila/scheduler/utils.py View File

@@ -55,6 +55,8 @@ def generate_stats(host_state, properties):
55 55
             host_state.max_over_subscription_ratio,
56 56
         'sg_consistent_snapshot_support': (
57 57
             host_state.sg_consistent_snapshot_support),
58
+        'ipv4_support': host_state.ipv4_support,
59
+        'ipv6_support': host_state.ipv6_support,
58 60
     }
59 61
 
60 62
     host_caps = host_state.capabilities

+ 23
- 0
manila/share/access.py View File

@@ -14,6 +14,7 @@
14 14
 #    under the License.
15 15
 
16 16
 import copy
17
+import ipaddress
17 18
 
18 19
 from oslo_log import log
19 20
 
@@ -21,6 +22,8 @@ from manila.common import constants
21 22
 from manila.i18n import _
22 23
 from manila import utils
23 24
 
25
+import six
26
+
24 27
 LOG = log.getLogger(__name__)
25 28
 
26 29
 
@@ -459,6 +462,18 @@ class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin):
459 462
 
460 463
         return access_rules_to_be_on_share
461 464
 
465
+    @staticmethod
466
+    def _filter_ipv6_rules(rules, share_instance_proto):
467
+        filtered = []
468
+        for rule in rules:
469
+            if rule['access_type'] == 'ip' and share_instance_proto == 'nfs':
470
+                ip_version = ipaddress.ip_network(
471
+                    six.text_type(rule['access_to'])).version
472
+                if 6 == ip_version:
473
+                    continue
474
+            filtered.append(rule)
475
+        return filtered
476
+
462 477
     def _get_rules_to_send_to_driver(self, context, share_instance):
463 478
         add_rules = []
464 479
         delete_rules = []
@@ -472,6 +487,7 @@ class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin):
472 487
             share_instance_id=share_instance['id'])
473 488
         # Update queued rules to transitional states
474 489
         for rule in existing_rules_in_db:
490
+
475 491
             if rule['state'] == constants.ACCESS_STATE_APPLYING:
476 492
                 add_rules.append(rule)
477 493
             elif rule['state'] == constants.ACCESS_STATE_DENYING:
@@ -480,6 +496,13 @@ class ShareInstanceAccess(ShareInstanceAccessDatabaseMixin):
480 496
         access_rules_to_be_on_share = [
481 497
             r for r in existing_rules_in_db if r['id'] not in delete_rule_ids
482 498
         ]
499
+        share = self.db.share_get(context, share_instance['share_id'])
500
+        si_proto = share['share_proto'].lower()
501
+        if not self.driver.ipv6_implemented:
502
+            add_rules = self._filter_ipv6_rules(add_rules, si_proto)
503
+            delete_rules = self._filter_ipv6_rules(delete_rules, si_proto)
504
+            access_rules_to_be_on_share = self._filter_ipv6_rules(
505
+                access_rules_to_be_on_share, si_proto)
483 506
         return access_rules_to_be_on_share, add_rules, delete_rules
484 507
 
485 508
     def _check_needs_refresh(self, context, share_instance_id):

+ 62
- 0
manila/share/driver.py View File

@@ -252,6 +252,8 @@ class ShareDriver(object):
252 252
         self.configuration = kwargs.get('configuration', None)
253 253
         self.initialized = False
254 254
         self._stats = {}
255
+        self.ip_version = None
256
+        self.ipv6_implemented = False
255 257
 
256 258
         self.pools = []
257 259
         if self.configuration:
@@ -1116,6 +1118,7 @@ class ShareDriver(object):
1116 1118
             replication_domain=self.replication_domain,
1117 1119
             filter_function=self.get_filter_function(),
1118 1120
             goodness_function=self.get_goodness_function(),
1121
+            ipv4_support=True,
1119 1122
         )
1120 1123
         if isinstance(data, dict):
1121 1124
             common.update(data)
@@ -1126,6 +1129,7 @@ class ShareDriver(object):
1126 1129
                 'consistent_snapshot_support'),
1127 1130
         }
1128 1131
 
1132
+        self.add_ip_version_capability(common)
1129 1133
         self._stats = common
1130 1134
 
1131 1135
     def get_share_server_pools(self, share_server):
@@ -2446,3 +2450,61 @@ class ShareDriver(object):
2446 2450
         LOG.debug("This backend does not support gathering 'used_size' of "
2447 2451
                   "shares created on it.")
2448 2452
         return []
2453
+
2454
+    def get_configured_ip_version(self):
2455
+        """"Get Configured IP versions when DHSS is false.
2456
+
2457
+        The supported versions are returned with list, possible
2458
+        values are: [4], [6] or [4, 6]
2459
+        Each driver could override the method to return the IP version
2460
+        which represents its self configuration.
2461
+        """
2462
+
2463
+        # For drivers that haven't implemented IPv6, assume legacy behavior
2464
+        if not self.ipv6_implemented:
2465
+            return [4]
2466
+
2467
+        raise NotImplementedError()
2468
+
2469
+    def add_ip_version_capability(self, data):
2470
+        """Add IP version support capabilities.
2471
+
2472
+        When DHSS is true, the capabilities are determined by driver
2473
+        and configured network plugin.
2474
+        When DHSS is false, the capabilities are determined by driver and its
2475
+        configuration.
2476
+        :param data: the capability dictionary
2477
+        :returns: capability data
2478
+        """
2479
+        ipv4_support = data.get('ipv4_support', False)
2480
+        ipv6_support = data.get('ipv6_support', False)
2481
+        if self.ip_version is None:
2482
+            if self.driver_handles_share_servers:
2483
+                user_network_version = self.network_api.enabled_ip_version
2484
+                if self.admin_network_api:
2485
+                    if (user_network_version ==
2486
+                            self.admin_network_api.enabled_ip_version):
2487
+                        self.ip_version = user_network_version
2488
+                    else:
2489
+                        LOG.warning("The enabled IP version for the admin "
2490
+                                    "network plugin is different from "
2491
+                                    "that of user network plugin, this "
2492
+                                    "may lead to the backend never being "
2493
+                                    "chosen by the scheduler when ip "
2494
+                                    "version is specified in the share "
2495
+                                    "type.")
2496
+                else:
2497
+                    self.ip_version = user_network_version
2498
+            else:
2499
+                self.ip_version = self.get_configured_ip_version()
2500
+
2501
+        if not isinstance(self.ip_version, list):
2502
+            self.ip_version = [self.ip_version]
2503
+
2504
+        data['ipv4_support'] = (4 in self.ip_version) and ipv4_support
2505
+        data['ipv6_support'] = (6 in self.ip_version) and ipv6_support
2506
+        if not (data['ipv4_support'] or data['ipv6_support']):
2507
+            LOG.error("Backend %s capabilities 'ipv4_support' "
2508
+                      "and 'ipv6_support' are both False.",
2509
+                      data['share_backend_name'])
2510
+        return data

+ 37
- 11
manila/share/drivers/helpers.py View File

@@ -13,6 +13,8 @@
13 13
 # License for the specific language governing permissions and limitations
14 14
 # under the License.
15 15
 
16
+import copy
17
+import netaddr
16 18
 import os
17 19
 import re
18 20
 
@@ -183,7 +185,20 @@ class NFSHelper(NASHelperBase):
183 185
 
184 186
     def create_exports(self, server, share_name, recreate=False):
185 187
         path = os.path.join(self.configuration.share_mount_path, share_name)
186
-        return self.get_exports_for_share(server, path)
188
+        server_copy = copy.copy(server)
189
+        public_addresses = []
190
+        if 'public_addresses' in server_copy:
191
+            for address in server_copy['public_addresses']:
192
+                public_addresses.append(
193
+                    self._get_parsed_address_or_cidr(address))
194
+            server_copy['public_addresses'] = public_addresses
195
+
196
+        for t in ['public_address', 'admin_ip', 'ip']:
197
+            address = server_copy.get(t)
198
+            if address is not None:
199
+                server_copy[t] = self._get_parsed_address_or_cidr(address)
200
+
201
+        return self.get_exports_for_share(server_copy, path)
187 202
 
188 203
     def init_helper(self, server):
189 204
         try:
@@ -198,12 +213,6 @@ class NFSHelper(NASHelperBase):
198 213
     def remove_exports(self, server, share_name):
199 214
         """Remove exports."""
200 215
 
201
-    def _get_parsed_access_to(self, access_to):
202
-        netmask = utils.cidr_to_netmask(access_to)
203
-        if netmask == '255.255.255.255':
204
-            return access_to.split('/')[0]
205
-        return access_to.split('/')[0] + '/' + netmask
206
-
207 216
     @nfs_synchronized
208 217
     def update_access(self, server, share_name, access_rules, add_rules,
209 218
                       delete_rules):
@@ -234,8 +243,9 @@ class NFSHelper(NASHelperBase):
234 243
                     server,
235 244
                     ['sudo', 'exportfs', '-o',
236 245
                      rules_options % access['access_level'],
237
-                     ':'.join((self._get_parsed_access_to(access['access_to']),
238
-                               local_path))])
246
+                     ':'.join((
247
+                         self._get_parsed_address_or_cidr(access['access_to']),
248
+                         local_path))])
239 249
             self._sync_nfs_temp_and_perm_files(server)
240 250
         # Adding/Deleting specific rules
241 251
         else:
@@ -245,7 +255,7 @@ class NFSHelper(NASHelperBase):
245 255
                 (const.ACCESS_LEVEL_RO, const.ACCESS_LEVEL_RW))
246 256
 
247 257
             for access in delete_rules:
248
-                access['access_to'] = self._get_parsed_access_to(
258
+                access['access_to'] = self._get_parsed_address_or_cidr(
249 259
                     access['access_to'])
250 260
                 try:
251 261
                     self.validate_access_rules(
@@ -265,7 +275,7 @@ class NFSHelper(NASHelperBase):
265 275
             if delete_rules:
266 276
                 self._sync_nfs_temp_and_perm_files(server)
267 277
             for access in add_rules:
268
-                access['access_to'] = self._get_parsed_access_to(
278
+                access['access_to'] = self._get_parsed_address_or_cidr(
269 279
                     access['access_to'])
270 280
                 found_item = re.search(
271 281
                     re.escape(local_path) + '[\s\n]*' + re.escape(
@@ -290,6 +300,22 @@ class NFSHelper(NASHelperBase):
290 300
             if add_rules:
291 301
                 self._sync_nfs_temp_and_perm_files(server)
292 302
 
303
+    def _get_parsed_address_or_cidr(self, access_to):
304
+        try:
305
+            network = netaddr.IPNetwork(access_to)
306
+        except netaddr.AddrFormatError:
307
+            raise exception.InvalidInput(
308
+                reason=_("Invalid address or cidr supplied %s.") % access_to)
309
+        mask_length = network.netmask.netmask_bits()
310
+        address = access_to.split('/')[0]
311
+        if network.version == 4:
312
+            if mask_length == 32:
313
+                return address
314
+            return '%s/%s' % (address, mask_length)
315
+        if mask_length == 128:
316
+            return "[%s]" % address
317
+        return "[%s]/%s" % (address, mask_length)
318
+
293 319
     @staticmethod
294 320
     def get_host_list(output, local_path):
295 321
         entries = []

+ 30
- 1
manila/share/drivers/lvm.py View File

@@ -18,6 +18,7 @@ LVM Driver for shares.
18 18
 
19 19
 """
20 20
 
21
+import ipaddress
21 22
 import math
22 23
 import os
23 24
 import re
@@ -154,6 +155,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
154 155
         self.configuration.share_mount_path = (
155 156
             self.configuration.lvm_share_export_root)
156 157
         self._helpers = None
158
+        self.configured_ip_version = None
157 159
         self.backend_name = self.configuration.safe_get(
158 160
             'share_backend_name') or 'LVM'
159 161
         # Set of parameters used for compatibility with
@@ -168,6 +170,7 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
168 170
         else:
169 171
             self.share_server['public_addresses'] = (
170 172
                 self.configuration.lvm_share_export_ips)
173
+        self.ipv6_implemented = True
171 174
 
172 175
     def _ssh_exec_as_root(self, server, command, check_exit_code=True):
173 176
         kwargs = {}
@@ -212,7 +215,8 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
212 215
             'revert_to_snapshot_support': True,
213 216
             'mount_snapshot_support': True,
214 217
             'driver_name': 'LVMShareDriver',
215
-            'pools': self.get_share_server_pools()
218
+            'pools': self.get_share_server_pools(),
219
+            'ipv6_support': True
216 220
         }
217 221
         super(LVMShareDriver, self)._update_share_stats(data)
218 222
 
@@ -431,6 +435,31 @@ class LVMShareDriver(LVMMixin, driver.ShareDriver):
431 435
         super(LVMShareDriver, self).delete_snapshot(context, snapshot,
432 436
                                                     share_server)
433 437
 
438
+    def get_configured_ip_version(self):
439
+        """"Get Configured IP versions when DHSS is false."""
440
+        if self.configured_ip_version is None:
441
+            try:
442
+                self.configured_ip_version = []
443
+                if self.configuration.lvm_share_export_ip:
444
+                    self.configured_ip_version.append(ipaddress.ip_address(
445
+                        six.text_type(
446
+                            self.configuration.lvm_share_export_ip)).version)
447
+                else:
448
+                    for ip in self.configuration.lvm_share_export_ips:
449
+                        self.configured_ip_version.append(
450
+                            ipaddress.ip_address(six.text_type(ip)).version)
451
+            except Exception:
452
+                if self.configuration.lvm_share_export_ip:
453
+                    message = (_("Invalid 'lvm_share_export_ip' option "
454
+                                 "supplied %s.") %
455
+                               self.configuration.lvm_share_export_ip)
456
+                else:
457
+                    message = (_("Invalid 'lvm_share_export_ips' option "
458
+                                 "supplied %s.") %
459
+                               self.configuration.lvm_share_export_ips)
460
+                raise exception.InvalidInput(reason=message)
461
+        return self.configured_ip_version
462
+
434 463
     def snapshot_update_access(self, context, snapshot, access_rules,
435 464
                                add_rules, delete_rules, share_server=None):
436 465
         """Update access rules for given snapshot.

+ 9
- 9
manila/share/drivers/service_instance.py View File

@@ -121,13 +121,13 @@ no_share_servers_handling_mode_opts = [
121 121
         "service_net_name_or_ip",
122 122
         help="Can be either name of network that is used by service "
123 123
              "instance within Nova to get IP address or IP address itself "
124
-             "for managing shares there. "
124
+             "(either IPv4 or IPv6) for managing shares there. "
125 125
              "Used only when share servers handling is disabled."),
126 126
     cfg.HostAddressOpt(
127 127
         "tenant_net_name_or_ip",
128 128
         help="Can be either name of network that is used by service "
129 129
              "instance within Nova to get IP address or IP address itself "
130
-             "for exporting shares. "
130
+             "(either IPv4 or IPv6) for exporting shares. "
131 131
              "Used only when share servers handling is disabled."),
132 132
 ]
133 133
 
@@ -241,13 +241,13 @@ class ServiceInstanceManager(object):
241 241
             self.admin_context,
242 242
             self.get_config_option('service_instance_name_or_id'))
243 243
 
244
-        if netutils.is_valid_ipv4(data['service_net_name_or_ip']):
244
+        if netutils.is_valid_ip(data['service_net_name_or_ip']):
245 245
             data['private_address'] = [data['service_net_name_or_ip']]
246 246
         else:
247 247
             data['private_address'] = self._get_addresses_by_network_name(
248 248
                 data['service_net_name_or_ip'], data['instance'])
249 249
 
250
-        if netutils.is_valid_ipv4(data['tenant_net_name_or_ip']):
250
+        if netutils.is_valid_ip(data['tenant_net_name_or_ip']):
251 251
             data['public_address'] = [data['tenant_net_name_or_ip']]
252 252
         else:
253 253
             data['public_address'] = self._get_addresses_by_network_name(
@@ -267,13 +267,13 @@ class ServiceInstanceManager(object):
267 267
             'instance_id': data['instance']['id'],
268 268
         }
269 269
         for key in ('private_address', 'public_address'):
270
-            data[key + '_v4'] = None
270
+            data[key + '_first'] = None
271 271
             for address in data[key]:
272
-                if netutils.is_valid_ipv4(address):
273
-                    data[key + '_v4'] = address
272
+                if netutils.is_valid_ip(address):
273
+                    data[key + '_first'] = address
274 274
                     break
275
-        share_server['ip'] = data['private_address_v4']
276
-        share_server['public_address'] = data['public_address_v4']
275
+        share_server['ip'] = data['private_address_first']
276
+        share_server['public_address'] = data['public_address_first']
277 277
         return {'backend_details': share_server}
278 278
 
279 279
     def _get_addresses_by_network_name(self, net_name, server):

+ 39
- 1
manila/tests/network/neutron/test_neutron_plugin.py View File

@@ -44,7 +44,7 @@ fake_neutron_port = {
44 44
     "binding:capabilities": {"port_filter": True},
45 45
     "mac_address": "test_mac",
46 46
     "fixed_ips": [
47
-        {"subnet_id": "test_subnet_id", "ip_address": "test_ip"},
47
+        {"subnet_id": "test_subnet_id", "ip_address": "203.0.113.100"},
48 48
     ],
49 49
     "id": "test_port_id",
50 50
     "security_groups": ["fake_sec_group_id"],
@@ -1505,6 +1505,7 @@ class NeutronBindNetworkPluginWithNormalTypeTest(test.TestCase):
1505 1505
             [fake_neutron_port], fake_share_server)
1506 1506
 
1507 1507
 
1508
+@ddt.ddt
1508 1509
 class NeutronBindSingleNetworkPluginWithNormalTypeTest(test.TestCase):
1509 1510
     def setUp(self):
1510 1511
         super(NeutronBindSingleNetworkPluginWithNormalTypeTest, self).setUp()
@@ -1588,3 +1589,40 @@ class NeutronBindSingleNetworkPluginWithNormalTypeTest(test.TestCase):
1588 1589
 
1589 1590
         self.bind_plugin._wait_for_ports_bind.assert_called_once_with(
1590 1591
             [fake_neutron_port], fake_share_server)
1592
+
1593
+    @ddt.data({'fix_ips': [{'ip_address': 'test_ip'},
1594
+                           {'ip_address': '10.78.223.129'}],
1595
+               'ip_version': 4},
1596
+              {'fix_ips': [{'ip_address': 'test_ip'},
1597
+                           {'ip_address': 'ad80::abaa:0:c2:2'}],
1598
+               'ip_version': 6},
1599
+              {'fix_ips': [{'ip_address': '10.78.223.129'},
1600
+                           {'ip_address': 'ad80::abaa:0:c2:2'}],
1601
+               'ip_version': 6},
1602
+              )
1603
+    @ddt.unpack
1604
+    def test__get_matched_ip_address(self, fix_ips, ip_version):
1605
+        result = self.bind_plugin._get_matched_ip_address(fix_ips, ip_version)
1606
+        self.assertEqual(fix_ips[1]['ip_address'], result)
1607
+
1608
+    @ddt.data({'fix_ips': [{'ip_address': 'test_ip_1'},
1609
+                           {'ip_address': 'test_ip_2'}],
1610
+               'ip_version': (4, 6)},
1611
+              {'fix_ips': [{'ip_address': 'ad80::abaa:0:c2:1'},
1612
+                           {'ip_address': 'ad80::abaa:0:c2:2'}],
1613
+               'ip_version': (4, )},
1614
+              {'fix_ips': [{'ip_address': '192.0.0.2'},
1615
+                           {'ip_address': '192.0.0.3'}],
1616
+               'ip_version': (6, )},
1617
+              {'fix_ips': [{'ip_address': '192.0.0.2/12'},
1618
+                           {'ip_address': '192.0.0.330'},
1619
+                           {'ip_address': 'ad80::001::ad80'},
1620
+                           {'ip_address': 'ad80::abaa:0:c2:2/64'}],
1621
+               'ip_version': (4, 6)},
1622
+              )
1623
+    @ddt.unpack
1624
+    def test__get_matched_ip_address_illegal(self, fix_ips, ip_version):
1625
+        for version in ip_version:
1626
+            self.assertRaises(exception.NetworkBadConfigurationException,
1627
+                              self.bind_plugin._get_matched_ip_address,
1628
+                              fix_ips, version)

+ 12
- 7
manila/tests/network/test_standalone_network_plugin.py View File

@@ -70,7 +70,7 @@ class StandaloneNetworkPluginTest(test.TestCase):
70 70
                 'standalone_network_plugin_segmentation_id': 1001,
71 71
                 'standalone_network_plugin_allowed_ip_ranges': (
72 72
                     '10.0.0.3-10.0.0.7,10.0.0.69-10.0.0.157,10.0.0.213'),
73
-                'standalone_network_plugin_ip_version': 4,
73
+                'network_plugin_ipv4_enabled': True,
74 74
             },
75 75
         }
76 76
         allowed_cidrs = [
@@ -104,7 +104,7 @@ class StandaloneNetworkPluginTest(test.TestCase):
104 104
                 'standalone_network_plugin_gateway': (
105 105
                     '2001:cdba::3257:9652'),
106 106
                 'standalone_network_plugin_mask': '48',
107
-                'standalone_network_plugin_ip_version': 6,
107
+                'network_plugin_ipv6_enabled': True,
108 108
             },
109 109
         }
110 110
         with test_utils.create_temp_config_with_opts(data):
@@ -138,7 +138,7 @@ class StandaloneNetworkPluginTest(test.TestCase):
138 138
                 'standalone_network_plugin_segmentation_id': 3999,
139 139
                 'standalone_network_plugin_allowed_ip_ranges': (
140 140
                     '2001:db8::-2001:db8:0000:0000:0000:007f:ffff:ffff'),
141
-                'standalone_network_plugin_ip_version': 6,
141
+                'network_plugin_ipv6_enabled': True,
142 142
             },
143 143
         }
144 144
         with test_utils.create_temp_config_with_opts(data):
@@ -168,7 +168,7 @@ class StandaloneNetworkPluginTest(test.TestCase):
168 168
                 'standalone_network_plugin_mask': '255.255.0.0',
169 169
                 'standalone_network_plugin_network_type': network_type,
170 170
                 'standalone_network_plugin_segmentation_id': 1001,
171
-                'standalone_network_plugin_ip_version': 4,
171
+                'network_plugin_ipv4_enabled': True,
172 172
             },
173 173
         }
174 174
         with test_utils.create_temp_config_with_opts(data):
@@ -186,7 +186,7 @@ class StandaloneNetworkPluginTest(test.TestCase):
186 186
                 'standalone_network_plugin_mask': '255.255.0.0',
187 187
                 'standalone_network_plugin_network_type': fake_network_type,
188 188
                 'standalone_network_plugin_segmentation_id': 1001,
189
-                'standalone_network_plugin_ip_version': 4,
189
+                'network_plugin_ipv4_enabled': True,
190 190
             },
191 191
         }
192 192
         with test_utils.create_temp_config_with_opts(data):
@@ -255,10 +255,15 @@ class StandaloneNetworkPluginTest(test.TestCase):
255 255
         data = {
256 256
             group_name: {
257 257
                 'standalone_network_plugin_gateway': gateway,
258
-                'standalone_network_plugin_ip_version': vers,
259 258
                 'standalone_network_plugin_mask': '25',
260 259
             },
261 260
         }
261
+        if vers == 4:
262
+            data[group_name]['network_plugin_ipv4_enabled'] = True
263
+        if vers == 6:
264
+            data[group_name]['network_plugin_ipv4_enabled'] = False
265
+            data[group_name]['network_plugin_ipv6_enabled'] = True
266
+
262 267
         with test_utils.create_temp_config_with_opts(data):
263 268
             self.assertRaises(
264 269
                 exception.NetworkBadConfigurationException,
@@ -319,7 +324,7 @@ class StandaloneNetworkPluginTest(test.TestCase):
319 324
             'DEFAULT': {
320 325
                 'standalone_network_plugin_gateway': '2001:db8::0001',
321 326
                 'standalone_network_plugin_mask': '64',
322
-                'standalone_network_plugin_ip_version': 6,
327
+                'network_plugin_ipv6_enabled': True,
323 328
             },
324 329
         }
325 330
         with test_utils.create_temp_config_with_opts(data):

+ 31
- 5
manila/tests/scheduler/test_host_manager.py View File

@@ -637,7 +637,9 @@ class HostStateTestCase(test.TestCase):
637 637
         share_capability = {'total_capacity_gb': 0,
638 638
                             'free_capacity_gb': 100,
639 639
                             'reserved_percentage': 0,
640
-                            'timestamp': None}
640
+                            'timestamp': None,
641
+                            'ipv4_support': True,
642
+                            'ipv6_support': False}
641 643
         fake_host = host_manager.HostState('host1', share_capability)
642 644
         self.assertIsNone(fake_host.free_capacity_gb)
643 645
 
@@ -646,9 +648,13 @@ class HostStateTestCase(test.TestCase):
646 648
         # Backend level stats remain uninitialized
647 649
         self.assertEqual(0, fake_host.total_capacity_gb)
648 650
         self.assertIsNone(fake_host.free_capacity_gb)
651
+        self.assertTrue(fake_host.ipv4_support)
652
+        self.assertFalse(fake_host.ipv6_support)
649 653
         # Pool stats has been updated
650 654
         self.assertEqual(0, fake_host.pools['_pool0'].total_capacity_gb)
651 655
         self.assertEqual(100, fake_host.pools['_pool0'].free_capacity_gb)
656
+        self.assertTrue(fake_host.pools['_pool0'].ipv4_support)
657
+        self.assertFalse(fake_host.pools['_pool0'].ipv6_support)
652 658
 
653 659
         # Test update for existing host state
654 660
         share_capability.update(dict(total_capacity_gb=1000))
@@ -674,6 +680,8 @@ class HostStateTestCase(test.TestCase):
674 680
             'vendor_name': 'OpenStack',
675 681
             'driver_version': '1.1',
676 682
             'storage_protocol': 'NFS_CIFS',
683
+            'ipv4_support': True,
684
+            'ipv6_support': False,
677 685
             'pools': [
678 686
                 {'pool_name': 'pool1',
679 687
                  'total_capacity_gb': 500,
@@ -707,6 +715,8 @@ class HostStateTestCase(test.TestCase):
707 715
         self.assertEqual('NFS_CIFS', fake_host.storage_protocol)
708 716
         self.assertEqual('OpenStack', fake_host.vendor_name)
709 717
         self.assertEqual('1.1', fake_host.driver_version)
718
+        self.assertTrue(fake_host.ipv4_support)
719
+        self.assertFalse(fake_host.ipv6_support)
710 720
 
711 721
         # Backend level stats remain uninitialized
712 722
         self.assertEqual(0, fake_host.total_capacity_gb)
@@ -716,8 +726,12 @@ class HostStateTestCase(test.TestCase):
716 726
 
717 727
         self.assertEqual(500, fake_host.pools['pool1'].total_capacity_gb)
718 728
         self.assertEqual(230, fake_host.pools['pool1'].free_capacity_gb)
729
+        self.assertTrue(fake_host.pools['pool1'].ipv4_support)
730
+        self.assertFalse(fake_host.pools['pool1'].ipv6_support)
719 731
         self.assertEqual(1024, fake_host.pools['pool2'].total_capacity_gb)
720 732
         self.assertEqual(1024, fake_host.pools['pool2'].free_capacity_gb)
733
+        self.assertTrue(fake_host.pools['pool2'].ipv4_support)
734
+        self.assertFalse(fake_host.pools['pool2'].ipv6_support)
721 735
 
722 736
         capability = {
723 737
             'share_backend_name': 'Backend1',
@@ -872,14 +886,17 @@ class PoolStateTestCase(test.TestCase):
872 886
             'share_capability':
873 887
                 {'total_capacity_gb': 1024, 'free_capacity_gb': 512,
874 888
                  'reserved_percentage': 0, 'timestamp': None,
875
-                 'cap1': 'val1', 'cap2': 'val2'},
889
+                 'cap1': 'val1', 'cap2': 'val2', 'ipv4_support': True,
890
+                 'ipv6_support': False},
876 891
             'instances': []
877 892
         },
878 893
         {
879 894
             'share_capability':
880 895
                 {'total_capacity_gb': 1024, 'free_capacity_gb': 512,
881 896
                  'allocated_capacity_gb': 256, 'reserved_percentage': 0,
882
-                 'timestamp': None, 'cap1': 'val1', 'cap2': 'val2'},
897
+                 'timestamp': None, 'cap1': 'val1', 'cap2': 'val2',
898
+                 'ipv4_support': False, 'ipv6_support': True
899
+                 },
883 900
             'instances':
884 901
                 [
885 902
                     {
@@ -894,14 +911,17 @@ class PoolStateTestCase(test.TestCase):
894 911
             'share_capability':
895 912
                 {'total_capacity_gb': 1024, 'free_capacity_gb': 512,
896 913
                  'allocated_capacity_gb': 256, 'reserved_percentage': 0,
897
-                 'timestamp': None, 'cap1': 'val1', 'cap2': 'val2'},
914
+                 'timestamp': None, 'cap1': 'val1', 'cap2': 'val2',
915
+                 'ipv4_support': True, 'ipv6_support': True},
898 916
             'instances': []
899 917
         },
900 918
         {
901 919
             'share_capability':
902 920
                 {'total_capacity_gb': 1024, 'free_capacity_gb': 512,
903 921
                  'provisioned_capacity_gb': 256, 'reserved_percentage': 0,
904
-                 'timestamp': None, 'cap1': 'val1', 'cap2': 'val2'},
922
+                 'timestamp': None, 'cap1': 'val1', 'cap2': 'val2',
923
+                 'ipv4_support': False, 'ipv6_support': False
924
+                 },
905 925
             'instances':
906 926
                 [
907 927
                     {
@@ -976,3 +996,9 @@ class PoolStateTestCase(test.TestCase):
976 996
                              fake_pool.allocated_capacity_gb)
977 997
             self.assertEqual(share_capability['provisioned_capacity_gb'],
978 998
                              fake_pool.provisioned_capacity_gb)
999
+        if 'ipv4_support' in share_capability:
1000
+            self.assertEqual(share_capability['ipv4_support'],
1001
+                             fake_pool.ipv4_support)
1002
+        if 'ipv6_support' in share_capability:
1003
+            self.assertEqual(share_capability['ipv6_support'],
1004
+                             fake_pool.ipv6_support)

+ 2
- 0
manila/tests/share/drivers/cephfs/test_driver.py View File

@@ -336,6 +336,8 @@ class CephFSDriverTestCase(test.TestCase):
336 336
         self._driver._update_share_stats()
337 337
         result = self._driver._stats
338 338
 
339
+        self.assertTrue(result['ipv4_support'])
340
+        self.assertFalse(result['ipv6_support'])
339 341
         self.assertEqual("CEPHFS", result['storage_protocol'])
340 342
 
341 343
     def test_module_missing(self):

+ 2
- 0
manila/tests/share/drivers/container/test_driver.py View File

@@ -105,6 +105,8 @@ class ContainerShareDriverTestCase(test.TestCase):
105 105
         self.assertEqual('ContainerShareDriver',
106 106
                          self._driver._stats['driver_name'])
107 107
         self.assertEqual('test-pool', self._driver._stats['pools'])
108
+        self.assertTrue(self._driver._stats['ipv4_support'])
109
+        self.assertFalse(self._driver._stats['ipv6_support'])
108 110
 
109 111
     def test_create_share(self):
110 112
         helper = mock.Mock()

+ 2
- 2
manila/tests/share/drivers/dell_emc/test_driver.py View File

@@ -16,7 +16,6 @@
16 16
 import mock
17 17
 from stevedore import extension
18 18
 
19
-from manila import network
20 19
 from manila.share import configuration as conf
21 20
 from manila.share.drivers.dell_emc import driver as emcdriver
22 21
 from manila.share.drivers.dell_emc.plugins import base
@@ -98,7 +97,6 @@ class EMCShareFrameworkTestCase(test.TestCase):
98 97
         self.configuration.append_config_values = mock.Mock(return_value=0)
99 98
         self.configuration.share_backend_name = FAKE_BACKEND
100 99
         self.mock_object(self.configuration, 'safe_get', self._fake_safe_get)
101
-        self.mock_object(network, 'API')
102 100
         self.driver = emcdriver.EMCShareDriver(
103 101
             configuration=self.configuration)
104 102
 
@@ -133,6 +131,8 @@ class EMCShareFrameworkTestCase(test.TestCase):
133 131
         data['goodness_function'] = None
134 132
         data['snapshot_support'] = True
135 133
         data['create_share_from_snapshot_support'] = True
134
+        data['ipv4_support'] = True
135
+        data['ipv6_support'] = False
136 136
         self.assertEqual(data, self.driver._stats)
137 137
 
138 138
     def _fake_safe_get(self, value):

+ 2
- 0
manila/tests/share/drivers/glusterfs/test_glusterfs_native.py View File

@@ -265,6 +265,8 @@ class GlusterfsNativeShareDriverTestCase(test.TestCase):
265 265
             'replication_domain': None,
266 266
             'filter_function': None,
267 267
             'goodness_function': None,
268
+            'ipv4_support': True,
269
+            'ipv6_support': False,
268 270
         }
269 271
         self.assertEqual(test_data, self._driver._stats)
270 272
 

+ 3
- 0
manila/tests/share/drivers/hdfs/test_hdfs_native.py View File

@@ -409,9 +409,12 @@ class HDFSNativeShareDriverTestCase(test.TestCase):
409 409
             'free_capacity_gb', 'total_capacity_gb',
410 410
             'driver_handles_share_servers',
411 411
             'reserved_percentage', 'vendor_name', 'storage_protocol',
412
+            'ipv4_support', 'ipv6_support'
412 413
         ]
413 414
         for key in expected_keys:
414 415
             self.assertIn(key, result)
416
+        self.assertTrue(result['ipv4_support'])
417
+        self.assertFalse(False, result['ipv6_support'])
415 418
         self.assertEqual('HDFS', result['storage_protocol'])
416 419
         self._driver._get_available_capacity.assert_called_once_with()
417 420
 

+ 6
- 0
manila/tests/share/drivers/hpe/test_hpe_3par_driver.py View File

@@ -747,6 +747,8 @@ class HPE3ParDriverTestCase(test.TestCase):
747 747
             'replication_domain': None,
748 748
             'filter_function': None,
749 749
             'goodness_function': None,
750
+            'ipv4_support': True,
751
+            'ipv6_support': False,
750 752
         }
751 753
 
752 754
         result = self.driver.get_share_stats(refresh=True)
@@ -822,6 +824,8 @@ class HPE3ParDriverTestCase(test.TestCase):
822 824
             'replication_domain': None,
823 825
             'filter_function': None,
824 826
             'goodness_function': None,
827
+            'ipv4_support': True,
828
+            'ipv6_support': False,
825 829
         }
826 830
 
827 831
         result = self.driver.get_share_stats(refresh=True)
@@ -864,6 +868,8 @@ class HPE3ParDriverTestCase(test.TestCase):
864 868
             'replication_domain': None,
865 869
             'filter_function': None,
866 870
             'goodness_function': None,
871
+            'ipv4_support': True,
872
+            'ipv6_support': False,
867 873
         }
868 874
 
869 875
         result = self.driver.get_share_stats(refresh=True)

+ 2
- 0
manila/tests/share/drivers/huawei/test_huawei_nas.py View File

@@ -2431,6 +2431,8 @@ class HuaweiShareDriverTestCase(test.TestCase):
2431 2431
             "goodness_function": None,
2432 2432
             "pools": [],
2433 2433
             "share_group_stats": {"consistent_snapshot_support": None},
2434
+            "ipv4_support": True,
2435
+            "ipv6_support": False,
2434 2436
         }
2435 2437
 
2436 2438
         if replication_support:

+ 66
- 18
manila/tests/share/drivers/test_helpers.py View File

@@ -81,26 +81,55 @@ class NFSHelperTestCase(test.TestCase):
81 81
             self.server, ['sudo', 'exportfs'])
82 82
 
83 83
     @ddt.data(
84
-        {"public_address": "1.2.3.4"},
85
-        {"public_address": "1.2.3.4", "admin_ip": "5.6.7.8"},
86
-        {"public_address": "1.2.3.4", "ip": "9.10.11.12"},
84
+        {"server": {"public_address": "1.2.3.4"}, "version": 4},
85
+        {"server": {"public_address": "1001::1002"}, "version": 6},
86
+        {"server": {"public_address": "1.2.3.4", "admin_ip": "5.6.7.8"},
87
+         "version": 4},
88
+        {"server": {"public_address": "1.2.3.4", "ip": "9.10.11.12"},
89
+         "version": 4},
90
+        {"server": {"public_address": "1001::1001", "ip": "1001::1002"},
91
+         "version": 6},
92
+        {"server": {"public_address": "1001::1002", "admin_ip": "1001::1002"},
93
+         "version": 6},
94
+        {"server": {"public_addresses": ["1001::1002"]}, "version": 6},
95
+        {"server": {"public_addresses": ["1.2.3.4", "1001::1002"]},
96
+         "version": {"1.2.3.4": 4, "1001::1002": 6}},
87 97
     )
88
-    def test_create_exports(self, server):
98
+    @ddt.unpack
99
+    def test_create_exports(self, server, version):
89 100
         result = self._helper.create_exports(server, self.share_name)
90 101
 
91 102
         expected_export_locations = []
92 103
         path = os.path.join(CONF.share_mount_path, self.share_name)
93 104
         service_address = server.get("admin_ip", server.get("ip"))
94
-        for ip, is_admin in ((server['public_address'], False),
95
-                             (service_address, True)):
96
-            if ip:
97
-                expected_export_locations.append({
98
-                    "path": "%s:%s" % (ip, path),
99
-                    "is_admin_only": is_admin,
100
-                    "metadata": {
101
-                        "export_location_metadata_example": "example",
102
-                    },
103
-                })
105
+        version_copy = version
106
+
107
+        def convert_address(address, version):
108
+            if version == 4:
109
+                return address
110
+            return "[%s]" % address
111
+
112
+        if 'public_addresses' in server:
113
+            pairs = list(map(lambda addr: (addr, False),
114
+                             server['public_addresses']))
115
+        else:
116
+            pairs = [(server['public_address'], False)]
117
+
118
+        service_address = server.get("admin_ip", server.get("ip"))
119
+        if service_address:
120
+            pairs.append((service_address, True))
121
+
122
+        for ip, is_admin in pairs:
123
+            if isinstance(version_copy, dict):
124
+                version = version_copy.get(ip)
125
+
126
+            expected_export_locations.append({
127
+                "path": "%s:%s" % (convert_address(ip, version), path),
128
+                "is_admin_only": is_admin,
129
+                "metadata": {
130
+                    "export_location_metadata_example": "example",
131
+                },
132
+            })
104 133
         self.assertEqual(expected_export_locations, result)
105 134
 
106 135
     @ddt.data(const.ACCESS_LEVEL_RW, const.ACCESS_LEVEL_RO)
@@ -135,19 +164,36 @@ class NFSHelperTestCase(test.TestCase):
135 164
             mock.call(self.server, ['sudo', 'exportfs', '-u',
136 165
                                     ':'.join(['3.3.3.3', local_path])]),
137 166
             mock.call(self.server, ['sudo', 'exportfs', '-u',
138
-                                    ':'.join(['6.6.6.6/0.0.0.0',
167
+                                    ':'.join(['6.6.6.6/0',
139 168
                                               local_path])]),
140 169
             mock.call(self.server, ['sudo', 'exportfs', '-o',
141 170
                                     expected_mount_options % access_level,
142 171
                                     ':'.join(['2.2.2.2', local_path])]),
143 172
             mock.call(self.server, ['sudo', 'exportfs', '-o',
144 173
                                     expected_mount_options % access_level,
145
-                                    ':'.join(['5.5.5.5/255.255.255.0',
174
+                                    ':'.join(['5.5.5.5/24',
146 175
                                               local_path])]),
147 176
         ])
148 177
         self._helper._sync_nfs_temp_and_perm_files.assert_has_calls([
149 178
             mock.call(self.server), mock.call(self.server)])
150 179
 
180
+    @ddt.data({'access': '10.0.0.1', 'result': '10.0.0.1'},
181
+              {'access': '10.0.0.1/32', 'result': '10.0.0.1'},
182
+              {'access': '10.0.0.0/24', 'result': '10.0.0.0/24'},
183
+              {'access': '1001::1001', 'result': '[1001::1001]'},
184
+              {'access': '1001::1000/128', 'result': '[1001::1000]'},
185
+              {'access': '1001::1000/124', 'result': '[1001::1000]/124'})
186
+    @ddt.unpack
187
+    def test__get_parsed_address_or_cidr(self, access, result):
188
+        self.assertEqual(result,
189
+                         self._helper._get_parsed_address_or_cidr(access))
190
+
191
+    @ddt.data('10.0.0.265', '10.0.0.1/33', '1001::10069', '1001::1000/129')
192
+    def test__get_parsed_address_or_cidr_with_invalid_access(self, access):
193
+        self.assertRaises(exception.InvalidInput,
194
+                          self._helper._get_parsed_address_or_cidr,
195
+                          access)
196
+
151 197
     def test_update_access_invalid_type(self):
152 198
         access_rules = [test_generic.get_fake_access_rule(
153 199
             '2.2.2.2', const.ACCESS_LEVEL_RW, access_type='fake'), ]
@@ -215,7 +261,8 @@ class NFSHelperTestCase(test.TestCase):
215 261
         self._helper._ssh_exec.assert_has_calls(
216 262
             [mock.call(self.server, mock.ANY) for i in range(1)])
217 263
 
218
-    @ddt.data('/foo/bar', '5.6.7.8:/bar/quuz', '5.6.7.9:/foo/quuz')
264
+    @ddt.data('/foo/bar', '5.6.7.8:/bar/quuz', '5.6.7.9:/foo/quuz',
265
+              '[1001::1001]:/foo/bar', '[1001::1000]/:124:/foo/bar')
219 266
     def test_get_exports_for_share_single_ip(self, export_location):
220 267
         server = dict(public_address='1.2.3.4')
221 268
 
@@ -257,7 +304,8 @@ class NFSHelperTestCase(test.TestCase):
257 304
             exception.ManilaException,
258 305
             self._helper.get_exports_for_share, server, export_location)
259 306
 
260
-    @ddt.data('/foo/bar', '5.6.7.8:/foo/bar', '5.6.7.88:fake:/foo/bar')
307
+    @ddt.data('/foo/bar', '5.6.7.8:/foo/bar', '5.6.7.88:fake:/foo/bar',
308
+              '[1001::1002]:/foo/bar', '[1001::1000]/124:/foo/bar')
261 309
     def test_get_share_path_by_export_location(self, export_location):
262 310
         result = self._helper.get_share_path_by_export_location(
263 311
             dict(), export_location)

+ 23
- 1
manila/tests/share/drivers/test_lvm.py View File

@@ -416,6 +416,23 @@ class LVMShareDriverTestCase(test.TestCase):
416 416
                 self.server, self.share['name'],
417 417
                 access_rules, add_rules=add_rules, delete_rules=delete_rules))
418 418
 
419
+    @ddt.data(('1001::1001/129', None, False), ('1.1.1.256', None, False),
420
+              ('1001::1001', None, [6]), ('1.1.1.0', None, [4]),
421
+              (None, ['1001::1001', '1.1.1.0'], [6, 4]),
422
+              (None, ['1001::1001'], [6]), (None, ['1.1.1.0'], [4]),
423
+              (None, ['1001::1001/129', '1.1.1.0'], False))
424
+    @ddt.unpack
425
+    def test_get_configured_ip_version(
426
+            self, configured_ip, configured_ips, configured_ip_version):
427
+        CONF.set_default('lvm_share_export_ip', configured_ip)
428
+        CONF.set_default('lvm_share_export_ips', configured_ips)
429
+        if configured_ip_version:
430
+            self.assertEqual(configured_ip_version,
431
+                             self._driver.get_configured_ip_version())
432
+        else:
433
+            self.assertRaises(exception.InvalidInput,
434
+                              self._driver.get_configured_ip_version)
435
+
419 436
     def test_mount_device(self):
420 437
         mount_path = self._get_mount_path(self.share)
421 438
         ret = self._driver._mount_device(self.share, 'fakedevice')
@@ -541,7 +558,10 @@ class LVMShareDriverTestCase(test.TestCase):
541 558
                                               'count=1024', 'bs=1M',
542 559
                                               run_as_root=True)
543 560
 
544
-    def test_update_share_stats(self):
561
+    @ddt.data(('1.1.1.1', 4), ('1001::1001', 6))
562
+    @ddt.unpack
563
+    def test_update_share_stats(self, configured_ip, version):
564
+        CONF.set_default('lvm_share_export_ip', configured_ip)
545 565
         self.mock_object(self._driver, 'get_share_server_pools',
546 566
                          mock.Mock(return_value='test-pool'))
547 567
 
@@ -552,6 +572,8 @@ class LVMShareDriverTestCase(test.TestCase):
552 572
         self.assertTrue(self._driver._stats['snapshot_support'])
553 573
         self.assertEqual('LVMShareDriver', self._driver._stats['driver_name'])
554 574
         self.assertEqual('test-pool', self._driver._stats['pools'])
575
+        self.assertEqual(version == 4, self._driver._stats['ipv4_support'])
576
+        self.assertEqual(version == 6, self._driver._stats['ipv6_support'])
555 577
 
556 578
     def test_revert_to_snapshot(self):
557 579
         mock_update_access = self.mock_object(self._helper_nfs,

+ 48
- 16
manila/tests/share/drivers/test_service_instance.py View File

@@ -779,8 +779,10 @@ class ServiceInstanceManagerTestCase(test.TestCase):
779 779
             fake_server_details)
780 780
 
781 781
     @ddt.data(
782
-        *[{'s': s, 't': t, 'server': server}
783
-            for s, t in (
782
+        *[{'service_config': service_config,
783
+           'tenant_config': tenant_config,
784
+           'server': server}
785
+            for service_config, tenant_config in (
784 786
                 ('fake_net_s', 'fake_net_t'),
785 787
                 ('fake_net_s', '12.34.56.78'),
786 788
                 ('98.76.54.123', 'fake_net_t'),
@@ -800,18 +802,25 @@ class ServiceInstanceManagerTestCase(test.TestCase):
800 802
                         {'addr': 'fake4'}],
801 803
                 }})])
802 804
     @ddt.unpack
803
-    def test_get_common_server_valid_cases(self, s, t, server):
804
-        self._get_common_server(s, t, server, True)
805
+    def test_get_common_server_valid_cases(self, service_config,
806
+                                           tenant_config, server):
807
+        self._get_common_server(service_config, tenant_config, server,
808
+                                '98.76.54.123', '12.34.56.78', True)
805 809
 
806 810
     @ddt.data(
807
-        *[{'s': s, 't': t, 'server': server}
808
-            for s, t in (
811
+        *[{'service_config': service_config,
812
+           'tenant_config': tenant_config,
813
+           'server': server}
814
+            for service_config, tenant_config in (
809 815
                 ('fake_net_s', 'fake'),
810 816
                 ('fake', 'fake_net_t'),
811 817
                 ('fake', 'fake'),
812 818
                 ('98.76.54.123', '12.12.12.1212'),
813 819
                 ('12.12.12.1212', '12.34.56.78'),
814
-                ('12.12.12.1212', '12.12.12.1212'))
820
+                ('12.12.12.1212', '12.12.12.1212'),
821
+                ('1001::1001', '1001::100G'),
822
+                ('1001::10G1', '1001::1001'),
823
+                )
815 824
             for server in (
816 825
                 {'networks': {
817 826
                     'fake_net_s': ['foo', '98.76.54.123', 'bar'],
@@ -827,15 +836,38 @@ class ServiceInstanceManagerTestCase(test.TestCase):
827 836
                         {'addr': 'fake4'}],
828 837
                 }})])
829 838
     @ddt.unpack
830
-    def test_get_common_server_invalid_cases(self, s, t, server):
831
-        self._get_common_server(s, t, server, False)
839
+    def test_get_common_server_invalid_cases(self, service_config,
840
+                                             tenant_config, server):
841
+        self._get_common_server(service_config, tenant_config, server,
842
+                                '98.76.54.123', '12.34.56.78', False)
832 843
 
833
-    def _get_common_server(self, s, t, server, is_valid=True):
844
+    @ddt.data(
845
+        *[{'service_config': service_config,
846
+            'tenant_config': tenant_config,
847
+            'server': server}
848
+            for service_config, tenant_config in (
849
+            ('fake_net_s', '1001::1002'),
850
+            ('1001::1001', 'fake_net_t'),
851
+            ('1001::1001', '1001::1002'))
852
+            for server in (
853
+                {'networks': {
854
+                 'fake_net_s': ['foo', '1001::1001'],
855
+                 'fake_net_t': ['bar', '1001::1002']}},
856
+                {'addresses': {
857
+                 'fake_net_s': [{'addr': 'foo'}, {'addr': '1001::1001'}],
858
+                 'fake_net_t': [{'addr': 'bar'}, {'addr': '1001::1002'}]}})])
859
+    @ddt.unpack
860
+    def test_get_common_server_valid_ipv6_address(self, service_config,
861
+                                                  tenant_config, server):
862
+        self._get_common_server(service_config, tenant_config, server,
863
+                                '1001::1001', '1001::1002', True)
864
+
865
+    def _get_common_server(self, service_config, tenant_config,
866
+                           server, service_address, network_address,
867
+                           is_valid=True):
834 868
         fake_instance_id = 'fake_instance_id'
835 869
         fake_user = 'fake_user'
836 870
         fake_pass = 'fake_pass'
837
-        fake_addr_s = '98.76.54.123'
838
-        fake_addr_t = '12.34.56.78'
839 871
         fake_server = {'id': fake_instance_id}
840 872
         fake_server.update(server)
841 873
         expected = {
@@ -843,17 +875,17 @@ class ServiceInstanceManagerTestCase(test.TestCase):
843 875
                 'username': fake_user,
844 876
                 'password': fake_pass,
845 877
                 'pk_path': self._manager.path_to_private_key,
846
-                'ip': fake_addr_s,
847
-                'public_address': fake_addr_t,
878
+                'ip': service_address,
879
+                'public_address': network_address,
848 880
                 'instance_id': fake_instance_id,
849 881
             }
850 882
         }
851 883
 
852 884
         def fake_get_config_option(attr):
853 885
             if attr == 'service_net_name_or_ip':
854
-                return s
886
+                return service_config
855 887
             elif attr == 'tenant_net_name_or_ip':
856
-                return t
888
+                return tenant_config
857 889
             elif attr == 'service_instance_name_or_id':
858 890
                 return fake_instance_id
859 891
             elif attr == 'service_instance_user':

+ 2
- 0
manila/tests/share/drivers/zfsonlinux/test_driver.py View File

@@ -362,6 +362,8 @@ class ZFSonLinuxShareDriverTestCase(test.TestCase):
362 362
             'vendor_name': 'Open Source',
363 363
             'filter_function': None,
364 364
             'goodness_function': None,
365
+            'ipv4_support': True,
366
+            'ipv6_support': False,
365 367
         }
366 368
         if replication_domain:
367 369
             expected['replication_type'] = 'readable'

+ 53
- 0
manila/tests/share/test_access.py View File

@@ -720,3 +720,56 @@ class ShareInstanceAccessTestCase(test.TestCase):
720 720
         else:
721 721
             self.assertEqual(states[0], rule_1['state'])
722 722
             self.assertEqual(states[-1], rule_4['state'])
723
+
724
+    @ddt.data(('nfs', True), ('cifs', False), ('ceph', False))
725
+    @ddt.unpack
726
+    def test__filter_ipv6_rules(self, proto, filtered):
727
+        pass_rules = [
728
+            {
729
+                'access_type': 'ip',
730
+                'access_to': '1.1.1.1'
731
+            },
732
+            {
733
+                'access_type': 'ip',
734
+                'access_to': '1.1.1.0/24'
735
+            },
736
+            {
737
+                'access_type': 'user',
738
+                'access_to': 'fake_user'
739
+            },
740
+        ]
741
+        fail_rules = [
742
+            {
743
+                'access_type': 'ip',
744
+                'access_to': '1001::1001'
745
+            },
746
+            {
747
+                'access_type': 'ip',
748
+                'access_to': '1001::/64'
749
+            },
750
+        ]
751
+        test_rules = pass_rules + fail_rules
752
+        filtered_rules = self.access_helper._filter_ipv6_rules(
753
+            test_rules, proto)
754
+        if filtered:
755
+            self.assertEqual(pass_rules, filtered_rules)
756
+        else:
757
+            self.assertEqual(test_rules, filtered_rules)
758
+
759
+    def test__get_rules_to_send_to_driver(self):
760
+        self.driver.ipv6_implemented = False
761
+
762
+        share = db_utils.create_share(status=constants.STATUS_AVAILABLE)
763
+        share_instance = share['instance']
764
+        db_utils.create_access(share_id=share['id'], access_to='1001::/64',
765
+                               state=constants.ACCESS_STATE_ACTIVE)
766
+        self.mock_object(
767
+            self.access_helper, 'get_and_update_share_instance_access_rules',
768
+            mock.Mock(side_effect=self.access_helper.
769
+                      get_and_update_share_instance_access_rules))
770
+
771
+        access_rules_to_be_on_share, add_rules, delete_rules = (
772
+            self.access_helper._get_rules_to_send_to_driver(
773
+                self.context, share_instance))
774
+        self.assertEqual([], add_rules)
775
+        self.assertEqual([], delete_rules)

+ 69
- 0
manila/tests/share/test_driver.py View File

@@ -19,6 +19,7 @@ import time
19 19
 
20 20
 import ddt
21 21
 import mock
22
+from mock import PropertyMock
22 23
 
23 24
 from manila import exception
24 25
 from manila import network
@@ -1083,3 +1084,71 @@ class ShareDriverTestCase(test.TestCase):
1083 1084
                           share_driver.snapshot_update_access,
1084 1085
                           'fake_context', 'fake_snapshot', ['r1', 'r2'],
1085 1086
                           [], [])
1087
+
1088
+    @ddt.data({'capability': (True, True),
1089
+               'user_admin_networks': [[4], [4]],
1090
+               'expected': {'ipv4': True, 'ipv6': False}},
1091
+              {'capability': (True, True),
1092
+               'user_admin_networks': [[6], [6]],
1093
+               'expected': {'ipv4': False, 'ipv6': True}},
1094
+              {'capability': (False, False),
1095
+               'user_admin_networks': [[4], [4]],
1096
+               'expected': {'ipv4': False, 'ipv6': False}},
1097
+              {'capability': (True, True),
1098
+               'user_admin_networks': [[4], [6]],
1099
+               'expected': {'ipv4': False, 'ipv6': False}},
1100
+              {'capability': (False, False),
1101
+               'user_admin_networks': [[6], [4]],
1102
+               'expected': {'ipv4': False, 'ipv6': False}},)
1103
+    @ddt.unpack
1104
+    def test_add_ip_version_capability_if_dhss_true(self, capability,
1105
+                                                    user_admin_networks,
1106
+                                                    expected):
1107
+        share_driver = self._instantiate_share_driver(None, True)
1108
+        version = PropertyMock(side_effect=user_admin_networks)
1109
+        type(share_driver.network_api).enabled_ip_version = version
1110
+        data = {'share_backend_name': 'fake_backend',
1111
+                'ipv4_support': capability[0],
1112
+                'ipv6_support': capability[1]}
1113
+
1114
+        result = share_driver.add_ip_version_capability(data)
1115
+
1116
+        self.assertIsNotNone(result['ipv4_support'])
1117
+        self.assertEqual(expected['ipv4'], result['ipv4_support'])
1118
+        self.assertIsNotNone(result['ipv6_support'])
1119
+        self.assertEqual(expected['ipv6'], result['ipv6_support'])
1120
+
1121
+    @ddt.data({'capability': (True, False),
1122
+               'conf': [4],
1123
+               'expected': {'ipv4': True, 'ipv6': False}},
1124
+              {'capability': (True, True),
1125
+               'conf': [6],
1126
+               'expected': {'ipv4': False, 'ipv6': True}},
1127
+              {'capability': (False, False),
1128
+               'conf': [4],
1129
+               'expected': {'ipv4': False, 'ipv6': False}},
1130
+              {'capability': (False, True),
1131
+               'conf': [4],
1132
+               'expected': {'ipv4': False, 'ipv6': False}},
1133
+              {'capability': (False, True),
1134
+               'conf': [6],
1135
+               'expected': {'ipv4': False, 'ipv6': True}},
1136
+              {'capability': (True, True),
1137
+               'conf': [4, 6],
1138
+               'expected': {'ipv4': True, 'ipv6': True}},
1139
+              )
1140
+    @ddt.unpack
1141
+    def test_add_ip_version_capability_if_dhss_false(self, capability,
1142
+                                                     conf, expected):
1143
+        share_driver = self._instantiate_share_driver(None, False)
1144
+        self.mock_object(share_driver, 'get_configured_ip_version',
1145
+                         mock.Mock(return_value=conf))
1146
+        data = {'share_backend_name': 'fake_backend',
1147
+                'ipv4_support': capability[0],
1148
+                'ipv6_support': capability[1]}
1149
+        result = share_driver.add_ip_version_capability(data)
1150
+
1151
+        self.assertIsNotNone(result['ipv4_support'])
1152
+        self.assertEqual(expected['ipv4'], result['ipv4_support'])
1153
+        self.assertIsNotNone(result['ipv6_support'])
1154
+        self.assertEqual(expected['ipv6'], result['ipv6_support'])

+ 27
- 0
manila/tests/test_network.py View File

@@ -128,3 +128,30 @@ class NetworkBaseAPITestCase(test.TestCase):
128 128
         self.assertRaises(
129 129
             exception.NetworkBadConfigurationException,
130 130
             result._verify_share_network, 'foo_id', None)
131
+
132
+    @ddt.data((True, False, 6), (False, True, 4),
133
+              (True, True, 6), (None, None, False))
134
+    @ddt.unpack
135
+    def test_enabled_ip_version(self, network_plugin_ipv6_enabled,
136
+                                network_plugin_ipv4_enabled,
137
+                                enable_ip_version):
138
+        class FakeNetworkAPI(network.NetworkBaseAPI):
139
+            def allocate_network(self, *args, **kwargs):
140
+                pass
141
+
142
+            def deallocate_network(self, *args, **kwargs):
143
+                pass
144
+
145
+        network.CONF.set_default('network_plugin_ipv6_enabled',
146
+                                 network_plugin_ipv6_enabled)
147
+        network.CONF.set_default('network_plugin_ipv4_enabled',
148
+                                 network_plugin_ipv4_enabled)
149
+
150
+        result = FakeNetworkAPI()
151
+
152
+        if enable_ip_version:
153
+            self.assertTrue(hasattr(result, 'enabled_ip_version'))
154
+            self.assertEqual(enable_ip_version, result.enabled_ip_version)
155
+        else:
156
+            self.assertRaises(exception.NetworkBadConfigurationException,
157
+                              getattr, result, 'enabled_ip_version')

+ 13
- 5
manila/utils.py View File

@@ -386,14 +386,22 @@ def cidr_to_netmask(cidr):
386 386
 
387 387
 
388 388
 def is_valid_ip_address(ip_address, ip_version):
389
-    if int(ip_version) == 4:
390
-        return netutils.is_valid_ipv4(ip_address)
391
-    elif int(ip_version) == 6:
392
-        return netutils.is_valid_ipv6(ip_address)
393
-    else:
389
+    ip_version = ([int(ip_version)] if not isinstance(ip_version, list)
390
+                  else ip_version)
391
+
392
+    if not set(ip_version).issubset(set([4, 6])):
394 393
         raise exception.ManilaException(
395 394
             _("Provided improper IP version '%s'.") % ip_version)
396 395
 
396
+    if 4 in ip_version:
397
+        if netutils.is_valid_ipv4(ip_address):
398
+            return True
399
+    if 6 in ip_version:
400
+        if netutils.is_valid_ipv6(ip_address):
401
+            return True
402
+
403
+    return False
404
+
397 405
 
398 406
 class IsAMatcher(object):
399 407
     def __init__(self, expected_value=None):

+ 8
- 0
releasenotes/notes/support-ipv6-in-drivers-and-network-plugins-1833121513edb13d.yaml View File

@@ -0,0 +1,8 @@
1
+---
2
+features:
3
+  - Added optional extra spec 'ipv4_support' and 'ipv6_support' for share
4
+    type.
5
+  - Added new capabilities 'ipv4_support' and 'ipv6_support' for IP based
6
+    drivers.
7
+  - Added IPv6 support in network plugins. (support either IPv6 or IPv4)
8
+  - Added IPv6 support in the lvm driver. (support both IPv6 and IPv4)

Loading…
Cancel
Save