diff --git a/doc/source/devref/api_converters.rst b/doc/source/devref/api_converters.rst index 8288c4257..3679b0b74 100644 --- a/doc/source/devref/api_converters.rst +++ b/doc/source/devref/api_converters.rst @@ -84,3 +84,14 @@ Test The Validator Do the right thing, and make sure you've created a unit test for any converter that you add to verify that it works as expected. +IPv6 canonical address formatter +-------------------------------- + +There are several ways to display an IPv6 address, which can lead to a lot +of confusion for users, engineers and operators alike. To reduce the impact +of the multifaceted style of writing an IPv6 address, it is proposed that +the IPv6 address in Neutron should be saved in the canonical format. + +If a user passes an IPv6 address, it will be saved in the canonical format. + +The full document is found at : http://tools.ietf.org/html/rfc5952 diff --git a/neutron_lib/api/converters.py b/neutron_lib/api/converters.py index 600e64a43..803160ba3 100644 --- a/neutron_lib/api/converters.py +++ b/neutron_lib/api/converters.py @@ -10,10 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import netaddr from oslo_utils import strutils import six from neutron_lib._i18n import _ +from neutron_lib import constants from neutron_lib import exceptions as n_exc @@ -162,3 +164,21 @@ def convert_to_list(data): return list(data) else: return [data] + + +def convert_ip_to_canonical_format(value): + """IP Address is validated and then converted to canonical format. + + :param value: The IP Address which needs to be checked. + :returns: - None if 'value' is None, + - 'value' if 'value' is IPv4 address, + - 'value' if 'value' is not an IP Address + - canonical IPv6 address if 'value' is IPv6 address. + """ + try: + ip = netaddr.IPAddress(value) + if ip.version == constants.IP_VERSION_6: + return six.text_type(ip.format(dialect=netaddr.ipv6_compact)) + except netaddr.core.AddrFormatError: + pass + return value diff --git a/neutron_lib/tests/unit/api/test_conversions.py b/neutron_lib/tests/unit/api/test_conversions.py index 6cfea0d61..4ad38dbf8 100644 --- a/neutron_lib/tests/unit/api/test_conversions.py +++ b/neutron_lib/tests/unit/api/test_conversions.py @@ -167,3 +167,34 @@ class TestConvertToList(base.BaseTestCase): def test_convert_to_list_non_iterable(self): for item in (True, False, 1, 1.2, object()): self.assertEqual([item], converters.convert_to_list(item)) + + +class TestConvertIPv6CanonicalFormat(base.BaseTestCase): + + def test_convert_ipv6_address_extended_add_with_zeroes(self): + result = converters.convert_ip_to_canonical_format( + u'2001:0db8:0:0:0:0:0:0001') + self.assertEqual(u'2001:db8::1', result) + + @testtools.skipIf(tools.is_bsd(), 'bug/1484837') + def test_convert_ipv6_compressed_address_OSX_skip(self): + result = converters.convert_ip_to_canonical_format( + u'2001:db8:0:1:1:1:1:1') + self.assertEqual(u'2001:db8:0:1:1:1:1:1', result) + + def test_convert_ipv6_extended_addr_to_compressed(self): + result = converters.convert_ip_to_canonical_format( + u"Fe80:0:0:0:0:0:0:1") + self.assertEqual(u'fe80::1', result) + + def test_convert_ipv4_address(self): + result = converters.convert_ip_to_canonical_format(u"192.168.1.1") + self.assertEqual(u'192.168.1.1', result) + + def test_convert_None_address(self): + result = converters.convert_ip_to_canonical_format(None) + self.assertIsNone(result) + + def test_convert_invalid_address(self): + result = converters.convert_ip_to_canonical_format("on") + self.assertEqual("on", result) diff --git a/releasenotes/notes/ipv6_address_usage-ef3d65ad5aa5798b.yaml b/releasenotes/notes/ipv6_address_usage-ef3d65ad5aa5798b.yaml new file mode 100644 index 000000000..a988ce0a3 --- /dev/null +++ b/releasenotes/notes/ipv6_address_usage-ef3d65ad5aa5798b.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A converter ``convert_ip_to_canonical_format`` has been added to + neutron-lib which allows IPv6 addresses to be stored and + displayed in canonical format. \ No newline at end of file