Merge ~xiaofengw/cloud-init:xiaofengw-fix-post-script into cloud-init:master

Proposed by Xiaofeng Wang
Status: Merged
Approved by: Ryan Harper
Approved revision: 5e1dac265dabf225718d8bcd8383b8760ca2f27f
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~xiaofengw/cloud-init:xiaofengw-fix-post-script
Merge into: cloud-init:master
Diff against target: 363 lines (+111/-155)
3 files modified
cloudinit/sources/DataSourceOVF.py (+6/-1)
cloudinit/sources/helpers/vmware/imc/config_custom_script.py (+42/-101)
tests/unittests/test_vmware/test_custom_script.py (+63/-53)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Ryan Harper Approve
Review via email: mp+368902@code.launchpad.net

Commit message

VMWare: Trigger the post customization script via cc_scripts module.

cloud-init does not trigger reboots of a VM therefore adding custom
scripts to rc.local does not execute the post scripts. This patch
moves post-scripts into per-instance scripts dir and has cc_scripts
module run the post-scripts.

Also in this branch:
  - Remove the sh interpreter and execute the customization script
    directly.
  - Update the unit test.

LP: #1833192

To post a comment you must log in.
Revision history for this message
Xiaofeng Wang (xiaofengw) wrote :

Test launched:
1. tox test passed.
2. Deploy cloud-init and perform the guest customization(specify a custom script), make sure the custom script is run and only run once.The guest customization finished successfully.
3. Deploy cloud-init and perform the guest customization(without custom script), make sure the guest customization finished successfully.
4. Repeat test 2 with sh, bash, dash custom script.

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

This looks like a bug; did you file a bug to track the issue? If not, could you do that please?

I've added some comments in-line and some questions so I can provide better feedback.

review: Needs Information
Revision history for this message
Xiaofeng Wang (xiaofengw) wrote :

> This looks like a bug; did you file a bug to track the issue? If not, could
> you do that please?
>
> I've added some comments in-line and some questions so I can provide better
> feedback.
The bug is created: https://bugs.launchpad.net/ubuntu/+source/evince/+bug/1833192
Thanks.

ecac298... by Xiaofeng Wang

Address the review comments from Ryan.
Changes to be committed:
 modified: cloudinit/sources/DataSourceOVF.py
 modified: cloudinit/sources/helpers/vmware/imc/config_custom_script.py
 modified: tests/unittests/test_vmware/test_custom_script.py

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

One minor change requested.

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

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

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

review: Approve (continuous-integration)
5e1dac2... by Xiaofeng Wang

Address comments to move variable definition to the begining

Revision history for this message
Xiaofeng Wang (xiaofengw) wrote :

> One minor change requested.
Fixed, PTAL. Thanks.

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

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

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

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

FAILED: Autolanding.
Approved revid is not set in launchpad. This is most likely a launchpad issue and re-approve should fix it. There is also a chance (although a very small one) this is a permission problem of the ps-jenkins bot.
https://jenkins.ubuntu.com/server/job/cloud-init-autoland-test/260/
Executed test runs:
    SUCCESS: Checkout
    SUCCESS: Unit & Style Tests
    SUCCESS: Ubuntu LTS: Build
    SUCCESS: Ubuntu LTS: Integration
    IN_PROGRESS: Declarative: Post Actions

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

Thanks!

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

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
2index 70e7a5c..dd941d2 100644
3--- a/cloudinit/sources/DataSourceOVF.py
4+++ b/cloudinit/sources/DataSourceOVF.py
5@@ -148,6 +148,9 @@ class DataSourceOVF(sources.DataSource):
6 product_marker, os.path.join(self.paths.cloud_dir, 'data'))
7 special_customization = product_marker and not hasmarkerfile
8 customscript = self._vmware_cust_conf.custom_script_name
9+ ccScriptsDir = os.path.join(
10+ self.paths.get_cpath("scripts"),
11+ "per-instance")
12 except Exception as e:
13 _raise_error_status(
14 "Error parsing the customization Config File",
15@@ -201,7 +204,9 @@ class DataSourceOVF(sources.DataSource):
16
17 if customscript:
18 try:
19- postcust = PostCustomScript(customscript, imcdirpath)
20+ postcust = PostCustomScript(customscript,
21+ imcdirpath,
22+ ccScriptsDir)
23 postcust.execute()
24 except Exception as e:
25 _raise_error_status(
26diff --git a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
27index a7d4ad9..9f14770 100644
28--- a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
29+++ b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
30@@ -1,5 +1,5 @@
31 # Copyright (C) 2017 Canonical Ltd.
32-# Copyright (C) 2017 VMware Inc.
33+# Copyright (C) 2017-2019 VMware Inc.
34 #
35 # Author: Maitreyee Saikia <msaikia@vmware.com>
36 #
37@@ -8,7 +8,6 @@
38 import logging
39 import os
40 import stat
41-from textwrap import dedent
42
43 from cloudinit import util
44
45@@ -20,12 +19,15 @@ class CustomScriptNotFound(Exception):
46
47
48 class CustomScriptConstant(object):
49- RC_LOCAL = "/etc/rc.local"
50- POST_CUST_TMP_DIR = "/root/.customization"
51- POST_CUST_RUN_SCRIPT_NAME = "post-customize-guest.sh"
52- POST_CUST_RUN_SCRIPT = os.path.join(POST_CUST_TMP_DIR,
53- POST_CUST_RUN_SCRIPT_NAME)
54- POST_REBOOT_PENDING_MARKER = "/.guest-customization-post-reboot-pending"
55+ CUSTOM_TMP_DIR = "/root/.customization"
56+
57+ # The user defined custom script
58+ CUSTOM_SCRIPT_NAME = "customize.sh"
59+ CUSTOM_SCRIPT = os.path.join(CUSTOM_TMP_DIR,
60+ CUSTOM_SCRIPT_NAME)
61+ POST_CUSTOM_PENDING_MARKER = "/.guest-customization-post-reboot-pending"
62+ # The cc_scripts_per_instance script to launch custom script
63+ POST_CUSTOM_SCRIPT_NAME = "post-customize-guest.sh"
64
65
66 class RunCustomScript(object):
67@@ -39,10 +41,19 @@ class RunCustomScript(object):
68 raise CustomScriptNotFound("Script %s not found!! "
69 "Cannot execute custom script!"
70 % self.scriptpath)
71+
72+ util.ensure_dir(CustomScriptConstant.CUSTOM_TMP_DIR)
73+
74+ LOG.debug("Copying custom script to %s",
75+ CustomScriptConstant.CUSTOM_SCRIPT)
76+ util.copy(self.scriptpath, CustomScriptConstant.CUSTOM_SCRIPT)
77+
78 # Strip any CR characters from the decoded script
79- util.load_file(self.scriptpath).replace("\r", "")
80- st = os.stat(self.scriptpath)
81- os.chmod(self.scriptpath, st.st_mode | stat.S_IEXEC)
82+ content = util.load_file(
83+ CustomScriptConstant.CUSTOM_SCRIPT).replace("\r", "")
84+ util.write_file(CustomScriptConstant.CUSTOM_SCRIPT,
85+ content,
86+ mode=0o544)
87
88
89 class PreCustomScript(RunCustomScript):
90@@ -50,104 +61,34 @@ class PreCustomScript(RunCustomScript):
91 """Executing custom script with precustomization argument."""
92 LOG.debug("Executing pre-customization script")
93 self.prepare_script()
94- util.subp(["/bin/sh", self.scriptpath, "precustomization"])
95+ util.subp([CustomScriptConstant.CUSTOM_SCRIPT, "precustomization"])
96
97
98 class PostCustomScript(RunCustomScript):
99- def __init__(self, scriptname, directory):
100+ def __init__(self, scriptname, directory, ccScriptsDir):
101 super(PostCustomScript, self).__init__(scriptname, directory)
102- # Determine when to run custom script. When postreboot is True,
103- # the user uploaded script will run as part of rc.local after
104- # the machine reboots. This is determined by presence of rclocal.
105- # When postreboot is False, script will run as part of cloud-init.
106- self.postreboot = False
107-
108- def _install_post_reboot_agent(self, rclocal):
109- """
110- Install post-reboot agent for running custom script after reboot.
111- As part of this process, we are editing the rclocal file to run a
112- VMware script, which in turn is resposible for handling the user
113- script.
114- @param: path to rc local.
115- """
116- LOG.debug("Installing post-reboot customization from %s to %s",
117- self.directory, rclocal)
118- if not self.has_previous_agent(rclocal):
119- LOG.info("Adding post-reboot customization agent to rc.local")
120- new_content = dedent("""
121- # Run post-reboot guest customization
122- /bin/sh %s
123- exit 0
124- """) % CustomScriptConstant.POST_CUST_RUN_SCRIPT
125- existing_rclocal = util.load_file(rclocal).replace('exit 0\n', '')
126- st = os.stat(rclocal)
127- # "x" flag should be set
128- mode = st.st_mode | stat.S_IEXEC
129- util.write_file(rclocal, existing_rclocal + new_content, mode)
130-
131- else:
132- # We don't need to update rclocal file everytime a customization
133- # is requested. It just needs to be done for the first time.
134- LOG.info("Post-reboot guest customization agent is already "
135- "registered in rc.local")
136- LOG.debug("Installing post-reboot customization agent finished: %s",
137- self.postreboot)
138-
139- def has_previous_agent(self, rclocal):
140- searchstring = "# Run post-reboot guest customization"
141- if searchstring in open(rclocal).read():
142- return True
143- return False
144-
145- def find_rc_local(self):
146- """
147- Determine if rc local is present.
148- """
149- rclocal = ""
150- if os.path.exists(CustomScriptConstant.RC_LOCAL):
151- LOG.debug("rc.local detected.")
152- # resolving in case of symlink
153- rclocal = os.path.realpath(CustomScriptConstant.RC_LOCAL)
154- LOG.debug("rc.local resolved to %s", rclocal)
155- else:
156- LOG.warning("Can't find rc.local, post-customization "
157- "will be run before reboot")
158- return rclocal
159-
160- def install_agent(self):
161- rclocal = self.find_rc_local()
162- if rclocal:
163- self._install_post_reboot_agent(rclocal)
164- self.postreboot = True
165+ self.ccScriptsDir = ccScriptsDir
166+ self.ccScriptPath = os.path.join(
167+ ccScriptsDir,
168+ CustomScriptConstant.POST_CUSTOM_SCRIPT_NAME)
169
170 def execute(self):
171 """
172- This method executes post-customization script before or after reboot
173- based on the presence of rc local.
174+ This method copy the post customize run script to
175+ cc_scripts_per_instance directory and let this
176+ module to run post custom script.
177 """
178 self.prepare_script()
179- self.install_agent()
180- if not self.postreboot:
181- LOG.warning("Executing post-customization script inline")
182- util.subp(["/bin/sh", self.scriptpath, "postcustomization"])
183- else:
184- LOG.debug("Scheduling custom script to run post reboot")
185- if not os.path.isdir(CustomScriptConstant.POST_CUST_TMP_DIR):
186- os.mkdir(CustomScriptConstant.POST_CUST_TMP_DIR)
187- # Script "post-customize-guest.sh" and user uploaded script are
188- # are present in the same directory and needs to copied to a temp
189- # directory to be executed post reboot. User uploaded script is
190- # saved as customize.sh in the temp directory.
191- # post-customize-guest.sh excutes customize.sh after reboot.
192- LOG.debug("Copying post-customization script")
193- util.copy(self.scriptpath,
194- CustomScriptConstant.POST_CUST_TMP_DIR + "/customize.sh")
195- LOG.debug("Copying script to run post-customization script")
196- util.copy(
197- os.path.join(self.directory,
198- CustomScriptConstant.POST_CUST_RUN_SCRIPT_NAME),
199- CustomScriptConstant.POST_CUST_RUN_SCRIPT)
200- LOG.info("Creating post-reboot pending marker")
201- util.ensure_file(CustomScriptConstant.POST_REBOOT_PENDING_MARKER)
202+
203+ LOG.debug("Copying post customize run script to %s",
204+ self.ccScriptPath)
205+ util.copy(
206+ os.path.join(self.directory,
207+ CustomScriptConstant.POST_CUSTOM_SCRIPT_NAME),
208+ self.ccScriptPath)
209+ st = os.stat(self.ccScriptPath)
210+ os.chmod(self.ccScriptPath, st.st_mode | stat.S_IEXEC)
211+ LOG.info("Creating post customization pending marker")
212+ util.ensure_file(CustomScriptConstant.POST_CUSTOM_PENDING_MARKER)
213
214 # vi: ts=4 expandtab
215diff --git a/tests/unittests/test_vmware/test_custom_script.py b/tests/unittests/test_vmware/test_custom_script.py
216index 2d9519b..f89f815 100644
217--- a/tests/unittests/test_vmware/test_custom_script.py
218+++ b/tests/unittests/test_vmware/test_custom_script.py
219@@ -1,10 +1,12 @@
220 # Copyright (C) 2015 Canonical Ltd.
221-# Copyright (C) 2017 VMware INC.
222+# Copyright (C) 2017-2019 VMware INC.
223 #
224 # Author: Maitreyee Saikia <msaikia@vmware.com>
225 #
226 # This file is part of cloud-init. See LICENSE file for license information.
227
228+import os
229+import stat
230 from cloudinit import util
231 from cloudinit.sources.helpers.vmware.imc.config_custom_script import (
232 CustomScriptConstant,
233@@ -18,6 +20,10 @@ from cloudinit.tests.helpers import CiTestCase, mock
234 class TestVmwareCustomScript(CiTestCase):
235 def setUp(self):
236 self.tmpDir = self.tmp_dir()
237+ # Mock the tmpDir as the root dir in VM.
238+ self.execDir = os.path.join(self.tmpDir, ".customization")
239+ self.execScript = os.path.join(self.execDir,
240+ ".customize.sh")
241
242 def test_prepare_custom_script(self):
243 """
244@@ -37,63 +43,67 @@ class TestVmwareCustomScript(CiTestCase):
245
246 # Custom script exists.
247 custScript = self.tmp_path("test-cust", self.tmpDir)
248- util.write_file(custScript, "test-CR-strip/r/r")
249- postCust = PostCustomScript("test-cust", self.tmpDir)
250- self.assertEqual("test-cust", postCust.scriptname)
251- self.assertEqual(self.tmpDir, postCust.directory)
252- self.assertEqual(custScript, postCust.scriptpath)
253- self.assertFalse(postCust.postreboot)
254- postCust.prepare_script()
255- # Check if all carraige returns are stripped from script.
256- self.assertFalse("/r" in custScript)
257+ util.write_file(custScript, "test-CR-strip\r\r")
258+ with mock.patch.object(CustomScriptConstant,
259+ "CUSTOM_TMP_DIR",
260+ self.execDir):
261+ with mock.patch.object(CustomScriptConstant,
262+ "CUSTOM_SCRIPT",
263+ self.execScript):
264+ postCust = PostCustomScript("test-cust",
265+ self.tmpDir,
266+ self.tmpDir)
267+ self.assertEqual("test-cust", postCust.scriptname)
268+ self.assertEqual(self.tmpDir, postCust.directory)
269+ self.assertEqual(custScript, postCust.scriptpath)
270+ postCust.prepare_script()
271
272- def test_rc_local_exists(self):
273- """
274- This test is designed to verify the different scenarios associated
275- with the presence of rclocal.
276- """
277- # test when rc local does not exist
278- postCust = PostCustomScript("test-cust", self.tmpDir)
279- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", "/no/path"):
280- rclocal = postCust.find_rc_local()
281- self.assertEqual("", rclocal)
282-
283- # test when rc local exists
284- rclocalFile = self.tmp_path("vmware-rclocal", self.tmpDir)
285- util.write_file(rclocalFile, "# Run post-reboot guest customization",
286- omode="w")
287- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocalFile):
288- rclocal = postCust.find_rc_local()
289- self.assertEqual(rclocalFile, rclocal)
290- self.assertTrue(postCust.has_previous_agent, rclocal)
291-
292- # test when rc local is a symlink
293- rclocalLink = self.tmp_path("dummy-rclocal-link", self.tmpDir)
294- util.sym_link(rclocalFile, rclocalLink, True)
295- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocalLink):
296- rclocal = postCust.find_rc_local()
297- self.assertEqual(rclocalFile, rclocal)
298+ # Custom script is copied with exec privilege
299+ self.assertTrue(os.path.exists(self.execScript))
300+ st = os.stat(self.execScript)
301+ self.assertTrue(st.st_mode & stat.S_IEXEC)
302+ with open(self.execScript, "r") as f:
303+ content = f.read()
304+ self.assertEqual(content, "test-CR-strip")
305+ # Check if all carraige returns are stripped from script.
306+ self.assertFalse("\r" in content)
307
308 def test_execute_post_cust(self):
309 """
310- This test is to identify if rclocal was properly populated to be
311- run after reboot.
312+ This test is designed to verify the behavior after execute post
313+ customization.
314 """
315- customscript = self.tmp_path("vmware-post-cust-script", self.tmpDir)
316- rclocal = self.tmp_path("vmware-rclocal", self.tmpDir)
317- # Create a temporary rclocal file
318- open(customscript, "w")
319- util.write_file(rclocal, "tests\nexit 0", omode="w")
320- postCust = PostCustomScript("vmware-post-cust-script", self.tmpDir)
321- with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocal):
322- # Test that guest customization agent is not installed initially.
323- self.assertFalse(postCust.postreboot)
324- self.assertIs(postCust.has_previous_agent(rclocal), False)
325- postCust.install_agent()
326+ # Prepare the customize package
327+ postCustRun = self.tmp_path("post-customize-guest.sh", self.tmpDir)
328+ util.write_file(postCustRun, "This is the script to run post cust")
329+ userScript = self.tmp_path("test-cust", self.tmpDir)
330+ util.write_file(userScript, "This is the post cust script")
331
332- # Assert rclocal has been modified to have guest customization
333- # agent.
334- self.assertTrue(postCust.postreboot)
335- self.assertTrue(postCust.has_previous_agent, rclocal)
336+ # Mock the cc_scripts_per_instance dir and marker file.
337+ # Create another tmp dir for cc_scripts_per_instance.
338+ ccScriptDir = self.tmp_dir()
339+ ccScript = os.path.join(ccScriptDir, "post-customize-guest.sh")
340+ markerFile = os.path.join(self.tmpDir, ".markerFile")
341+ with mock.patch.object(CustomScriptConstant,
342+ "CUSTOM_TMP_DIR",
343+ self.execDir):
344+ with mock.patch.object(CustomScriptConstant,
345+ "CUSTOM_SCRIPT",
346+ self.execScript):
347+ with mock.patch.object(CustomScriptConstant,
348+ "POST_CUSTOM_PENDING_MARKER",
349+ markerFile):
350+ postCust = PostCustomScript("test-cust",
351+ self.tmpDir,
352+ ccScriptDir)
353+ postCust.execute()
354+ # Check cc_scripts_per_instance and marker file
355+ # are created.
356+ self.assertTrue(os.path.exists(ccScript))
357+ with open(ccScript, "r") as f:
358+ content = f.read()
359+ self.assertEqual(content,
360+ "This is the script to run post cust")
361+ self.assertTrue(os.path.exists(markerFile))
362
363 # vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches