Merge ~xiaofengw/cloud-init:xiaofengw-add-custom-script-option into cloud-init:master

Proposed by Xiaofeng Wang
Status: Merged
Approved by: Ryan Harper
Approved revision: 68f7158757001d0ecfb676109b72342c818ec59f
Merge reported by: Server Team CI bot
Merged at revision: not available
Proposed branch: ~xiaofengw/cloud-init:xiaofengw-add-custom-script-option
Merge into: cloud-init:master
Diff against target: 248 lines (+169/-10)
5 files modified
cloudinit/sources/DataSourceOVF.py (+20/-1)
cloudinit/sources/helpers/vmware/imc/guestcust_error.py (+1/-0)
cloudinit/sources/helpers/vmware/imc/guestcust_util.py (+37/-0)
tests/unittests/test_datasource/test_ovf.py (+46/-9)
tests/unittests/test_vmware/test_guestcust_util.py (+65/-0)
Reviewer Review Type Date Requested Status
Ryan Harper Approve
Server Team CI bot continuous-integration Approve
Review via email: mp+372469@code.launchpad.net

Commit message

VMWware: add option into VMTools config to enable/disable custom script.

VMWware customization already has support to run a custom script during
the VM customization. Adding this option allows a VM administrator to
disable the execution of customization scripts. If set the script
will not execute and the customization status is set to
GUESTCUST_ERROR_SCRIPT_DISABLED.

To post a comment you must log in.
c516af5... by Xiaofeng Wang

VMWware: add option into VMTools configuration to enable/disable custom script.

VMWware customization has already support to run a custom script during the VM
customization. We add this option so that VM administrator could prevent custom
 script to run, then the customization will fail and customization status is
set to GUESTCUST_ERROR_SCRIPT_DISABLED.

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

Thanks for resubmitting. This looks much better. A couple of inline comments below.

review: Needs Fixing
68f7158... by Xiaofeng Wang

VMWware: add option into VMTools configuration to enable/disable custom script.

Fix the comments from Ryan.

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

> Thanks for resubmitting. This looks much better. A couple of inline comments
> below.

Thanks, all comments are fixed, and also add a new test case to cover "key=Bar=Wark" case. Please review. Thanks.

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

No commit message was specified in the merge proposal. Click on the following link and set the commit message (if you want jenkins to rebuild you need to trigger it yourself):
https://code.launchpad.net/~xiaofengw/cloud-init/+git/cloud-init/+merge/372469/+edit-commit-message
https://jenkins.ubuntu.com/server/job/cloud-init-ci/1117/
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/1117//rebuild

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

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

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

I've updated the commit message. Everything looks good now.

Thanks!

review: Approve

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 dd941d2..b156189 100644
3--- a/cloudinit/sources/DataSourceOVF.py
4+++ b/cloudinit/sources/DataSourceOVF.py
5@@ -40,11 +40,15 @@ from cloudinit.sources.helpers.vmware.imc.guestcust_state \
6 from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
7 enable_nics,
8 get_nics_to_enable,
9- set_customization_status
10+ set_customization_status,
11+ get_tools_config
12 )
13
14 LOG = logging.getLogger(__name__)
15
16+CONFGROUPNAME_GUESTCUSTOMIZATION = "deployPkg"
17+GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS = "enable-custom-scripts"
18+
19
20 class DataSourceOVF(sources.DataSource):
21
22@@ -148,6 +152,21 @@ class DataSourceOVF(sources.DataSource):
23 product_marker, os.path.join(self.paths.cloud_dir, 'data'))
24 special_customization = product_marker and not hasmarkerfile
25 customscript = self._vmware_cust_conf.custom_script_name
26+ custScriptConfig = get_tools_config(
27+ CONFGROUPNAME_GUESTCUSTOMIZATION,
28+ GUESTCUSTOMIZATION_ENABLE_CUST_SCRIPTS,
29+ "true")
30+ if custScriptConfig.lower() == "false":
31+ # Update the customization status if there is a
32+ # custom script is disabled
33+ if special_customization and customscript:
34+ msg = "Custom script is disabled by VM Administrator"
35+ LOG.debug(msg)
36+ set_customization_status(
37+ GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
38+ GuestCustErrorEnum.GUESTCUST_ERROR_SCRIPT_DISABLED)
39+ raise RuntimeError(msg)
40+
41 ccScriptsDir = os.path.join(
42 self.paths.get_cpath("scripts"),
43 "per-instance")
44diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
45index db5a00d..65ae739 100644
46--- a/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
47+++ b/cloudinit/sources/helpers/vmware/imc/guestcust_error.py
48@@ -10,5 +10,6 @@ class GuestCustErrorEnum(object):
49 """Specifies different errors of Guest Customization engine"""
50
51 GUESTCUST_ERROR_SUCCESS = 0
52+ GUESTCUST_ERROR_SCRIPT_DISABLED = 6
53
54 # vi: ts=4 expandtab
55diff --git a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
56index a590f32..eb78172 100644
57--- a/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
58+++ b/cloudinit/sources/helpers/vmware/imc/guestcust_util.py
59@@ -7,6 +7,7 @@
60
61 import logging
62 import os
63+import re
64 import time
65
66 from cloudinit import util
67@@ -117,4 +118,40 @@ def enable_nics(nics):
68 logger.warning("Can't connect network interfaces after %d attempts",
69 enableNicsWaitRetries)
70
71+
72+def get_tools_config(section, key, defaultVal):
73+ """ Return the value of [section] key from VMTools configuration.
74+
75+ @param section: String of section to read from VMTools config
76+ @returns: String value from key in [section] or defaultVal if
77+ [section] is not present or vmware-toolbox-cmd is
78+ not installed.
79+ """
80+
81+ if not util.which('vmware-toolbox-cmd'):
82+ logger.debug(
83+ 'vmware-toolbox-cmd not installed, returning default value')
84+ return defaultVal
85+
86+ retValue = defaultVal
87+ cmd = ['vmware-toolbox-cmd', 'config', 'get', section, key]
88+
89+ try:
90+ (outText, _) = util.subp(cmd)
91+ m = re.match(r'([a-zA-Z0-9 ]+)=(.*)', outText)
92+ if m:
93+ retValue = m.group(2).strip()
94+ logger.debug("Get tools config: [%s] %s = %s",
95+ section, key, retValue)
96+ else:
97+ logger.debug(
98+ "Tools config: [%s] %s is not found, return default value: %s",
99+ section, key, retValue)
100+ except util.ProcessExecutionError as e:
101+ logger.error("Failed running %s[%s]", cmd, e.exit_code)
102+ logger.exception(e)
103+
104+ return retValue
105+
106+
107 # vi: ts=4 expandtab
108diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
109index 349d54c..a615470 100644
110--- a/tests/unittests/test_datasource/test_ovf.py
111+++ b/tests/unittests/test_datasource/test_ovf.py
112@@ -169,19 +169,56 @@ class TestDatasourceOVF(CiTestCase):
113 MARKER-ID = 12345345
114 """)
115 util.write_file(conf_file, conf_content)
116- with self.assertRaises(CustomScriptNotFound) as context:
117- wrap_and_call(
118- 'cloudinit.sources.DataSourceOVF',
119- {'util.read_dmi_data': 'vmware',
120- 'util.del_dir': True,
121- 'search_file': self.tdir,
122- 'wait_for_imc_cfg_file': conf_file,
123- 'get_nics_to_enable': ''},
124- ds.get_data)
125+ with mock.patch(MPATH + 'get_tools_config', return_value='true'):
126+ with self.assertRaises(CustomScriptNotFound) as context:
127+ wrap_and_call(
128+ 'cloudinit.sources.DataSourceOVF',
129+ {'util.read_dmi_data': 'vmware',
130+ 'util.del_dir': True,
131+ 'search_file': self.tdir,
132+ 'wait_for_imc_cfg_file': conf_file,
133+ 'get_nics_to_enable': ''},
134+ ds.get_data)
135 customscript = self.tmp_path('test-script', self.tdir)
136 self.assertIn('Script %s not found!!' % customscript,
137 str(context.exception))
138
139+ def test_get_data_cust_script_disabled(self):
140+ """If custom script is disabled by VMware tools configuration,
141+ raise a RuntimeError.
142+ """
143+ paths = Paths({'cloud_dir': self.tdir})
144+ ds = self.datasource(
145+ sys_cfg={'disable_vmware_customization': False}, distro={},
146+ paths=paths)
147+ # Prepare the conf file
148+ conf_file = self.tmp_path('test-cust', self.tdir)
149+ conf_content = dedent("""\
150+ [CUSTOM-SCRIPT]
151+ SCRIPT-NAME = test-script
152+ [MISC]
153+ MARKER-ID = 12345346
154+ """)
155+ util.write_file(conf_file, conf_content)
156+ # Prepare the custom sript
157+ customscript = self.tmp_path('test-script', self.tdir)
158+ util.write_file(customscript, "This is the post cust script")
159+
160+ with mock.patch(MPATH + 'get_tools_config', return_value='false'):
161+ with mock.patch(MPATH + 'set_customization_status',
162+ return_value=('msg', b'')):
163+ with self.assertRaises(RuntimeError) as context:
164+ wrap_and_call(
165+ 'cloudinit.sources.DataSourceOVF',
166+ {'util.read_dmi_data': 'vmware',
167+ 'util.del_dir': True,
168+ 'search_file': self.tdir,
169+ 'wait_for_imc_cfg_file': conf_file,
170+ 'get_nics_to_enable': ''},
171+ ds.get_data)
172+ self.assertIn('Custom script is disabled by VM Administrator',
173+ str(context.exception))
174+
175 def test_get_data_non_vmware_seed_platform_info(self):
176 """Platform info properly reports when on non-vmware platforms."""
177 paths = Paths({'cloud_dir': self.tdir, 'run_dir': self.tdir})
178diff --git a/tests/unittests/test_vmware/test_guestcust_util.py b/tests/unittests/test_vmware/test_guestcust_util.py
179new file mode 100644
180index 0000000..b8fa994
181--- /dev/null
182+++ b/tests/unittests/test_vmware/test_guestcust_util.py
183@@ -0,0 +1,65 @@
184+# Copyright (C) 2019 Canonical Ltd.
185+# Copyright (C) 2019 VMware INC.
186+#
187+# Author: Xiaofeng Wang <xiaofengw@vmware.com>
188+#
189+# This file is part of cloud-init. See LICENSE file for license information.
190+
191+from cloudinit import util
192+from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
193+ get_tools_config,
194+)
195+from cloudinit.tests.helpers import CiTestCase, mock
196+
197+
198+class TestGuestCustUtil(CiTestCase):
199+ def test_get_tools_config_not_installed(self):
200+ """
201+ This test is designed to verify the behavior if vmware-toolbox-cmd
202+ is not installed.
203+ """
204+ with mock.patch.object(util, 'which', return_value=None):
205+ self.assertEqual(
206+ get_tools_config('section', 'key', 'defaultVal'), 'defaultVal')
207+
208+ def test_get_tools_config_internal_exception(self):
209+ """
210+ This test is designed to verify the behavior if internal exception
211+ is raised.
212+ """
213+ with mock.patch.object(util, 'which', return_value='/dummy/path'):
214+ with mock.patch.object(util, 'subp',
215+ return_value=('key=value', b''),
216+ side_effect=util.ProcessExecutionError(
217+ "subp failed", exit_code=99)):
218+ # verify return value is 'defaultVal', not 'value'.
219+ self.assertEqual(
220+ get_tools_config('section', 'key', 'defaultVal'),
221+ 'defaultVal')
222+
223+ def test_get_tools_config_normal(self):
224+ """
225+ This test is designed to verify the value could be parsed from
226+ key = value of the given [section]
227+ """
228+ with mock.patch.object(util, 'which', return_value='/dummy/path'):
229+ # value is not blank
230+ with mock.patch.object(util, 'subp',
231+ return_value=('key = value ', b'')):
232+ self.assertEqual(
233+ get_tools_config('section', 'key', 'defaultVal'),
234+ 'value')
235+ # value is blank
236+ with mock.patch.object(util, 'subp',
237+ return_value=('key = ', b'')):
238+ self.assertEqual(
239+ get_tools_config('section', 'key', 'defaultVal'),
240+ '')
241+ # value contains =
242+ with mock.patch.object(util, 'subp',
243+ return_value=('key=Bar=Wark', b'')):
244+ self.assertEqual(
245+ get_tools_config('section', 'key', 'defaultVal'),
246+ 'Bar=Wark')
247+
248+# vi: ts=4 expandtab

Subscribers

People subscribed via source and target branches