sailesh1993 / rpms / cloud-init

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