Merge ~smoser/cloud-init:fix/lxd-only-create-network-if-noexist into cloud-init:master

Proposed by Scott Moser
Status: Merged
Approved by: Chad Smith
Approved revision: 01ce34e069827aa4cd0bac732acab119df90a901
Merge reported by: Chad Smith
Merged at revision: 4ce6720104ec92d8d7c5aa993bf7ec405a2f53db
Proposed branch: ~smoser/cloud-init:fix/lxd-only-create-network-if-noexist
Merge into: cloud-init:master
Diff against target: 277 lines (+120/-24)
2 files modified
cloudinit/config/cc_lxd.py (+56/-8)
tests/unittests/test_handler/test_handler_lxd.py (+64/-16)
Reviewer Review Type Date Requested Status
Chad Smith Approve
Server Team CI bot continuous-integration Approve
Ryan Harper Approve
Review via email: mp+348005@code.launchpad.net

Commit message

lxd: Delete default network and detach device if lxd-init created them.

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.

Similarly, we need to remove the device from the default profile
so that the attach code can work.

Also, add a _lxc method and use it to make sure we're getting the
--force-local flag everywhere.

LP: #1776958

Description of the change

see commit message

To post a comment you must log in.
Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:c16f9104673e1e8a1fdaa024cc3511d5ad147c17
https://jenkins.ubuntu.com/server/job/cloud-init-ci/85/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/85/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:0cede0e8ee836f150b6382fddc551735fbae52c2
https://jenkins.ubuntu.com/server/job/cloud-init-ci/86/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/86/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Ryan Harper (raharper) :
review: Approve
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:3df8f0f56e8cabff262cb696ab20b7db79967bfa
https://jenkins.ubuntu.com/server/job/cloud-init-ci/94/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/94/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

just a nit

review: Approve
Revision history for this message
Chad Smith (chad.smith) :
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:5de7fc34c8e429941cac84267d6ef5a604cddcaf
https://jenkins.ubuntu.com/server/job/cloud-init-ci/96/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/96/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:5164e07ffad62915f7d6c54ea6e335bb4db6f90c
https://jenkins.ubuntu.com/server/job/cloud-init-ci/98/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/98/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Ryan Harper (raharper) :
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

FAILED: Continuous integration, rev:fe884295ba8ed9a85b16a94069cfdcaa5d461d1f
https://jenkins.ubuntu.com/server/job/cloud-init-ci/100/
Executed test runs:
    SUCCESS: Checkout
    FAILED: Unit & Style Tests

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/100/rebuild

