cloud-init-dev team mailing list archive
-
cloud-init-dev team
-
Mailing list archive
-
Message #05026
[Merge] ~smoser/cloud-init:fix/lxd-only-create-network-if-noexist into cloud-init:master
Scott Moser has proposed merging ~smoser/cloud-init:fix/lxd-only-create-network-if-noexist into cloud-init:master.
Commit message:
lxd: Delete lxdbr0 network if it exists and is to be created.
Newer versions (3.0.1+) of lxd create the 'lxdbr0' network when
'lxd init --auto' is invoked.
When cloud-init is given a network configuration to pass on to
lxc and that config had no name specified or 'lxdbr0', then cloud-init
would fail to create the network as it already exists.
The change here is to check if it exists and delete it if it does.
LP: #1776958
Requested reviews:
cloud-init commiters (cloud-init-dev)
Related bugs:
Bug #1776958 in cloud-init: "error creating lxdbr0."
https://bugs.launchpad.net/cloud-init/+bug/1776958
For more details, see:
https://code.launchpad.net/~smoser/cloud-init/+git/cloud-init/+merge/348005
see commit message
--
Your team cloud-init commiters is requested to review the proposed merge of ~smoser/cloud-init:fix/lxd-only-create-network-if-noexist into cloud-init:master.
diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py
index 09374d2..b6ee756 100644
--- a/cloudinit/config/cc_lxd.py
+++ b/cloudinit/config/cc_lxd.py
@@ -47,11 +47,16 @@ lxd-bridge will be configured accordingly.
domain: <domain>
"""
+from cloudinit import log as logging
from cloudinit import util
import os
distros = ['ubuntu']
+LOG = logging.getLogger(__name__)
+
+_DEFAULT_NETWORK_NAME = "lxdbr0"
+
def handle(name, cfg, cloud, log, args):
# Get config
@@ -134,6 +139,7 @@ def handle(name, cfg, cloud, log, args):
'--frontend=noninteractive'])
else:
# Built-in LXD bridge support
+ maybe_delete_network(bridge_cfg)
cmd_create, cmd_attach = bridge_to_cmd(bridge_cfg)
if cmd_create:
log.debug("Creating lxd bridge: %s" %
@@ -204,7 +210,7 @@ def bridge_to_cmd(bridge_cfg):
if bridge_cfg.get("mode") == "none":
return None, None
- bridge_name = bridge_cfg.get("name", "lxdbr0")
+ bridge_name = bridge_cfg.get("name", _DEFAULT_NETWORK_NAME)
cmd_create = []
cmd_attach = ["lxc", "network", "attach-profile", bridge_name,
"default", "eth0", "--force-local"]
@@ -251,4 +257,47 @@ def bridge_to_cmd(bridge_cfg):
return cmd_create, cmd_attach
+
+def _network_exists(name):
+ """Return boolean indicating if network exists."""
+ try:
+ _out, _err = util.subp(["lxc", "network", "show", name],
+ update_env={'LC_ALL': 'C'})
+ return True
+ except util.ProcessExecutionError as e:
+ if e.exit_code != 1 or "not found" not in e.stderr.lower():
+ raise e
+ return False
+
+
+def _network_delete(name):
+ util.subp(["lxc", "network", "delete", name])
+
+
+def maybe_delete_network(cfg, default=None):
+ """Some versions of lxd init automatically create a lxdbr0.
+ If cfg specified to create that name, then we need to delete it
+ so that the creation will work. If cfg specified a name other
+ than default, or mode == existing, then do not do anything.
+
+ https://github.com/lxc/lxd/issues/4649
+ """
+
+ if default is None:
+ default = _DEFAULT_NETWORK_NAME
+
+ if cfg.get("mode", "new") == "existing":
+ return
+
+ name = cfg.get("name", default)
+ if name != default:
+ return
+
+ if not _network_exists(name):
+ return
+
+ LOG.debug("Removing lxd network '%s'", name)
+ _network_delete(name)
+
+
# vi: ts=4 expandtab
diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py
index a205498..02b0c8d 100644
--- a/tests/unittests/test_handler/test_handler_lxd.py
+++ b/tests/unittests/test_handler/test_handler_lxd.py
@@ -4,6 +4,7 @@ from cloudinit.config import cc_lxd
from cloudinit.sources import DataSourceNoCloud
from cloudinit import (distros, helpers, cloud)
from cloudinit.tests import helpers as t_help
+from cloudinit.util import ProcessExecutionError
try:
from unittest import mock
@@ -33,12 +34,16 @@ class TestLxd(t_help.CiTestCase):
cc = cloud.Cloud(ds, paths, {}, d, None)
return cc
+ @mock.patch("cloudinit.config.cc_lxd.maybe_delete_network")
@mock.patch("cloudinit.config.cc_lxd.util")
- def test_lxd_init(self, mock_util):
+ def test_lxd_init(self, mock_util, m_maybe_del):
cc = self._get_cloud('ubuntu')
mock_util.which.return_value = True
+ m_maybe_del.return_value = None
cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
self.assertTrue(mock_util.which.called)
+ # no bridge config, so maybe_delete should not be called.
+ self.assertFalse(m_maybe_del.called)
init_call = mock_util.subp.call_args_list[0][0][0]
self.assertEqual(init_call,
['lxd', 'init', '--auto',
@@ -46,32 +51,39 @@ class TestLxd(t_help.CiTestCase):
'--storage-backend=zfs',
'--storage-pool=poolname'])
+ @mock.patch("cloudinit.config.cc_lxd.maybe_delete_network")
@mock.patch("cloudinit.config.cc_lxd.util")
- def test_lxd_install(self, mock_util):
+ def test_lxd_install(self, mock_util, m_maybe_del):
cc = self._get_cloud('ubuntu')
cc.distro = mock.MagicMock()
mock_util.which.return_value = None
cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
self.assertNotIn('WARN', self.logs.getvalue())
self.assertTrue(cc.distro.install_packages.called)
+ cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
+ self.assertFalse(m_maybe_del.called)
install_pkg = cc.distro.install_packages.call_args_list[0][0][0]
self.assertEqual(sorted(install_pkg), ['lxd', 'zfs'])
+ @mock.patch("cloudinit.config.cc_lxd.maybe_delete_network")
@mock.patch("cloudinit.config.cc_lxd.util")
- def test_no_init_does_nothing(self, mock_util):
+ def test_no_init_does_nothing(self, mock_util, m_maybe_del):
cc = self._get_cloud('ubuntu')
cc.distro = mock.MagicMock()
cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, self.logger, [])
self.assertFalse(cc.distro.install_packages.called)
self.assertFalse(mock_util.subp.called)
+ self.assertFalse(m_maybe_del.called)
+ @mock.patch("cloudinit.config.cc_lxd.maybe_delete_network")
@mock.patch("cloudinit.config.cc_lxd.util")
- def test_no_lxd_does_nothing(self, mock_util):
+ def test_no_lxd_does_nothing(self, mock_util, m_maybe_del):
cc = self._get_cloud('ubuntu')
cc.distro = mock.MagicMock()
cc_lxd.handle('cc_lxd', {'package_update': True}, cc, self.logger, [])
self.assertFalse(cc.distro.install_packages.called)
self.assertFalse(mock_util.subp.called)
+ self.assertFalse(m_maybe_del.called)
def test_lxd_debconf_new_full(self):
data = {"mode": "new",
@@ -183,4 +195,81 @@ class TestLxd(t_help.CiTestCase):
cc_lxd.bridge_to_cmd(data),
(None, None))
+
+class TestLxdMaybeDeleteNetwork(t_help.CiTestCase):
+ """Test the implementation of maybe_delete_network."""
+
+ defnet = cc_lxd._DEFAULT_NETWORK_NAME
+
+ @mock.patch("cloudinit.config.cc_lxd._network_exists", return_value=True)
+ @mock.patch("cloudinit.config.cc_lxd._network_delete")
+ def test_network_other_than_default_not_deleted(self, m_delete, m_exists):
+ """deletion should only occur if bridge is default."""
+ cc_lxd.maybe_delete_network({"name": "lxdbr1"})
+ # do not make assertions on m_exists as that call is optimized out.
+ m_delete.assert_not_called()
+
+ @mock.patch("cloudinit.config.cc_lxd._network_exists", return_value=True)
+ @mock.patch("cloudinit.config.cc_lxd._network_delete")
+ def test_mode_existing_should_not_delete(self, m_delete, m_exists):
+ """deletion should not occur if mode == existing."""
+ cc_lxd.maybe_delete_network({"name": "lxdbr1", "mode": "existing"})
+ # do not make assertions on m_exists as that call is optimized out.
+ m_delete.assert_not_called()
+
+ @mock.patch("cloudinit.config.cc_lxd._network_exists", return_value=True)
+ @mock.patch("cloudinit.config.cc_lxd._network_delete")
+ def test_delete_if_default_exists(self, m_delete, m_exists):
+ """deletion should occur if network existed."""
+ cc_lxd.maybe_delete_network({"name": self.defnet})
+ m_exists.assert_called_once_with(self.defnet)
+ m_delete.assert_called_once_with(self.defnet)
+
+ @mock.patch("cloudinit.config.cc_lxd._network_exists", return_value=False)
+ @mock.patch("cloudinit.config.cc_lxd._network_delete")
+ def test_delete_only_if_default_exists(self, m_delete, m_exists):
+ """deletion should not occur if network did not exist."""
+ cc_lxd.maybe_delete_network({"name": self.defnet})
+ m_exists.assert_called_once_with(self.defnet)
+ m_delete.assert_not_called()
+
+ @mock.patch("cloudinit.config.cc_lxd.util.subp")
+ def test_network_delete_calls_lxc_correctly(self, name, m_subp):
+ """Test _network_delete function operates correctly."""
+ m_subp.return_value = (b'', b'')
+ netname = "foo"
+ cc_lxd._network_delete(netname)
+ m_subp.assert_called_once_with(["lxc", "network", "delete", netname])
+
+ @mock.patch("cloudinit.config.cc_lxd.util.subp")
+ def test_network_exists_when_network_exists(self, m_subp):
+ """Test _network_exists exists path."""
+ m_subp.return_value = ('not important\nwould be yaml\n', '')
+ ret = cc_lxd._network_exists("thisexists")
+ self.assertTrue(m_subp.called)
+ self.assertTrue(ret)
+
+ @mock.patch("cloudinit.config.cc_lxd.util.subp")
+ def test_network_exists_when_network_does_not_exist(self, m_subp):
+ """Test _network_exists when network does not exist."""
+ # lxc network info foo is expected to return 1 and
+ # write 'Error: not found' to stderr.
+ m_subp.side_effect = ProcessExecutionError(
+ stdout="", stderr="Error: not found", exit_code=1)
+ ret = cc_lxd._network_exists("foonet")
+ self.assertTrue(m_subp.called)
+ self.assertFalse(ret)
+
+ @mock.patch("cloudinit.config.cc_lxd.util.subp")
+ def test_network_exists_error_path(self, m_subp):
+ """Test _network_exists with unexpected subp error raises."""
+ myexc = ProcessExecutionError(
+ stdout="", stderr="Error: unexpected.", exit_code=2)
+ m_subp.side_effect = myexc
+ with self.assertRaises(ProcessExecutionError) as e:
+ cc_lxd._network_exists("foonet")
+ self.assertEqual(myexc, e.exception)
+ self.assertTrue(m_subp.called)
+
+
# vi: ts=4 expandtab
Follow ups