5bfa6b
From 7bd016008429f0a18393a070d88e669f3ed89caa Mon Sep 17 00:00:00 2001
5bfa6b
From: Emanuele Giuseppe Esposito <eesposit@redhat.com>
5bfa6b
Date: Fri, 11 Feb 2022 14:37:46 +0100
5bfa6b
Subject: [PATCH] Fix IPv6 netmask format for sysconfig (#1215)
5bfa6b
5bfa6b
RH-Author: Emanuele Giuseppe Esposito <eesposit@redhat.com>
5bfa6b
RH-MergeRequest: 48: Fix IPv6 netmask format for sysconfig (#1215)
5bfa6b
RH-Commit: [1/1] 4c940bbcf85dba1fce9f4acb9fc7820c0d7777f6
5bfa6b
RH-Bugzilla: 2046540
5bfa6b
RH-Acked-by: Eduardo Otubo <otubo@redhat.com>
5bfa6b
RH-Acked-by: Vitaly Kuznetsov <vkuznets@redhat.com>
5bfa6b
5bfa6b
commit b97a30f0a05c1dea918c46ca9c05c869d15fe2d5
5bfa6b
Author: Harald <hjensas@redhat.com>
5bfa6b
Date:   Tue Feb 8 15:49:00 2022 +0100
5bfa6b
5bfa6b
    Fix IPv6 netmask format for sysconfig (#1215)
5bfa6b
5bfa6b
    This change converts the IPv6 netmask from the network_data.json[1]
5bfa6b
    format to the CIDR style, <IPv6_addr>/<prefix>.
5bfa6b
5bfa6b
    Using an IPv6 address like ffff:ffff:ffff:ffff:: does not work with
5bfa6b
    NetworkManager, nor networkscripts.
5bfa6b
5bfa6b
    NetworkManager will ignore the route, logging:
5bfa6b
      ifcfg-rh: ignoring invalid route at \
5bfa6b
        "::/:: via fd00:fd00:fd00:2::fffe dev $DEV" \
5bfa6b
        (/etc/sysconfig/network-scripts/route6-$DEV:3): \
5bfa6b
        Argument for "::/::" is not ADDR/PREFIX format
5bfa6b
5bfa6b
    Similarly if using networkscripts, ip route fail with error:
5bfa6b
      Error: inet6 prefix is expected rather than \
5bfa6b
        "fd00:fd00:fd00::/ffff:ffff:ffff:ffff::".
5bfa6b
5bfa6b
    Also a bit of refactoring ...
5bfa6b
5bfa6b
    cloudinit.net.sysconfig.Route.to_string:
5bfa6b
    * Move a couple of lines around to reduce repeated code.
5bfa6b
    * if "ADDRESS" not in key -> continute, so that the
5bfa6b
      code block following it can be de-indented.
5bfa6b
    cloudinit.net.network_state:
5bfa6b
    * Refactors the ipv4_mask_to_net_prefix, ipv6_mask_to_net_prefix
5bfa6b
      removes mask_to_net_prefix methods. Utilize ipaddress library to
5bfa6b
      do some of the heavy lifting.
5bfa6b
5bfa6b
    LP: #1959148
5bfa6b
5bfa6b
Signed-off-by: Emanuele Giuseppe Esposito <eesposit@redhat.com>
5bfa6b
---
5bfa6b
 cloudinit/net/__init__.py                     |   7 +-
5bfa6b
 cloudinit/net/network_state.py                | 103 +++++++-----------
5bfa6b
 cloudinit/net/sysconfig.py                    |  91 ++++++++++------
5bfa6b
 cloudinit/sources/DataSourceOpenNebula.py     |   2 +-
5bfa6b
 .../sources/helpers/vmware/imc/config_nic.py  |   4 +-
5bfa6b
 tests/unittests/test_net.py                   |  78 ++++++++++++-
5bfa6b
 6 files changed, 176 insertions(+), 109 deletions(-)
5bfa6b
5bfa6b
diff --git a/cloudinit/net/__init__.py b/cloudinit/net/__init__.py
5bfa6b
index 003efa2a..12bf64de 100644
5bfa6b
--- a/cloudinit/net/__init__.py
5bfa6b
+++ b/cloudinit/net/__init__.py
5bfa6b
@@ -14,7 +14,7 @@ import re
5bfa6b
 
5bfa6b
 from cloudinit import subp
5bfa6b
 from cloudinit import util
5bfa6b
-from cloudinit.net.network_state import mask_to_net_prefix
5bfa6b
+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
5bfa6b
 from cloudinit.url_helper import UrlError, readurl
5bfa6b
 
5bfa6b
 LOG = logging.getLogger(__name__)
5bfa6b
@@ -1048,10 +1048,11 @@ class EphemeralIPv4Network(object):
5bfa6b
                 'Cannot init network on {0} with {1}/{2} and bcast {3}'.format(
5bfa6b
                     interface, ip, prefix_or_mask, broadcast))
5bfa6b
         try:
5bfa6b
-            self.prefix = mask_to_net_prefix(prefix_or_mask)
5bfa6b
+            self.prefix = ipv4_mask_to_net_prefix(prefix_or_mask)
5bfa6b
         except ValueError as e:
5bfa6b
             raise ValueError(
5bfa6b
-                'Cannot setup network: {0}'.format(e)
5bfa6b
+                "Cannot setup network, invalid prefix or "
5bfa6b
+                "netmask: {0}".format(e)
5bfa6b
             ) from e
5bfa6b
 
5bfa6b
         self.connectivity_url = connectivity_url
5bfa6b
diff --git a/cloudinit/net/network_state.py b/cloudinit/net/network_state.py
5bfa6b
index e8bf9e39..2768ef94 100644
5bfa6b
--- a/cloudinit/net/network_state.py
5bfa6b
+++ b/cloudinit/net/network_state.py
5bfa6b
@@ -6,6 +6,7 @@
5bfa6b
 
5bfa6b
 import copy
5bfa6b
 import functools
5bfa6b
+import ipaddress
5bfa6b
 import logging
5bfa6b
 import socket
5bfa6b
 import struct
5bfa6b
@@ -872,12 +873,18 @@ def _normalize_net_keys(network, address_keys=()):
5bfa6b
         try:
5bfa6b
             prefix = int(maybe_prefix)
5bfa6b
         except ValueError:
5bfa6b
-            # this supports input of <address>/255.255.255.0
5bfa6b
-            prefix = mask_to_net_prefix(maybe_prefix)
5bfa6b
-    elif netmask:
5bfa6b
-        prefix = mask_to_net_prefix(netmask)
5bfa6b
-    elif 'prefix' in net:
5bfa6b
-        prefix = int(net['prefix'])
5bfa6b
+            if ipv6:
5bfa6b
+                # this supports input of ffff:ffff:ffff::
5bfa6b
+                prefix = ipv6_mask_to_net_prefix(maybe_prefix)
5bfa6b
+            else:
5bfa6b
+                # this supports input of 255.255.255.0
5bfa6b
+                prefix = ipv4_mask_to_net_prefix(maybe_prefix)
5bfa6b
+    elif netmask and not ipv6:
5bfa6b
+        prefix = ipv4_mask_to_net_prefix(netmask)
5bfa6b
+    elif netmask and ipv6:
5bfa6b
+        prefix = ipv6_mask_to_net_prefix(netmask)
5bfa6b
+    elif "prefix" in net:
5bfa6b
+        prefix = int(net["prefix"])
5bfa6b
     else:
5bfa6b
         prefix = 64 if ipv6 else 24
5bfa6b
 
5bfa6b
@@ -972,72 +979,42 @@ def ipv4_mask_to_net_prefix(mask):
5bfa6b
        str(24)         => 24
5bfa6b
        "24"            => 24
5bfa6b
     """
5bfa6b
-    if isinstance(mask, int):
5bfa6b
-        return mask
5bfa6b
-    if isinstance(mask, str):
5bfa6b
-        try:
5bfa6b
-            return int(mask)
5bfa6b
-        except ValueError:
5bfa6b
-            pass
5bfa6b
-    else:
5bfa6b
-        raise TypeError("mask '%s' is not a string or int")
5bfa6b
-
5bfa6b
-    if '.' not in mask:
5bfa6b
-        raise ValueError("netmask '%s' does not contain a '.'" % mask)
5bfa6b
-
5bfa6b
-    toks = mask.split(".")
5bfa6b
-    if len(toks) != 4:
5bfa6b
-        raise ValueError("netmask '%s' had only %d parts" % (mask, len(toks)))
5bfa6b
-
5bfa6b
-    return sum([bin(int(x)).count('1') for x in toks])
5bfa6b
+    return ipaddress.ip_network(f"0.0.0.0/{mask}").prefixlen
5bfa6b
 
5bfa6b
 
5bfa6b
 def ipv6_mask_to_net_prefix(mask):
5bfa6b
     """Convert an ipv6 netmask (very uncommon) or prefix (64) to prefix.
5bfa6b
 
5bfa6b
-    If 'mask' is an integer or string representation of one then
5bfa6b
-    int(mask) will be returned.
5bfa6b
+    If the input is already an integer or a string representation of
5bfa6b
+    an integer, then int(mask) will be returned.
5bfa6b
+       "ffff:ffff:ffff::"  => 48
5bfa6b
+       "48"                => 48
5bfa6b
     """
5bfa6b
-
5bfa6b
-    if isinstance(mask, int):
5bfa6b
-        return mask
5bfa6b
-    if isinstance(mask, str):
5bfa6b
-        try:
5bfa6b
-            return int(mask)
5bfa6b
-        except ValueError:
5bfa6b
-            pass
5bfa6b
-    else:
5bfa6b
-        raise TypeError("mask '%s' is not a string or int")
5bfa6b
-
5bfa6b
-    if ':' not in mask:
5bfa6b
-        raise ValueError("mask '%s' does not have a ':'")
5bfa6b
-
5bfa6b
-    bitCount = [0, 0x8000, 0xc000, 0xe000, 0xf000, 0xf800, 0xfc00, 0xfe00,
5bfa6b
-                0xff00, 0xff80, 0xffc0, 0xffe0, 0xfff0, 0xfff8, 0xfffc,
5bfa6b
-                0xfffe, 0xffff]
5bfa6b
-    prefix = 0
5bfa6b
-    for word in mask.split(':'):
5bfa6b
-        if not word or int(word, 16) == 0:
5bfa6b
-            break
5bfa6b
-        prefix += bitCount.index(int(word, 16))
5bfa6b
-
5bfa6b
-    return prefix
5bfa6b
-
5bfa6b
-
5bfa6b
-def mask_to_net_prefix(mask):
5bfa6b
-    """Return the network prefix for the netmask provided.
5bfa6b
-
5bfa6b
-    Supports ipv4 or ipv6 netmasks."""
5bfa6b
     try:
5bfa6b
-        # if 'mask' is a prefix that is an integer.
5bfa6b
-        # then just return it.
5bfa6b
-        return int(mask)
5bfa6b
+        # In the case the mask is already a prefix
5bfa6b
+        prefixlen = ipaddress.ip_network(f"::/{mask}").prefixlen
5bfa6b
+        return prefixlen
5bfa6b
     except ValueError:
5bfa6b
+        # ValueError means mask is an IPv6 address representation and need
5bfa6b
+        # conversion.
5bfa6b
         pass
5bfa6b
-    if is_ipv6_addr(mask):
5bfa6b
-        return ipv6_mask_to_net_prefix(mask)
5bfa6b
-    else:
5bfa6b
-        return ipv4_mask_to_net_prefix(mask)
5bfa6b
+
5bfa6b
+    netmask = ipaddress.ip_address(mask)
5bfa6b
+    mask_int = int(netmask)
5bfa6b
+    # If the mask is all zeroes, just return it
5bfa6b
+    if mask_int == 0:
5bfa6b
+        return mask_int
5bfa6b
+
5bfa6b
+    trailing_zeroes = min(
5bfa6b
+        ipaddress.IPV6LENGTH, (~mask_int & (mask_int - 1)).bit_length()
5bfa6b
+    )
5bfa6b
+    leading_ones = mask_int >> trailing_zeroes
5bfa6b
+    prefixlen = ipaddress.IPV6LENGTH - trailing_zeroes
5bfa6b
+    all_ones = (1 << prefixlen) - 1
5bfa6b
+    if leading_ones != all_ones:
5bfa6b
+        raise ValueError("Invalid network mask '%s'" % mask)
5bfa6b
+
5bfa6b
+    return prefixlen
5bfa6b
 
5bfa6b
 
5bfa6b
 def mask_and_ipv4_to_bcast_addr(mask, ip):
5bfa6b
diff --git a/cloudinit/net/sysconfig.py b/cloudinit/net/sysconfig.py
5bfa6b
index d5440998..7ecbe1c3 100644
5bfa6b
--- a/cloudinit/net/sysconfig.py
5bfa6b
+++ b/cloudinit/net/sysconfig.py
5bfa6b
@@ -12,6 +12,7 @@ from cloudinit import util
5bfa6b
 from cloudinit import subp
5bfa6b
 from cloudinit.distros.parsers import networkmanager_conf
5bfa6b
 from cloudinit.distros.parsers import resolv_conf
5bfa6b
+from cloudinit.net import network_state
5bfa6b
 
5bfa6b
 from . import renderer
5bfa6b
 from .network_state import (
5bfa6b
@@ -171,43 +172,61 @@ class Route(ConfigMap):
5bfa6b
         # (because Route can contain a mix of IPv4 and IPv6)
5bfa6b
         reindex = -1
5bfa6b
         for key in sorted(self._conf.keys()):
5bfa6b
-            if 'ADDRESS' in key:
5bfa6b
-                index = key.replace('ADDRESS', '')
5bfa6b
-                address_value = str(self._conf[key])
5bfa6b
-                # only accept combinations:
5bfa6b
-                # if proto ipv6 only display ipv6 routes
5bfa6b
-                # if proto ipv4 only display ipv4 routes
5bfa6b
-                # do not add ipv6 routes if proto is ipv4
5bfa6b
-                # do not add ipv4 routes if proto is ipv6
5bfa6b
-                # (this array will contain a mix of ipv4 and ipv6)
5bfa6b
-                if proto == "ipv4" and not self.is_ipv6_route(address_value):
5bfa6b
-                    netmask_value = str(self._conf['NETMASK' + index])
5bfa6b
-                    gateway_value = str(self._conf['GATEWAY' + index])
5bfa6b
-                    # increase IPv4 index
5bfa6b
-                    reindex = reindex + 1
5bfa6b
-                    buf.write("%s=%s\n" % ('ADDRESS' + str(reindex),
5bfa6b
-                                           _quote_value(address_value)))
5bfa6b
-                    buf.write("%s=%s\n" % ('GATEWAY' + str(reindex),
5bfa6b
-                                           _quote_value(gateway_value)))
5bfa6b
-                    buf.write("%s=%s\n" % ('NETMASK' + str(reindex),
5bfa6b
-                                           _quote_value(netmask_value)))
5bfa6b
-                    metric_key = 'METRIC' + index
5bfa6b
-                    if metric_key in self._conf:
5bfa6b
-                        metric_value = str(self._conf['METRIC' + index])
5bfa6b
-                        buf.write("%s=%s\n" % ('METRIC' + str(reindex),
5bfa6b
-                                               _quote_value(metric_value)))
5bfa6b
-                elif proto == "ipv6" and self.is_ipv6_route(address_value):
5bfa6b
-                    netmask_value = str(self._conf['NETMASK' + index])
5bfa6b
-                    gateway_value = str(self._conf['GATEWAY' + index])
5bfa6b
-                    metric_value = (
5bfa6b
-                        'metric ' + str(self._conf['METRIC' + index])
5bfa6b
-                        if 'METRIC' + index in self._conf else '')
5bfa6b
+            if "ADDRESS" not in key:
5bfa6b
+                continue
5bfa6b
+
5bfa6b
+            index = key.replace("ADDRESS", "")
5bfa6b
+            address_value = str(self._conf[key])
5bfa6b
+            netmask_value = str(self._conf["NETMASK" + index])
5bfa6b
+            gateway_value = str(self._conf["GATEWAY" + index])
5bfa6b
+
5bfa6b
+            # only accept combinations:
5bfa6b
+            # if proto ipv6 only display ipv6 routes
5bfa6b
+            # if proto ipv4 only display ipv4 routes
5bfa6b
+            # do not add ipv6 routes if proto is ipv4
5bfa6b
+            # do not add ipv4 routes if proto is ipv6
5bfa6b
+            # (this array will contain a mix of ipv4 and ipv6)
5bfa6b
+            if proto == "ipv4" and not self.is_ipv6_route(address_value):
5bfa6b
+                # increase IPv4 index
5bfa6b
+                reindex = reindex + 1
5bfa6b
+                buf.write(
5bfa6b
+                    "%s=%s\n"
5bfa6b
+                    % ("ADDRESS" + str(reindex), _quote_value(address_value))
5bfa6b
+                )
5bfa6b
+                buf.write(
5bfa6b
+                    "%s=%s\n"
5bfa6b
+                    % ("GATEWAY" + str(reindex), _quote_value(gateway_value))
5bfa6b
+                )
5bfa6b
+                buf.write(
5bfa6b
+                    "%s=%s\n"
5bfa6b
+                    % ("NETMASK" + str(reindex), _quote_value(netmask_value))
5bfa6b
+                )
5bfa6b
+                metric_key = "METRIC" + index
5bfa6b
+                if metric_key in self._conf:
5bfa6b
+                    metric_value = str(self._conf["METRIC" + index])
5bfa6b
                     buf.write(
5bfa6b
-                        "%s/%s via %s %s dev %s\n" % (address_value,
5bfa6b
-                                                      netmask_value,
5bfa6b
-                                                      gateway_value,
5bfa6b
-                                                      metric_value,
5bfa6b
-                                                      self._route_name))
5bfa6b
+                        "%s=%s\n"
5bfa6b
+                        % ("METRIC" + str(reindex), _quote_value(metric_value))
5bfa6b
+                    )
5bfa6b
+            elif proto == "ipv6" and self.is_ipv6_route(address_value):
5bfa6b
+                prefix_value = network_state.ipv6_mask_to_net_prefix(
5bfa6b
+                    netmask_value
5bfa6b
+                )
5bfa6b
+                metric_value = (
5bfa6b
+                    "metric " + str(self._conf["METRIC" + index])
5bfa6b
+                    if "METRIC" + index in self._conf
5bfa6b
+                    else ""
5bfa6b
+                )
5bfa6b
+                buf.write(
5bfa6b
+                    "%s/%s via %s %s dev %s\n"
5bfa6b
+                    % (
5bfa6b
+                        address_value,
5bfa6b
+                        prefix_value,
5bfa6b
+                        gateway_value,
5bfa6b
+                        metric_value,
5bfa6b
+                        self._route_name,
5bfa6b
+                    )
5bfa6b
+                )
5bfa6b
 
5bfa6b
         return buf.getvalue()
5bfa6b
 
5bfa6b
diff --git a/cloudinit/sources/DataSourceOpenNebula.py b/cloudinit/sources/DataSourceOpenNebula.py
5bfa6b
index 730ec586..e7980ab1 100644
5bfa6b
--- a/cloudinit/sources/DataSourceOpenNebula.py
5bfa6b
+++ b/cloudinit/sources/DataSourceOpenNebula.py
5bfa6b
@@ -233,7 +233,7 @@ class OpenNebulaNetwork(object):
5bfa6b
             # Set IPv4 address
5bfa6b
             devconf['addresses'] = []
5bfa6b
             mask = self.get_mask(c_dev)
5bfa6b
-            prefix = str(net.mask_to_net_prefix(mask))
5bfa6b
+            prefix = str(net.ipv4_mask_to_net_prefix(mask))
5bfa6b
             devconf['addresses'].append(
5bfa6b
                 self.get_ip(c_dev, mac) + '/' + prefix)
5bfa6b
 
5bfa6b
diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
5bfa6b
index 9cd2c0c0..3a45c67e 100644
5bfa6b
--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
5bfa6b
+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
5bfa6b
@@ -9,7 +9,7 @@ import logging
5bfa6b
 import os
5bfa6b
 import re
5bfa6b
 
5bfa6b
-from cloudinit.net.network_state import mask_to_net_prefix
5bfa6b
+from cloudinit.net.network_state import ipv4_mask_to_net_prefix
5bfa6b
 from cloudinit import subp
5bfa6b
 from cloudinit import util
5bfa6b
 
5bfa6b
@@ -180,7 +180,7 @@ class NicConfigurator(object):
5bfa6b
         """
5bfa6b
         route_list = []
5bfa6b
 
5bfa6b
-        cidr = mask_to_net_prefix(netmask)
5bfa6b
+        cidr = ipv4_mask_to_net_prefix(netmask)
5bfa6b
 
5bfa6b
         for gateway in gateways:
5bfa6b
             destination = "%s/%d" % (gen_subnet(gateway, netmask), cidr)
5bfa6b
diff --git a/tests/unittests/test_net.py b/tests/unittests/test_net.py
5bfa6b
index 14d3462f..a7f6a1f7 100644
5bfa6b
--- a/tests/unittests/test_net.py
5bfa6b
+++ b/tests/unittests/test_net.py
5bfa6b
@@ -2025,10 +2025,10 @@ pre-down route del -net 10.0.0.0/8 gw 11.0.0.1 metric 3 || true
5bfa6b
                     routes:
5bfa6b
                         - gateway: 2001:67c:1562:1
5bfa6b
                           network: 2001:67c:1
5bfa6b
-                          netmask: ffff:ffff:0
5bfa6b
+                          netmask: "ffff:ffff::"
5bfa6b
                         - gateway: 3001:67c:1562:1
5bfa6b
                           network: 3001:67c:1
5bfa6b
-                          netmask: ffff:ffff:0
5bfa6b
+                          netmask: "ffff:ffff::"
5bfa6b
                           metric: 10000
5bfa6b
             """),
5bfa6b
         'expected_netplan': textwrap.dedent("""
5bfa6b
@@ -2295,8 +2295,8 @@ iface bond0 inet6 static
5bfa6b
             'route6-bond0': textwrap.dedent("""\
5bfa6b
         # Created by cloud-init on instance boot automatically, do not edit.
5bfa6b
         #
5bfa6b
-        2001:67c:1/ffff:ffff:0 via 2001:67c:1562:1  dev bond0
5bfa6b
-        3001:67c:1/ffff:ffff:0 via 3001:67c:1562:1 metric 10000 dev bond0
5bfa6b
+        2001:67c:1/32 via 2001:67c:1562:1  dev bond0
5bfa6b
+        3001:67c:1/32 via 3001:67c:1562:1 metric 10000 dev bond0
5bfa6b
             """),
5bfa6b
             'route-bond0': textwrap.dedent("""\
5bfa6b
         ADDRESS0=10.1.3.0
5bfa6b
@@ -3088,6 +3088,76 @@ USERCTL=no
5bfa6b
             renderer.render_network_state(ns, target=render_dir)
5bfa6b
         self.assertEqual([], os.listdir(render_dir))
5bfa6b
 
5bfa6b
+    def test_invalid_network_mask_ipv6(self):
5bfa6b
+        net_json = {
5bfa6b
+            "services": [{"type": "dns", "address": "172.19.0.12"}],
5bfa6b
+            "networks": [
5bfa6b
+                {
5bfa6b
+                    "network_id": "public-ipv6",
5bfa6b
+                    "type": "ipv6",
5bfa6b
+                    "netmask": "",
5bfa6b
+                    "link": "tap1a81968a-79",
5bfa6b
+                    "routes": [
5bfa6b
+                        {
5bfa6b
+                            "gateway": "2001:DB8::1",
5bfa6b
+                            "netmask": "ff:ff:ff:ff::",
5bfa6b
+                            "network": "2001:DB8:1::1",
5bfa6b
+                        },
5bfa6b
+                    ],
5bfa6b
+                    "ip_address": "2001:DB8::10",
5bfa6b
+                    "id": "network1",
5bfa6b
+                }
5bfa6b
+            ],
5bfa6b
+            "links": [
5bfa6b
+                {
5bfa6b
+                    "ethernet_mac_address": "fa:16:3e:ed:9a:59",
5bfa6b
+                    "mtu": None,
5bfa6b
+                    "type": "bridge",
5bfa6b
+                    "id": "tap1a81968a-79",
5bfa6b
+                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
5bfa6b
+                },
5bfa6b
+            ],
5bfa6b
+        }
5bfa6b
+        macs = {"fa:16:3e:ed:9a:59": "eth0"}
5bfa6b
+        network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
5bfa6b
+        with self.assertRaises(ValueError):
5bfa6b
+            network_state.parse_net_config_data(network_cfg, skip_broken=False)
5bfa6b
+
5bfa6b
+    def test_invalid_network_mask_ipv4(self):
5bfa6b
+        net_json = {
5bfa6b
+            "services": [{"type": "dns", "address": "172.19.0.12"}],
5bfa6b
+            "networks": [
5bfa6b
+                {
5bfa6b
+                    "network_id": "public-ipv4",
5bfa6b
+                    "type": "ipv4",
5bfa6b
+                    "netmask": "",
5bfa6b
+                    "link": "tap1a81968a-79",
5bfa6b
+                    "routes": [
5bfa6b
+                        {
5bfa6b
+                            "gateway": "172.20.0.1",
5bfa6b
+                            "netmask": "255.234.255.0",
5bfa6b
+                            "network": "172.19.0.0",
5bfa6b
+                        },
5bfa6b
+                    ],
5bfa6b
+                    "ip_address": "172.20.0.10",
5bfa6b
+                    "id": "network1",
5bfa6b
+                }
5bfa6b
+            ],
5bfa6b
+            "links": [
5bfa6b
+                {
5bfa6b
+                    "ethernet_mac_address": "fa:16:3e:ed:9a:59",
5bfa6b
+                    "mtu": None,
5bfa6b
+                    "type": "bridge",
5bfa6b
+                    "id": "tap1a81968a-79",
5bfa6b
+                    "vif_id": "1a81968a-797a-400f-8a80-567f997eb93f",
5bfa6b
+                },
5bfa6b
+            ],
5bfa6b
+        }
5bfa6b
+        macs = {"fa:16:3e:ed:9a:59": "eth0"}
5bfa6b
+        network_cfg = openstack.convert_net_json(net_json, known_macs=macs)
5bfa6b
+        with self.assertRaises(ValueError):
5bfa6b
+            network_state.parse_net_config_data(network_cfg, skip_broken=False)
5bfa6b
+
5bfa6b
     def test_openstack_rendering_samples(self):
5bfa6b
         for os_sample in OS_SAMPLES:
5bfa6b
             render_dir = self.tmp_dir()
5bfa6b
-- 
5bfa6b
2.27.0
5bfa6b