review: Needs Fixing (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:63dc423b222fd53a14938597c91c8ba52a8bbecf
https://jenkins.ubuntu.com/server/job/cloud-init-ci/102/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/102/rebuild

review: Approve (continuous-integration)
Revision history for this message
Server Team CI bot (server-team-bot) wrote :

PASSED: Continuous integration, rev:01ce34e069827aa4cd0bac732acab119df90a901
https://jenkins.ubuntu.com/server/job/cloud-init-ci/103/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    SUCCESS: MAAS Compatability Testing
    IN_PROGRESS: Declarative: Post Actions

Click here to trigger a rebuild:
https://jenkins.ubuntu.com/server/job/cloud-init-ci/103/rebuild

review: Approve (continuous-integration)
Revision history for this message
Chad Smith (chad.smith) wrote :

Good rework, validated on xenial and cosmic on Ec2

tox -e citest -- run --os-name xenial --platform ec2 --preserve-data --data-dir results --verbose --deb cloud-init_18.2-83-g01ce34e0-1~bddeb_all.deb --test modules/lxd_bridge

2018-06-16 01:58:09,290 - tests.cloud_tests - DEBUG -
---- Verify summarized results:

Platform: ec2
  Distro: xenial
    test modules passed:1 tests failed:0
  Distro: cosmic
    test modules passed:1 tests failed:0

review: Approve
Revision history for this message
Chad Smith (chad.smith) wrote :

An upstream commit landed for this bug.

To view that commit see the following URL:
https://git.launchpad.net/cloud-init/commit/?id=4ce67201

There was an error fetching revisions from git servers. Please try again in a few minutes. If the problem persists, contact Launchpad support.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/config/cc_lxd.py b/cloudinit/config/cc_lxd.py
2index 09374d2..ac72ac4 100644
3--- a/cloudinit/config/cc_lxd.py
4+++ b/cloudinit/config/cc_lxd.py
5@@ -47,11 +47,16 @@ lxd-bridge will be configured accordingly.
6 domain: <domain>
7 """
8
9+from cloudinit import log as logging
10 from cloudinit import util
11 import os
12
13 distros = ['ubuntu']
14
15+LOG = logging.getLogger(__name__)
16+
17+_DEFAULT_NETWORK_NAME = "lxdbr0"
18+
19
20 def handle(name, cfg, cloud, log, args):
21 # Get config
22@@ -109,6 +114,7 @@ def handle(name, cfg, cloud, log, args):
23 # Set up lxd-bridge if bridge config is given
24 dconf_comm = "debconf-communicate"
25 if bridge_cfg:
26+ net_name = bridge_cfg.get("name", _DEFAULT_NETWORK_NAME)
27 if os.path.exists("/etc/default/lxd-bridge") \
28 and util.which(dconf_comm):
29 # Bridge configured through packaging
30@@ -135,15 +141,18 @@ def handle(name, cfg, cloud, log, args):
31 else:
32 # Built-in LXD bridge support
33 cmd_create, cmd_attach = bridge_to_cmd(bridge_cfg)
34+ maybe_cleanup_default(
35+ net_name=net_name, did_init=bool(init_cfg),
36+ create=bool(cmd_create), attach=bool(cmd_attach))
37 if cmd_create:
38 log.debug("Creating lxd bridge: %s" %
39 " ".join(cmd_create))
40- util.subp(cmd_create)
41+ _lxc(cmd_create)
42
43 if cmd_attach:
44 log.debug("Setting up default lxd bridge: %s" %
45 " ".join(cmd_create))
46- util.subp(cmd_attach)
47+ _lxc(cmd_attach)
48
49 elif bridge_cfg:
50 raise RuntimeError(
51@@ -204,10 +213,10 @@ def bridge_to_cmd(bridge_cfg):
52 if bridge_cfg.get("mode") == "none":
53 return None, None
54
55- bridge_name = bridge_cfg.get("name", "lxdbr0")
56+ bridge_name = bridge_cfg.get("name", _DEFAULT_NETWORK_NAME)
57 cmd_create = []
58- cmd_attach = ["lxc", "network", "attach-profile", bridge_name,
59- "default", "eth0", "--force-local"]
60+ cmd_attach = ["network", "attach-profile", bridge_name,
61+ "default", "eth0"]
62
63 if bridge_cfg.get("mode") == "existing":
64 return None, cmd_attach
65@@ -215,7 +224,7 @@ def bridge_to_cmd(bridge_cfg):
66 if bridge_cfg.get("mode") != "new":
67 raise Exception("invalid bridge mode \"%s\"" % bridge_cfg.get("mode"))
68
69- cmd_create = ["lxc", "network", "create", bridge_name]
70+ cmd_create = ["network", "create", bridge_name]
71
72 if bridge_cfg.get("ipv4_address") and bridge_cfg.get("ipv4_netmask"):
73 cmd_create.append("ipv4.address=%s/%s" %
74@@ -247,8 +256,47 @@ def bridge_to_cmd(bridge_cfg):
75 if bridge_cfg.get("domain"):
76 cmd_create.append("dns.domain=%s" % bridge_cfg.get("domain"))
77
78- cmd_create.append("--force-local")
79-
80 return cmd_create, cmd_attach
81
82+
83+def _lxc(cmd):
84+ env = {'LC_ALL': 'C'}
85+ util.subp(['lxc'] + list(cmd) + ["--force-local"], update_env=env)
86+
87+
88+def maybe_cleanup_default(net_name, did_init, create, attach,
89+ profile="default", nic_name="eth0"):
90+ """Newer versions of lxc (3.0.1+) create a lxdbr0 network when
91+ 'lxd init --auto' is run. Older versions did not.
92+
93+ By removing ay that lxd-init created, we simply leave the add/attach
94+ code in-tact.
95+
96+ https://github.com/lxc/lxd/issues/4649"""
97+ if net_name != _DEFAULT_NETWORK_NAME or not did_init:
98+ return
99+
100+ fail_assume_enoent = " failed. Assuming it did not exist."
101+ succeeded = " succeeded."
102+ if create:
103+ msg = "Deletion of lxd network '%s'" % net_name
104+ try:
105+ _lxc(["network", "delete", net_name])
106+ LOG.debug(msg + succeeded)
107+ except util.ProcessExecutionError as e:
108+ if e.exit_code != 1:
109+ raise e
110+ LOG.debug(msg + fail_assume_enoent)
111+
112+ if attach:
113+ msg = "Removal of device '%s' from profile '%s'" % (nic_name, profile)
114+ try:
115+ _lxc(["profile", "device", "remove", profile, nic_name])
116+ LOG.debug(msg + succeeded)
117+ except util.ProcessExecutionError as e:
118+ if e.exit_code != 1:
119+ raise e
120+ LOG.debug(msg + fail_assume_enoent)
121+
122+
123 # vi: ts=4 expandtab
124diff --git a/tests/unittests/test_handler/test_handler_lxd.py b/tests/unittests/test_handler/test_handler_lxd.py
125index a205498..4dd7e09 100644
126--- a/tests/unittests/test_handler/test_handler_lxd.py
127+++ b/tests/unittests/test_handler/test_handler_lxd.py
128@@ -33,12 +33,16 @@ class TestLxd(t_help.CiTestCase):
129 cc = cloud.Cloud(ds, paths, {}, d, None)
130 return cc
131
132+ @mock.patch("cloudinit.config.cc_lxd.maybe_cleanup_default")
133 @mock.patch("cloudinit.config.cc_lxd.util")
134- def test_lxd_init(self, mock_util):
135+ def test_lxd_init(self, mock_util, m_maybe_clean):
136 cc = self._get_cloud('ubuntu')
137 mock_util.which.return_value = True
138+ m_maybe_clean.return_value = None
139 cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
140 self.assertTrue(mock_util.which.called)
141+ # no bridge config, so maybe_cleanup should not be called.
142+ self.assertFalse(m_maybe_clean.called)
143 init_call = mock_util.subp.call_args_list[0][0][0]
144 self.assertEqual(init_call,
145 ['lxd', 'init', '--auto',
146@@ -46,32 +50,39 @@ class TestLxd(t_help.CiTestCase):
147 '--storage-backend=zfs',
148 '--storage-pool=poolname'])
149
150+ @mock.patch("cloudinit.config.cc_lxd.maybe_cleanup_default")
151 @mock.patch("cloudinit.config.cc_lxd.util")
152- def test_lxd_install(self, mock_util):
153+ def test_lxd_install(self, mock_util, m_maybe_clean):
154 cc = self._get_cloud('ubuntu')
155 cc.distro = mock.MagicMock()
156 mock_util.which.return_value = None
157 cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
158 self.assertNotIn('WARN', self.logs.getvalue())
159 self.assertTrue(cc.distro.install_packages.called)
160+ cc_lxd.handle('cc_lxd', self.lxd_cfg, cc, self.logger, [])
161+ self.assertFalse(m_maybe_clean.called)
162 install_pkg = cc.distro.install_packages.call_args_list[0][0][0]
163 self.assertEqual(sorted(install_pkg), ['lxd', 'zfs'])
164
165+ @mock.patch("cloudinit.config.cc_lxd.maybe_cleanup_default")
166 @mock.patch("cloudinit.config.cc_lxd.util")
167- def test_no_init_does_nothing(self, mock_util):
168+ def test_no_init_does_nothing(self, mock_util, m_maybe_clean):
169 cc = self._get_cloud('ubuntu')
170 cc.distro = mock.MagicMock()
171 cc_lxd.handle('cc_lxd', {'lxd': {}}, cc, self.logger, [])
172 self.assertFalse(cc.distro.install_packages.called)
173 self.assertFalse(mock_util.subp.called)
174+ self.assertFalse(m_maybe_clean.called)
175
176+ @mock.patch("cloudinit.config.cc_lxd.maybe_cleanup_default")
177 @mock.patch("cloudinit.config.cc_lxd.util")
178- def test_no_lxd_does_nothing(self, mock_util):
179+ def test_no_lxd_does_nothing(self, mock_util, m_maybe_clean):
180 cc = self._get_cloud('ubuntu')
181 cc.distro = mock.MagicMock()
182 cc_lxd.handle('cc_lxd', {'package_update': True}, cc, self.logger, [])
183 self.assertFalse(cc.distro.install_packages.called)
184 self.assertFalse(mock_util.subp.called)
185+ self.assertFalse(m_maybe_clean.called)
186
187 def test_lxd_debconf_new_full(self):
188 data = {"mode": "new",
189@@ -147,14 +158,13 @@ class TestLxd(t_help.CiTestCase):
190 "domain": "lxd"}
191 self.assertEqual(
192 cc_lxd.bridge_to_cmd(data),
193- (["lxc", "network", "create", "testbr0",
194+ (["network", "create", "testbr0",
195 "ipv4.address=10.0.8.1/24", "ipv4.nat=true",
196 "ipv4.dhcp.ranges=10.0.8.2-10.0.8.254",
197 "ipv6.address=fd98:9e0:3744::1/64",
198- "ipv6.nat=true", "dns.domain=lxd",
199- "--force-local"],
200- ["lxc", "network", "attach-profile",
201- "testbr0", "default", "eth0", "--force-local"]))
202+ "ipv6.nat=true", "dns.domain=lxd"],
203+ ["network", "attach-profile",
204+ "testbr0", "default", "eth0"]))
205
206 def test_lxd_cmd_new_partial(self):
207 data = {"mode": "new",
208@@ -163,19 +173,18 @@ class TestLxd(t_help.CiTestCase):
209 "ipv6_nat": "true"}
210 self.assertEqual(
211 cc_lxd.bridge_to_cmd(data),
212- (["lxc", "network", "create", "lxdbr0", "ipv4.address=none",
213- "ipv6.address=fd98:9e0:3744::1/64", "ipv6.nat=true",
214- "--force-local"],
215- ["lxc", "network", "attach-profile",
216- "lxdbr0", "default", "eth0", "--force-local"]))
217+ (["network", "create", "lxdbr0", "ipv4.address=none",
218+ "ipv6.address=fd98:9e0:3744::1/64", "ipv6.nat=true"],
219+ ["network", "attach-profile",
220+ "lxdbr0", "default", "eth0"]))
221
222 def test_lxd_cmd_existing(self):
223 data = {"mode": "existing",
224 "name": "testbr0"}
225 self.assertEqual(
226 cc_lxd.bridge_to_cmd(data),
227- (None, ["lxc", "network", "attach-profile",
228- "testbr0", "default", "eth0", "--force-local"]))
229+ (None, ["network", "attach-profile",
230+ "testbr0", "default", "eth0"]))
231
232 def test_lxd_cmd_none(self):
233 data = {"mode": "none"}
234@@ -183,4 +192,43 @@ class TestLxd(t_help.CiTestCase):
235 cc_lxd.bridge_to_cmd(data),
236 (None, None))
237
238+
239+class TestLxdMaybeCleanupDefault(t_help.CiTestCase):
240+ """Test the implementation of maybe_cleanup_default."""
241+
242+ defnet = cc_lxd._DEFAULT_NETWORK_NAME
243+
244+ @mock.patch("cloudinit.config.cc_lxd._lxc")
245+ def test_network_other_than_default_not_deleted(self, m_lxc):
246+ """deletion or removal should only occur if bridge is default."""
247+ cc_lxd.maybe_cleanup_default(
248+ net_name="lxdbr1", did_init=True, create=True, attach=True)
249+ m_lxc.assert_not_called()
250+
251+ @mock.patch("cloudinit.config.cc_lxd._lxc")
252+ def test_did_init_false_does_not_delete(self, m_lxc):
253+ """deletion or removal should only occur if did_init is True."""
254+ cc_lxd.maybe_cleanup_default(
255+ net_name=self.defnet, did_init=False, create=True, attach=True)
256+ m_lxc.assert_not_called()
257+
258+ @mock.patch("cloudinit.config.cc_lxd._lxc")
259+ def test_network_deleted_if_create_true(self, m_lxc):
260+ """deletion of network should occur if create is True."""
261+ cc_lxd.maybe_cleanup_default(
262+ net_name=self.defnet, did_init=True, create=True, attach=False)
263+ m_lxc.assert_called_once_with(["network", "delete", self.defnet])
264+
265+ @mock.patch("cloudinit.config.cc_lxd._lxc")
266+ def test_device_removed_if_attach_true(self, m_lxc):
267+ """deletion of network should occur if create is True."""
268+ nic_name = "my_nic"
269+ profile = "my_profile"
270+ cc_lxd.maybe_cleanup_default(
271+ net_name=self.defnet, did_init=True, create=False, attach=True,
272+ profile=profile, nic_name=nic_name)
273+ m_lxc.assert_called_once_with(
274+ ["profile", "device", "remove", profile, nic_name])
275+
276+
277 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches