Merge ~smoser/cloud-init:test/ibmcloud-debug-build into cloud-init:master

Proposed by Scott Moser
Status: Merged
Approved by: Ryan Harper
Approved revision: 8264db518943c6260f904c7db95d7399cfcb6ca0
Merge reported by: Ryan Harper
Merged at revision: 6ef92c98c3d2b127b05d6708337efc8a81e00071
Proposed branch: ~smoser/cloud-init:test/ibmcloud-debug-build
Merge into: cloud-init:master
Diff against target: 373 lines (+175/-23)
5 files modified
cloudinit/sources/DataSourceIBMCloud.py (+29/-13)
cloudinit/tests/helpers.py (+12/-1)
tests/unittests/test_datasource/test_ibmcloud.py (+50/-0)
tests/unittests/test_ds_identify.py (+64/-8)
tools/ds-identify (+20/-1)
Reviewer Review Type Date Requested Status
Philip Roche (community) Approve
Ryan Harper Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+344546@code.launchpad.net

Commit message

IBMCloud: recognize provisioning environment during debug boots.

When images are deployed from template in a production environment
the artifacts of the provisioning stage (provisioningConfiguration.cfg)
that cloud-init referenced are cleaned up. However, when provisioned
in "debug" mode (internal to IBM) the artifacts are left.

This changes the 'is_ibm_provisioning' implementations in both
ds-identify and in the IBM datasource to identify the provisioning
stage more correctly. The change is to consider provisioning only
if the provisioing file existed and there was no log file or
the log file was older than this boot.

LP: #1767166

Description of the change

see commit message

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

PASSED: Continuous integration, rev:60214a14f174178bc7d5297dec176f0e4f904beb
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1066/
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/1066/rebuild

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

PASSED: Continuous integration, rev:cc93d1dfab1b73ef2636321bb1d3f1eac3997421
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1067/
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/1067/rebuild

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

PASSED: Continuous integration, rev:8264db518943c6260f904c7db95d7399cfcb6ca0
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1068/
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/1068/rebuild

review: Approve (continuous-integration)
Revision history for this message
Ryan Harper (raharper) wrote :

Looks good, couple of comments in line.

review: Approve
Revision history for this message
Scott Moser (smoser) :
Revision history for this message
Scott Moser (smoser) wrote :

I'm expecting to get a build from Phil shortly with this in it, and then we'll give it a run on IBMcloud and then pull.

Revision history for this message
Scott Moser (smoser) wrote :

OK. I got builds from Phil witih this in it, and I have run
 a.) template + user-data
 b.) template without user-data
 c.) os_code with user-data
 d.) os_code without user-data.

When I launch the image without cloud-init enabled and pass it user-data
seeing cloud-init run until I do a reboot. When I'm able to ssh in,
cloud-init says it is disabled because it was in the provisioning stage.
I suspect that what is happening is that the system did not get rebooted
after provisioning.

In all my testing with xenial there was a FINAL_REBOOT stage that would
be done, but that does not appear to be happening on 18.04 images.

I'm looking for information on why that might be.

Revision history for this message
Philip Roche (philroche) wrote :

Changes and approach look good to me and are as per SL's description of what should be happening wrt reboots.

review: Approve
Revision history for this message
Ryan Harper (raharper) wrote :

An upstream commit landed for this bug.

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

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/sources/DataSourceIBMCloud.py b/cloudinit/sources/DataSourceIBMCloud.py
2index cfa724b..01106ec 100644
3--- a/cloudinit/sources/DataSourceIBMCloud.py
4+++ b/cloudinit/sources/DataSourceIBMCloud.py
5@@ -8,17 +8,11 @@ There are 2 different api exposed launch methods.
6 * template: This is the legacy method of launching instances.
7 When booting from an image template, the system boots first into
8 a "provisioning" mode. There, host <-> guest mechanisms are utilized
9- to execute code in the guest and provision it.
10+ to execute code in the guest and configure it. The configuration
11+ includes configuring the system network and possibly installing
12+ packages and other software stack.
13
14- Cloud-init will disable itself when it detects that it is in the
15- provisioning mode. It detects this by the presence of
16- a file '/root/provisioningConfiguration.cfg'.
17-
18- When provided with user-data, the "first boot" will contain a
19- ConfigDrive-like disk labeled with 'METADATA'. If there is no user-data
20- provided, then there is no data-source.
21-
22- Cloud-init never does any network configuration in this mode.
23+ After the provisioning is finished, the system reboots.
24
25 * os_code: Essentially "launch by OS Code" (Operating System Code).
26 This is a more modern approach. There is no specific "provisioning" boot.
27@@ -200,8 +194,30 @@ def _is_xen():
28 return os.path.exists("/proc/xen")
29
30
31-def _is_ibm_provisioning():
32- return os.path.exists("/root/provisioningConfiguration.cfg")
33+def _is_ibm_provisioning(
34+ prov_cfg="/root/provisioningConfiguration.cfg",
35+ inst_log="/root/swinstall.log",
36+ boot_ref="/proc/1/environ"):
37+ """Return boolean indicating if this boot is ibm provisioning boot."""
38+ if os.path.exists(prov_cfg):
39+ msg = "config '%s' exists." % prov_cfg
40+ result = True
41+ if os.path.exists(inst_log):
42+ if os.path.exists(boot_ref):
43+ result = (os.stat(inst_log).st_mtime >
44+ os.stat(boot_ref).st_mtime)
45+ msg += (" log '%s' from %s boot." %
46+ (inst_log, "current" if result else "previous"))
47+ else:
48+ msg += (" log '%s' existed, but no reference file '%s'." %
49+ (inst_log, boot_ref))
50+ result = False
51+ else:
52+ msg += " log '%s' did not exist." % inst_log
53+ else:
54+ result, msg = (False, "config '%s' did not exist." % prov_cfg)
55+ LOG.debug("ibm_provisioning=%s: %s", result, msg)
56+ return result
57
58
59 def get_ibm_platform():
60@@ -251,7 +267,7 @@ def get_ibm_platform():
61 else:
62 return (Platforms.TEMPLATE_LIVE_METADATA, metadata_path)
63 elif _is_ibm_provisioning():
64- return (Platforms.TEMPLATE_PROVISIONING_NODATA, None)
65+ return (Platforms.TEMPLATE_PROVISIONING_NODATA, None)
66 return not_found
67
68
69diff --git a/cloudinit/tests/helpers.py b/cloudinit/tests/helpers.py
70index 4999f1f..117a9cf 100644
71--- a/cloudinit/tests/helpers.py
72+++ b/cloudinit/tests/helpers.py
73@@ -8,6 +8,7 @@ import os
74 import shutil
75 import sys
76 import tempfile
77+import time
78 import unittest
79
80 import mock
81@@ -263,7 +264,8 @@ class FilesystemMockingTestCase(ResourceUsingTestCase):
82 os.path: [('isfile', 1), ('exists', 1),
83 ('islink', 1), ('isdir', 1), ('lexists', 1)],
84 os: [('listdir', 1), ('mkdir', 1),
85- ('lstat', 1), ('symlink', 2)]
86+ ('lstat', 1), ('symlink', 2),
87+ ('stat', 1)]
88 }
89
90 if hasattr(os, 'scandir'):
91@@ -349,6 +351,15 @@ def populate_dir(path, files):
92 return ret
93
94
95+def populate_dir_with_ts(path, data):
96+ """data is {'file': ('contents', mtime)}. mtime relative to now."""
97+ populate_dir(path, dict((k, v[0]) for k, v in data.items()))
98+ btime = time.time()
99+ for fpath, (_contents, mtime) in data.items():
100+ ts = btime + mtime if mtime else btime
101+ os.utime(os.path.sep.join((path, fpath)), (ts, ts))
102+
103+
104 def dir2dict(startdir, prefix=None):
105 flist = {}
106 if prefix is None:
107diff --git a/tests/unittests/test_datasource/test_ibmcloud.py b/tests/unittests/test_datasource/test_ibmcloud.py
108index 621cfe4..e639ae4 100644
109--- a/tests/unittests/test_datasource/test_ibmcloud.py
110+++ b/tests/unittests/test_datasource/test_ibmcloud.py
111@@ -259,4 +259,54 @@ class TestReadMD(test_helpers.CiTestCase):
112 ret['metadata'])
113
114
115+class TestIsIBMProvisioning(test_helpers.FilesystemMockingTestCase):
116+ """Test the _is_ibm_provisioning method."""
117+ inst_log = "/root/swinstall.log"
118+ prov_cfg = "/root/provisioningConfiguration.cfg"
119+ boot_ref = "/proc/1/environ"
120+ with_logs = True
121+
122+ def _call_with_root(self, rootd):
123+ self.reRoot(rootd)
124+ return ibm._is_ibm_provisioning()
125+
126+ def test_no_config(self):
127+ """No provisioning config means not provisioning."""
128+ self.assertFalse(self._call_with_root(self.tmp_dir()))
129+
130+ def test_config_only(self):
131+ """A provisioning config without a log means provisioning."""
132+ rootd = self.tmp_dir()
133+ test_helpers.populate_dir(rootd, {self.prov_cfg: "key=value"})
134+ self.assertTrue(self._call_with_root(rootd))
135+
136+ def test_config_with_old_log(self):
137+ """A config with a log from previous boot is not provisioning."""
138+ rootd = self.tmp_dir()
139+ data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10),
140+ self.inst_log: ("log data\n", -30),
141+ self.boot_ref: ("PWD=/", 0)}
142+ test_helpers.populate_dir_with_ts(rootd, data)
143+ self.assertFalse(self._call_with_root(rootd=rootd))
144+ self.assertIn("from previous boot", self.logs.getvalue())
145+
146+ def test_config_with_new_log(self):
147+ """A config with a log from this boot is provisioning."""
148+ rootd = self.tmp_dir()
149+ data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10),
150+ self.inst_log: ("log data\n", 30),
151+ self.boot_ref: ("PWD=/", 0)}
152+ test_helpers.populate_dir_with_ts(rootd, data)
153+ self.assertTrue(self._call_with_root(rootd=rootd))
154+ self.assertIn("from current boot", self.logs.getvalue())
155+
156+ def test_config_and_log_no_reference(self):
157+ """If the config and log existed, but no reference, assume not."""
158+ rootd = self.tmp_dir()
159+ test_helpers.populate_dir(
160+ rootd, {self.prov_cfg: "key=value", self.inst_log: "log data\n"})
161+ self.assertFalse(self._call_with_root(rootd=rootd))
162+ self.assertIn("no reference file", self.logs.getvalue())
163+
164+
165 # vi: ts=4 expandtab
166diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
167index 5364398..ad7fe41 100644
168--- a/tests/unittests/test_ds_identify.py
169+++ b/tests/unittests/test_ds_identify.py
170@@ -1,5 +1,6 @@
171 # This file is part of cloud-init. See LICENSE file for license information.
172
173+from collections import namedtuple
174 import copy
175 import os
176 from uuid import uuid4
177@@ -7,7 +8,7 @@ from uuid import uuid4
178 from cloudinit import safeyaml
179 from cloudinit import util
180 from cloudinit.tests.helpers import (
181- CiTestCase, dir2dict, populate_dir)
182+ CiTestCase, dir2dict, populate_dir, populate_dir_with_ts)
183
184 from cloudinit.sources import DataSourceIBMCloud as dsibm
185
186@@ -66,7 +67,6 @@ P_SYS_VENDOR = "sys/class/dmi/id/sys_vendor"
187 P_SEED_DIR = "var/lib/cloud/seed"
188 P_DSID_CFG = "etc/cloud/ds-identify.cfg"
189
190-IBM_PROVISIONING_CHECK_PATH = "/root/provisioningConfiguration.cfg"
191 IBM_CONFIG_UUID = "9796-932E"
192
193 MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}
194@@ -74,11 +74,17 @@ MOCK_VIRT_IS_VMWARE = {'name': 'detect_virt', 'RET': 'vmware', 'ret': 0}
195 MOCK_VIRT_IS_XEN = {'name': 'detect_virt', 'RET': 'xen', 'ret': 0}
196 MOCK_UNAME_IS_PPC64 = {'name': 'uname', 'out': UNAME_PPC64EL, 'ret': 0}
197
198+shell_true = 0
199+shell_false = 1
200
201-class TestDsIdentify(CiTestCase):
202+CallReturn = namedtuple('CallReturn',
203+ ['rc', 'stdout', 'stderr', 'cfg', 'files'])
204+
205+
206+class DsIdentifyBase(CiTestCase):
207 dsid_path = os.path.realpath('tools/ds-identify')
208
209- def call(self, rootd=None, mocks=None, args=None, files=None,
210+ def call(self, rootd=None, mocks=None, func="main", args=None, files=None,
211 policy_dmi=DI_DEFAULT_POLICY,
212 policy_no_dmi=DI_DEFAULT_POLICY_NO_DMI,
213 ec2_strict_id=DI_EC2_STRICT_ID_DEFAULT):
214@@ -135,7 +141,7 @@ class TestDsIdentify(CiTestCase):
215 mocklines.append(write_mock(d))
216
217 endlines = [
218- 'main %s' % ' '.join(['"%s"' % s for s in args])
219+ func + ' ' + ' '.join(['"%s"' % s for s in args])
220 ]
221
222 with open(wrap, "w") as fp:
223@@ -159,7 +165,7 @@ class TestDsIdentify(CiTestCase):
224 cfg = {"_INVALID_YAML": contents,
225 "_EXCEPTION": str(e)}
226
227- return rc, out, err, cfg, dir2dict(rootd)
228+ return CallReturn(rc, out, err, cfg, dir2dict(rootd))
229
230 def _call_via_dict(self, data, rootd=None, **kwargs):
231 # return output of self.call with a dict input like VALID_CFG[item]
232@@ -190,6 +196,8 @@ class TestDsIdentify(CiTestCase):
233 _print_run_output(rc, out, err, cfg, files)
234 return rc, out, err, cfg, files
235
236+
237+class TestDsIdentify(DsIdentifyBase):
238 def test_wb_print_variables(self):
239 """_print_info reports an array of discovered variables to stderr."""
240 data = VALID_CFG['Azure-dmi-detection']
241@@ -250,7 +258,10 @@ class TestDsIdentify(CiTestCase):
242 Template provisioning with user-data has METADATA disk,
243 datasource should return not found."""
244 data = copy.deepcopy(VALID_CFG['IBMCloud-metadata'])
245- data['files'] = {IBM_PROVISIONING_CHECK_PATH: 'xxx'}
246+ # change the 'is_ibm_provisioning' mock to return 1 (false)
247+ isprov_m = [m for m in data['mocks']
248+ if m["name"] == "is_ibm_provisioning"][0]
249+ isprov_m['ret'] = shell_true
250 return self._check_via_dict(data, RC_NOT_FOUND)
251
252 def test_ibmcloud_template_userdata(self):
253@@ -265,7 +276,8 @@ class TestDsIdentify(CiTestCase):
254
255 no disks attached. Datasource should return not found."""
256 data = copy.deepcopy(VALID_CFG['IBMCloud-nodisks'])
257- data['files'] = {IBM_PROVISIONING_CHECK_PATH: 'xxx'}
258+ data['mocks'].append(
259+ {'name': 'is_ibm_provisioning', 'ret': shell_true})
260 return self._check_via_dict(data, RC_NOT_FOUND)
261
262 def test_ibmcloud_template_no_userdata(self):
263@@ -446,6 +458,47 @@ class TestDsIdentify(CiTestCase):
264 self._test_ds_found('Hetzner')
265
266
267+class TestIsIBMProvisioning(DsIdentifyBase):
268+ """Test the is_ibm_provisioning method in ds-identify."""
269+
270+ inst_log = "/root/swinstall.log"
271+ prov_cfg = "/root/provisioningConfiguration.cfg"
272+ boot_ref = "/proc/1/environ"
273+ funcname = "is_ibm_provisioning"
274+
275+ def test_no_config(self):
276+ """No provisioning config means not provisioning."""
277+ ret = self.call(files={}, func=self.funcname)
278+ self.assertEqual(shell_false, ret.rc)
279+
280+ def test_config_only(self):
281+ """A provisioning config without a log means provisioning."""
282+ ret = self.call(files={self.prov_cfg: "key=value"}, func=self.funcname)
283+ self.assertEqual(shell_true, ret.rc)
284+
285+ def test_config_with_old_log(self):
286+ """A config with a log from previous boot is not provisioning."""
287+ rootd = self.tmp_dir()
288+ data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10),
289+ self.inst_log: ("log data\n", -30),
290+ self.boot_ref: ("PWD=/", 0)}
291+ populate_dir_with_ts(rootd, data)
292+ ret = self.call(rootd=rootd, func=self.funcname)
293+ self.assertEqual(shell_false, ret.rc)
294+ self.assertIn("from previous boot", ret.stderr)
295+
296+ def test_config_with_new_log(self):
297+ """A config with a log from this boot is provisioning."""
298+ rootd = self.tmp_dir()
299+ data = {self.prov_cfg: ("key=value\nkey2=val2\n", -10),
300+ self.inst_log: ("log data\n", 30),
301+ self.boot_ref: ("PWD=/", 0)}
302+ populate_dir_with_ts(rootd, data)
303+ ret = self.call(rootd=rootd, func=self.funcname)
304+ self.assertEqual(shell_true, ret.rc)
305+ self.assertIn("from current boot", ret.stderr)
306+
307+
308 def blkid_out(disks=None):
309 """Convert a list of disk dictionaries into blkid content."""
310 if disks is None:
311@@ -639,6 +692,7 @@ VALID_CFG = {
312 'ds': 'IBMCloud',
313 'mocks': [
314 MOCK_VIRT_IS_XEN,
315+ {'name': 'is_ibm_provisioning', 'ret': shell_false},
316 {'name': 'blkid', 'ret': 0,
317 'out': blkid_out(
318 [{'DEVNAME': 'xvda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()},
319@@ -652,6 +706,7 @@ VALID_CFG = {
320 'ds': 'IBMCloud',
321 'mocks': [
322 MOCK_VIRT_IS_XEN,
323+ {'name': 'is_ibm_provisioning', 'ret': shell_false},
324 {'name': 'blkid', 'ret': 0,
325 'out': blkid_out(
326 [{'DEVNAME': 'xvda1', 'TYPE': 'ext3', 'PARTUUID': uuid4(),
327@@ -669,6 +724,7 @@ VALID_CFG = {
328 'ds': 'IBMCloud',
329 'mocks': [
330 MOCK_VIRT_IS_XEN,
331+ {'name': 'is_ibm_provisioning', 'ret': shell_false},
332 {'name': 'blkid', 'ret': 0,
333 'out': blkid_out(
334 [{'DEVNAME': 'xvda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()},
335diff --git a/tools/ds-identify b/tools/ds-identify
336index 9a2db5c..7fff5d1 100755
337--- a/tools/ds-identify
338+++ b/tools/ds-identify
339@@ -125,6 +125,7 @@ DI_ON_NOTFOUND=""
340 DI_EC2_STRICT_ID_DEFAULT="true"
341
342 _IS_IBM_CLOUD=""
343+_IS_IBM_PROVISIONING=""
344
345 error() {
346 set -- "ERROR:" "$@";
347@@ -1006,7 +1007,25 @@ dscheck_Hetzner() {
348 }
349
350 is_ibm_provisioning() {
351- [ -f "${PATH_ROOT}/root/provisioningConfiguration.cfg" ]
352+ local pcfg="${PATH_ROOT}/root/provisioningConfiguration.cfg"
353+ local logf="${PATH_ROOT}/root/swinstall.log"
354+ local is_prov=false msg="config '$pcfg' did not exist."
355+ if [ -f "$pcfg" ]; then
356+ msg="config '$pcfg' exists."
357+ is_prov=true
358+ if [ -f "$logf" ]; then
359+ if [ "$logf" -nt "$PATH_PROC_1_ENVIRON" ]; then
360+ msg="$msg log '$logf' from current boot."
361+ else
362+ is_prov=false
363+ msg="$msg log '$logf' from previous boot."
364+ fi
365+ else
366+ msg="$msg log '$logf' did not exist."
367+ fi
368+ fi
369+ debug 2 "ibm_provisioning=$is_prov: $msg"
370+ [ "$is_prov" = "true" ]
371 }
372
373 is_ibm_cloud() {

Subscribers

People subscribed via source and target branches