Merge ~chad.smith/cloud-init:ubuntu/devel into cloud-init:ubuntu/devel

Proposed by Chad Smith
Status: Merged
Merged at revision: 5db31ce1e2b61cecb458d8c4984a8db4d474f4f5
Proposed branch: ~chad.smith/cloud-init:ubuntu/devel
Merge into: cloud-init:ubuntu/devel
Diff against target: 992 lines (+630/-78)
11 files modified
cloudinit/sources/DataSourceOVF.py (+88/-37)
cloudinit/sources/helpers/vmware/imc/config.py (+4/-0)
cloudinit/sources/helpers/vmware/imc/config_custom_script.py (+153/-0)
cloudinit/sources/helpers/vmware/imc/config_nic.py (+1/-1)
debian/changelog (+10/-0)
tests/unittests/test_datasource/test_ovf.py (+107/-4)
tests/unittests/test_ds_identify.py (+84/-1)
tests/unittests/test_vmware/__init__.py (+0/-0)
tests/unittests/test_vmware/test_custom_script.py (+99/-0)
tests/unittests/test_vmware_config_file.py (+7/-0)
tools/ds-identify (+77/-35)
Reviewer Review Type Date Requested Status
Server Team CI bot continuous-integration Approve
Scott Moser Pending
Review via email: mp+334996@code.launchpad.net

Description of the change

Upstream snapshot for update into bionic

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:5db31ce1e2b61cecb458d8c4984a8db4d474f4f5
https://jenkins.ubuntu.com/server/job/cloud-init-ci/604/
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/604/rebuild

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 6ac621f..6e62f98 100644
3--- a/cloudinit/sources/DataSourceOVF.py
4+++ b/cloudinit/sources/DataSourceOVF.py
5@@ -21,6 +21,8 @@ from cloudinit import util
6
7 from cloudinit.sources.helpers.vmware.imc.config \
8 import Config
9+from cloudinit.sources.helpers.vmware.imc.config_custom_script \
10+ import PreCustomScript, PostCustomScript
11 from cloudinit.sources.helpers.vmware.imc.config_file \
12 import ConfigFile
13 from cloudinit.sources.helpers.vmware.imc.config_nic \
14@@ -30,7 +32,7 @@ from cloudinit.sources.helpers.vmware.imc.config_passwd \
15 from cloudinit.sources.helpers.vmware.imc.guestcust_error \
16 import GuestCustErrorEnum
17 from cloudinit.sources.helpers.vmware.imc.guestcust_event \
18- import GuestCustEventEnum
19+ import GuestCustEventEnum as GuestCustEvent
20 from cloudinit.sources.helpers.vmware.imc.guestcust_state \
21 import GuestCustStateEnum
22 from cloudinit.sources.helpers.vmware.imc.guestcust_util import (
23@@ -127,17 +129,31 @@ class DataSourceOVF(sources.DataSource):
24 self._vmware_cust_conf = Config(cf)
25 (md, ud, cfg) = read_vmware_imc(self._vmware_cust_conf)
26 self._vmware_nics_to_enable = get_nics_to_enable(nicspath)
27- markerid = self._vmware_cust_conf.marker_id
28- markerexists = check_marker_exists(markerid)
29+ imcdirpath = os.path.dirname(vmwareImcConfigFilePath)
30+ product_marker = self._vmware_cust_conf.marker_id
31+ hasmarkerfile = check_marker_exists(
32+ product_marker, os.path.join(self.paths.cloud_dir, 'data'))
33+ special_customization = product_marker and not hasmarkerfile
34+ customscript = self._vmware_cust_conf.custom_script_name
35 except Exception as e:
36- LOG.debug("Error parsing the customization Config File")
37- LOG.exception(e)
38- set_customization_status(
39- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
40- GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
41- raise e
42- finally:
43- util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
44+ _raise_error_status(
45+ "Error parsing the customization Config File",
46+ e,
47+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
48+ vmwareImcConfigFilePath)
49+
50+ if special_customization:
51+ if customscript:
52+ try:
53+ precust = PreCustomScript(customscript, imcdirpath)
54+ precust.execute()
55+ except Exception as e:
56+ _raise_error_status(
57+ "Error executing pre-customization script",
58+ e,
59+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
60+ vmwareImcConfigFilePath)
61+
62 try:
63 LOG.debug("Preparing the Network configuration")
64 self._network_config = get_network_config_from_conf(
65@@ -146,13 +162,13 @@ class DataSourceOVF(sources.DataSource):
66 True,
67 self.distro.osfamily)
68 except Exception as e:
69- LOG.exception(e)
70- set_customization_status(
71- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
72- GuestCustEventEnum.GUESTCUST_EVENT_NETWORK_SETUP_FAILED)
73- raise e
74+ _raise_error_status(
75+ "Error preparing Network Configuration",
76+ e,
77+ GuestCustEvent.GUESTCUST_EVENT_NETWORK_SETUP_FAILED,
78+ vmwareImcConfigFilePath)
79
80- if markerid and not markerexists:
81+ if special_customization:
82 LOG.debug("Applying password customization")
83 pwdConfigurator = PasswordConfigurator()
84 adminpwd = self._vmware_cust_conf.admin_password
85@@ -164,27 +180,41 @@ class DataSourceOVF(sources.DataSource):
86 else:
87 LOG.debug("Changing password is not needed")
88 except Exception as e:
89- LOG.debug("Error applying Password Configuration: %s", e)
90- set_customization_status(
91- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
92- GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
93- return False
94- if markerid:
95- LOG.debug("Handle marker creation")
96+ _raise_error_status(
97+ "Error applying Password Configuration",
98+ e,
99+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
100+ vmwareImcConfigFilePath)
101+
102+ if customscript:
103+ try:
104+ postcust = PostCustomScript(customscript, imcdirpath)
105+ postcust.execute()
106+ except Exception as e:
107+ _raise_error_status(
108+ "Error executing post-customization script",
109+ e,
110+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
111+ vmwareImcConfigFilePath)
112+
113+ if product_marker:
114 try:
115- setup_marker_files(markerid)
116+ setup_marker_files(
117+ product_marker,
118+ os.path.join(self.paths.cloud_dir, 'data'))
119 except Exception as e:
120- LOG.debug("Error creating marker files: %s", e)
121- set_customization_status(
122- GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
123- GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
124- return False
125+ _raise_error_status(
126+ "Error creating marker files",
127+ e,
128+ GuestCustEvent.GUESTCUST_EVENT_CUSTOMIZE_FAILED,
129+ vmwareImcConfigFilePath)
130
131 self._vmware_cust_found = True
132 found.append('vmware-tools')
133
134 # TODO: Need to set the status to DONE only when the
135 # customization is done successfully.
136+ util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
137 enable_nics(self._vmware_nics_to_enable)
138 set_customization_status(
139 GuestCustStateEnum.GUESTCUST_STATE_DONE,
140@@ -539,31 +569,52 @@ def get_datasource_list(depends):
141
142
143 # To check if marker file exists
144-def check_marker_exists(markerid):
145+def check_marker_exists(markerid, marker_dir):
146 """
147 Check the existence of a marker file.
148 Presence of marker file determines whether a certain code path is to be
149 executed. It is needed for partial guest customization in VMware.
150+ @param markerid: is an unique string representing a particular product
151+ marker.
152+ @param: marker_dir: The directory in which markers exist.
153 """
154 if not markerid:
155 return False
156- markerfile = "/.markerfile-" + markerid
157+ markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt")
158 if os.path.exists(markerfile):
159 return True
160 return False
161
162
163 # Create a marker file
164-def setup_marker_files(markerid):
165+def setup_marker_files(markerid, marker_dir):
166 """
167 Create a new marker file.
168 Marker files are unique to a full customization workflow in VMware
169 environment.
170+ @param markerid: is an unique string representing a particular product
171+ marker.
172+ @param: marker_dir: The directory in which markers exist.
173+
174 """
175- if not markerid:
176- return
177- markerfile = "/.markerfile-" + markerid
178- util.del_file("/.markerfile-*.txt")
179+ LOG.debug("Handle marker creation")
180+ markerfile = os.path.join(marker_dir, ".markerfile-" + markerid + ".txt")
181+ for fname in os.listdir(marker_dir):
182+ if fname.startswith(".markerfile"):
183+ util.del_file(os.path.join(marker_dir, fname))
184 open(markerfile, 'w').close()
185
186+
187+def _raise_error_status(prefix, error, event, config_file):
188+ """
189+ Raise error and send customization status to the underlying VMware
190+ Virtualization Platform. Also, cleanup the imc directory.
191+ """
192+ LOG.debug('%s: %s', prefix, error)
193+ set_customization_status(
194+ GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
195+ event)
196+ util.del_dir(os.path.dirname(config_file))
197+ raise error
198+
199 # vi: ts=4 expandtab
200diff --git a/cloudinit/sources/helpers/vmware/imc/config.py b/cloudinit/sources/helpers/vmware/imc/config.py
201index 49d441d..2eaeff3 100644
202--- a/cloudinit/sources/helpers/vmware/imc/config.py
203+++ b/cloudinit/sources/helpers/vmware/imc/config.py
204@@ -100,4 +100,8 @@ class Config(object):
205 """Returns marker id."""
206 return self._configFile.get(Config.MARKERID, None)
207
208+ @property
209+ def custom_script_name(self):
210+ """Return the name of custom (pre/post) script."""
211+ return self._configFile.get(Config.CUSTOM_SCRIPT, None)
212 # vi: ts=4 expandtab
213diff --git a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
214new file mode 100644
215index 0000000..a7d4ad9
216--- /dev/null
217+++ b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
218@@ -0,0 +1,153 @@
219+# Copyright (C) 2017 Canonical Ltd.
220+# Copyright (C) 2017 VMware Inc.
221+#
222+# Author: Maitreyee Saikia <msaikia@vmware.com>
223+#
224+# This file is part of cloud-init. See LICENSE file for license information.
225+
226+import logging
227+import os
228+import stat
229+from textwrap import dedent
230+
231+from cloudinit import util
232+
233+LOG = logging.getLogger(__name__)
234+
235+
236+class CustomScriptNotFound(Exception):
237+ pass
238+
239+
240+class CustomScriptConstant(object):
241+ RC_LOCAL = "/etc/rc.local"
242+ POST_CUST_TMP_DIR = "/root/.customization"
243+ POST_CUST_RUN_SCRIPT_NAME = "post-customize-guest.sh"
244+ POST_CUST_RUN_SCRIPT = os.path.join(POST_CUST_TMP_DIR,
245+ POST_CUST_RUN_SCRIPT_NAME)
246+ POST_REBOOT_PENDING_MARKER = "/.guest-customization-post-reboot-pending"
247+
248+
249+class RunCustomScript(object):
250+ def __init__(self, scriptname, directory):
251+ self.scriptname = scriptname
252+ self.directory = directory
253+ self.scriptpath = os.path.join(directory, scriptname)
254+
255+ def prepare_script(self):
256+ if not os.path.exists(self.scriptpath):
257+ raise CustomScriptNotFound("Script %s not found!! "
258+ "Cannot execute custom script!"
259+ % self.scriptpath)
260+ # Strip any CR characters from the decoded script
261+ util.load_file(self.scriptpath).replace("\r", "")
262+ st = os.stat(self.scriptpath)
263+ os.chmod(self.scriptpath, st.st_mode | stat.S_IEXEC)
264+
265+
266+class PreCustomScript(RunCustomScript):
267+ def execute(self):
268+ """Executing custom script with precustomization argument."""
269+ LOG.debug("Executing pre-customization script")
270+ self.prepare_script()
271+ util.subp(["/bin/sh", self.scriptpath, "precustomization"])
272+
273+
274+class PostCustomScript(RunCustomScript):
275+ def __init__(self, scriptname, directory):
276+ super(PostCustomScript, self).__init__(scriptname, directory)
277+ # Determine when to run custom script. When postreboot is True,
278+ # the user uploaded script will run as part of rc.local after
279+ # the machine reboots. This is determined by presence of rclocal.
280+ # When postreboot is False, script will run as part of cloud-init.
281+ self.postreboot = False
282+
283+ def _install_post_reboot_agent(self, rclocal):
284+ """
285+ Install post-reboot agent for running custom script after reboot.
286+ As part of this process, we are editing the rclocal file to run a
287+ VMware script, which in turn is resposible for handling the user
288+ script.
289+ @param: path to rc local.
290+ """
291+ LOG.debug("Installing post-reboot customization from %s to %s",
292+ self.directory, rclocal)
293+ if not self.has_previous_agent(rclocal):
294+ LOG.info("Adding post-reboot customization agent to rc.local")
295+ new_content = dedent("""
296+ # Run post-reboot guest customization
297+ /bin/sh %s
298+ exit 0
299+ """) % CustomScriptConstant.POST_CUST_RUN_SCRIPT
300+ existing_rclocal = util.load_file(rclocal).replace('exit 0\n', '')
301+ st = os.stat(rclocal)
302+ # "x" flag should be set
303+ mode = st.st_mode | stat.S_IEXEC
304+ util.write_file(rclocal, existing_rclocal + new_content, mode)
305+
306+ else:
307+ # We don't need to update rclocal file everytime a customization
308+ # is requested. It just needs to be done for the first time.
309+ LOG.info("Post-reboot guest customization agent is already "
310+ "registered in rc.local")
311+ LOG.debug("Installing post-reboot customization agent finished: %s",
312+ self.postreboot)
313+
314+ def has_previous_agent(self, rclocal):
315+ searchstring = "# Run post-reboot guest customization"
316+ if searchstring in open(rclocal).read():
317+ return True
318+ return False
319+
320+ def find_rc_local(self):
321+ """
322+ Determine if rc local is present.
323+ """
324+ rclocal = ""
325+ if os.path.exists(CustomScriptConstant.RC_LOCAL):
326+ LOG.debug("rc.local detected.")
327+ # resolving in case of symlink
328+ rclocal = os.path.realpath(CustomScriptConstant.RC_LOCAL)
329+ LOG.debug("rc.local resolved to %s", rclocal)
330+ else:
331+ LOG.warning("Can't find rc.local, post-customization "
332+ "will be run before reboot")
333+ return rclocal
334+
335+ def install_agent(self):
336+ rclocal = self.find_rc_local()
337+ if rclocal:
338+ self._install_post_reboot_agent(rclocal)
339+ self.postreboot = True
340+
341+ def execute(self):
342+ """
343+ This method executes post-customization script before or after reboot
344+ based on the presence of rc local.
345+ """
346+ self.prepare_script()
347+ self.install_agent()
348+ if not self.postreboot:
349+ LOG.warning("Executing post-customization script inline")
350+ util.subp(["/bin/sh", self.scriptpath, "postcustomization"])
351+ else:
352+ LOG.debug("Scheduling custom script to run post reboot")
353+ if not os.path.isdir(CustomScriptConstant.POST_CUST_TMP_DIR):
354+ os.mkdir(CustomScriptConstant.POST_CUST_TMP_DIR)
355+ # Script "post-customize-guest.sh" and user uploaded script are
356+ # are present in the same directory and needs to copied to a temp
357+ # directory to be executed post reboot. User uploaded script is
358+ # saved as customize.sh in the temp directory.
359+ # post-customize-guest.sh excutes customize.sh after reboot.
360+ LOG.debug("Copying post-customization script")
361+ util.copy(self.scriptpath,
362+ CustomScriptConstant.POST_CUST_TMP_DIR + "/customize.sh")
363+ LOG.debug("Copying script to run post-customization script")
364+ util.copy(
365+ os.path.join(self.directory,
366+ CustomScriptConstant.POST_CUST_RUN_SCRIPT_NAME),
367+ CustomScriptConstant.POST_CUST_RUN_SCRIPT)
368+ LOG.info("Creating post-reboot pending marker")
369+ util.ensure_file(CustomScriptConstant.POST_REBOOT_PENDING_MARKER)
370+
371+# vi: ts=4 expandtab
372diff --git a/cloudinit/sources/helpers/vmware/imc/config_nic.py b/cloudinit/sources/helpers/vmware/imc/config_nic.py
373index 2fb07c5..2d8900e 100644
374--- a/cloudinit/sources/helpers/vmware/imc/config_nic.py
375+++ b/cloudinit/sources/helpers/vmware/imc/config_nic.py
376@@ -161,7 +161,7 @@ class NicConfigurator(object):
377 if nic.primary and v4.gateways:
378 self.ipv4PrimaryGateway = v4.gateways[0]
379 subnet.update({'gateway': self.ipv4PrimaryGateway})
380- return [subnet]
381+ return ([subnet], route_list)
382
383 # Add routes if there is no primary nic
384 if not self._primaryNic:
385diff --git a/debian/changelog b/debian/changelog
386index b13bdea..bbc29e5 100644
387--- a/debian/changelog
388+++ b/debian/changelog
389@@ -1,3 +1,13 @@
390+cloud-init (17.1-53-ga5dc0f42-0ubuntu1) bionic; urgency=medium
391+
392+ * New upstream snapshot.
393+ - OVF: improve ds-identify to support finding OVF iso transport.
394+ (LP: #1731868)
395+ - VMware: Support for user provided pre and post-customization scripts
396+ [Maitreyee Saikia]
397+
398+ -- Chad Smith <chad.smith@canonical.com> Fri, 08 Dec 2017 14:46:36 -0700
399+
400 cloud-init (17.1-51-g05b2308a-0ubuntu1) bionic; urgency=medium
401
402 * New upstream snapshot.
403diff --git a/tests/unittests/test_datasource/test_ovf.py b/tests/unittests/test_datasource/test_ovf.py
404index 700da86..fc4eb36 100644
405--- a/tests/unittests/test_datasource/test_ovf.py
406+++ b/tests/unittests/test_datasource/test_ovf.py
407@@ -5,11 +5,17 @@
408 # This file is part of cloud-init. See LICENSE file for license information.
409
410 import base64
411-from collections import OrderedDict
412+import os
413
414-from cloudinit.tests import helpers as test_helpers
415+from collections import OrderedDict
416+from textwrap import dedent
417
418+from cloudinit import util
419+from cloudinit.tests.helpers import CiTestCase, wrap_and_call
420+from cloudinit.helpers import Paths
421 from cloudinit.sources import DataSourceOVF as dsovf
422+from cloudinit.sources.helpers.vmware.imc.config_custom_script import (
423+ CustomScriptNotFound)
424
425 OVF_ENV_CONTENT = """<?xml version="1.0" encoding="UTF-8"?>
426 <Environment xmlns="http://schemas.dmtf.org/ovf/environment/1"
427@@ -42,7 +48,7 @@ def fill_properties(props, template=OVF_ENV_CONTENT):
428 return template.format(properties=properties)
429
430
431-class TestReadOvfEnv(test_helpers.TestCase):
432+class TestReadOvfEnv(CiTestCase):
433 def test_with_b64_userdata(self):
434 user_data = "#!/bin/sh\necho hello world\n"
435 user_data_b64 = base64.b64encode(user_data.encode()).decode()
436@@ -72,7 +78,104 @@ class TestReadOvfEnv(test_helpers.TestCase):
437 self.assertIsNone(ud)
438
439
440-class TestTransportIso9660(test_helpers.CiTestCase):
441+class TestMarkerFiles(CiTestCase):
442+
443+ def setUp(self):
444+ super(TestMarkerFiles, self).setUp()
445+ self.tdir = self.tmp_dir()
446+
447+ def test_false_when_markerid_none(self):
448+ """Return False when markerid provided is None."""
449+ self.assertFalse(
450+ dsovf.check_marker_exists(markerid=None, marker_dir=self.tdir))
451+
452+ def test_markerid_file_exist(self):
453+ """Return False when markerid file path does not exist,
454+ True otherwise."""
455+ self.assertFalse(
456+ dsovf.check_marker_exists('123', self.tdir))
457+
458+ marker_file = self.tmp_path('.markerfile-123.txt', self.tdir)
459+ util.write_file(marker_file, '')
460+ self.assertTrue(
461+ dsovf.check_marker_exists('123', self.tdir)
462+ )
463+
464+ def test_marker_file_setup(self):
465+ """Test creation of marker files."""
466+ markerfilepath = self.tmp_path('.markerfile-hi.txt', self.tdir)
467+ self.assertFalse(os.path.exists(markerfilepath))
468+ dsovf.setup_marker_files(markerid='hi', marker_dir=self.tdir)
469+ self.assertTrue(os.path.exists(markerfilepath))
470+
471+
472+class TestDatasourceOVF(CiTestCase):
473+
474+ with_logs = True
475+
476+ def setUp(self):
477+ super(TestDatasourceOVF, self).setUp()
478+ self.datasource = dsovf.DataSourceOVF
479+ self.tdir = self.tmp_dir()
480+
481+ def test_get_data_false_on_none_dmi_data(self):
482+ """When dmi for system-product-name is None, get_data returns False."""
483+ paths = Paths({'seed_dir': self.tdir})
484+ ds = self.datasource(sys_cfg={}, distro={}, paths=paths)
485+ retcode = wrap_and_call(
486+ 'cloudinit.sources.DataSourceOVF',
487+ {'util.read_dmi_data': None},
488+ ds.get_data)
489+ self.assertFalse(retcode, 'Expected False return from ds.get_data')
490+ self.assertIn(
491+ 'DEBUG: No system-product-name found', self.logs.getvalue())
492+
493+ def test_get_data_no_vmware_customization_disabled(self):
494+ """When vmware customization is disabled via sys_cfg log a message."""
495+ paths = Paths({'seed_dir': self.tdir})
496+ ds = self.datasource(
497+ sys_cfg={'disable_vmware_customization': True}, distro={},
498+ paths=paths)
499+ retcode = wrap_and_call(
500+ 'cloudinit.sources.DataSourceOVF',
501+ {'util.read_dmi_data': 'vmware'},
502+ ds.get_data)
503+ self.assertFalse(retcode, 'Expected False return from ds.get_data')
504+ self.assertIn(
505+ 'DEBUG: Customization for VMware platform is disabled.',
506+ self.logs.getvalue())
507+
508+ def test_get_data_vmware_customization_disabled(self):
509+ """When cloud-init workflow for vmware is enabled via sys_cfg log a
510+ message.
511+ """
512+ paths = Paths({'seed_dir': self.tdir})
513+ ds = self.datasource(
514+ sys_cfg={'disable_vmware_customization': False}, distro={},
515+ paths=paths)
516+ conf_file = self.tmp_path('test-cust', self.tdir)
517+ conf_content = dedent("""\
518+ [CUSTOM-SCRIPT]
519+ SCRIPT-NAME = test-script
520+ [MISC]
521+ MARKER-ID = 12345345
522+ """)
523+ util.write_file(conf_file, conf_content)
524+ with self.assertRaises(CustomScriptNotFound) as context:
525+ wrap_and_call(
526+ 'cloudinit.sources.DataSourceOVF',
527+ {'util.read_dmi_data': 'vmware',
528+ 'util.del_dir': True,
529+ 'search_file': self.tdir,
530+ 'wait_for_imc_cfg_file': conf_file,
531+ 'get_nics_to_enable': ''},
532+ ds.get_data)
533+ customscript = self.tmp_path('test-script', self.tdir)
534+ self.assertIn('Script %s not found!!' % customscript,
535+ str(context.exception))
536+
537+
538+class TestTransportIso9660(CiTestCase):
539
540 def setUp(self):
541 super(TestTransportIso9660, self).setUp()
542diff --git a/tests/unittests/test_ds_identify.py b/tests/unittests/test_ds_identify.py
543index 7a920d4..3f1a671 100644
544--- a/tests/unittests/test_ds_identify.py
545+++ b/tests/unittests/test_ds_identify.py
546@@ -32,6 +32,7 @@ POLICY_FOUND_OR_MAYBE = "search,found=all,maybe=all,notfound=disabled"
547 DI_DEFAULT_POLICY = "search,found=all,maybe=all,notfound=enabled"
548 DI_DEFAULT_POLICY_NO_DMI = "search,found=all,maybe=all,notfound=disabled"
549 DI_EC2_STRICT_ID_DEFAULT = "true"
550+OVF_MATCH_STRING = 'http://schemas.dmtf.org/ovf/environment/1'
551
552 SHELL_MOCK_TMPL = """\
553 %(name)s() {
554@@ -55,6 +56,7 @@ P_SEED_DIR = "var/lib/cloud/seed"
555 P_DSID_CFG = "etc/cloud/ds-identify.cfg"
556
557 MOCK_VIRT_IS_KVM = {'name': 'detect_virt', 'RET': 'kvm', 'ret': 0}
558+MOCK_VIRT_IS_VMWARE = {'name': 'detect_virt', 'RET': 'vmware', 'ret': 0}
559 MOCK_UNAME_IS_PPC64 = {'name': 'uname', 'out': UNAME_PPC64EL, 'ret': 0}
560
561
562@@ -296,6 +298,48 @@ class TestDsIdentify(CiTestCase):
563 data, RC_FOUND, dslist=['OpenStack', 'None'])
564 self.assertIn("check for 'OpenStack' returned maybe", err)
565
566+ def test_default_ovf_is_found(self):
567+ """OVF is identified found when ovf/ovf-env.xml seed file exists."""
568+ self._test_ds_found('OVF-seed')
569+
570+ def test_default_ovf_with_detect_virt_none_not_found(self):
571+ """OVF identifies not found when detect_virt returns "none"."""
572+ self._check_via_dict(
573+ {'ds': 'OVF'}, rc=RC_NOT_FOUND, policy_dmi="disabled")
574+
575+ def test_default_ovf_returns_not_found_on_azure(self):
576+ """OVF datasource won't be found as false positive on Azure."""
577+ ovfonazure = copy.deepcopy(VALID_CFG['OVF'])
578+ # Set azure asset tag to assert OVF content not found
579+ ovfonazure['files'][P_CHASSIS_ASSET_TAG] = (
580+ '7783-7084-3265-9085-8269-3286-77\n')
581+ self._check_via_dict(
582+ ovfonazure, RC_FOUND, dslist=['Azure', DS_NONE])
583+
584+ def test_ovf_on_vmware_iso_found_by_cdrom_with_ovf_schema_match(self):
585+ """OVF is identified when iso9660 cdrom path contains ovf schema."""
586+ self._test_ds_found('OVF')
587+
588+ def test_ovf_on_vmware_iso_found_when_vmware_customization(self):
589+ """OVF is identified when vmware customization is enabled."""
590+ self._test_ds_found('OVF-vmware-customization')
591+
592+ def test_ovf_on_vmware_iso_found_by_cdrom_with_matching_fs_label(self):
593+ """OVF is identified when iso9660 cdrom label has ovf-transport."""
594+ ovf_cdrom_by_label = copy.deepcopy(VALID_CFG['OVF'])
595+ # Unset matching cdrom ovf schema content
596+ ovf_cdrom_by_label['files']['dev/sr0'] = 'No content match'
597+ self._check_via_dict(
598+ ovf_cdrom_by_label, rc=RC_NOT_FOUND, policy_dmi="disabled")
599+
600+ # Add recognized labels
601+ for valid_fs_label in ['ovf-transport', 'OVF-TRANSPORT']:
602+ ovf_cdrom_by_label['mocks'][0]['out'] = blkid_out([
603+ {'DEVNAME': 'sr0', 'TYPE': 'iso9660',
604+ 'LABEL': valid_fs_label}])
605+ self._check_via_dict(
606+ ovf_cdrom_by_label, rc=RC_FOUND, dslist=['OVF', DS_NONE])
607+
608
609 def blkid_out(disks=None):
610 """Convert a list of disk dictionaries into blkid content."""
611@@ -305,7 +349,9 @@ def blkid_out(disks=None):
612 for disk in disks:
613 if not disk["DEVNAME"].startswith("/dev/"):
614 disk["DEVNAME"] = "/dev/" + disk["DEVNAME"]
615- for key in disk:
616+ # devname needs to be first.
617+ lines.append("%s=%s" % ("DEVNAME", disk["DEVNAME"]))
618+ for key in [d for d in disk if d != "DEVNAME"]:
619 lines.append("%s=%s" % (key, disk[key]))
620 lines.append("")
621 return '\n'.join(lines)
622@@ -383,6 +429,43 @@ VALID_CFG = {
623 'policy_dmi': POLICY_FOUND_ONLY,
624 'policy_no_dmi': POLICY_FOUND_ONLY,
625 },
626+ 'OVF-seed': {
627+ 'ds': 'OVF',
628+ 'files': {
629+ os.path.join(P_SEED_DIR, 'ovf', 'ovf-env.xml'): 'present\n',
630+ }
631+ },
632+ 'OVF-vmware-customization': {
633+ 'ds': 'OVF',
634+ 'mocks': [
635+ # Include a mockes iso9660 potential, even though content not ovf
636+ {'name': 'blkid', 'ret': 0,
637+ 'out': blkid_out(
638+ [{'DEVNAME': 'sr0', 'TYPE': 'iso9660', 'LABEL': ''}])
639+ },
640+ MOCK_VIRT_IS_VMWARE,
641+ ],
642+ 'files': {
643+ 'dev/sr0': 'no match',
644+ # Setup vmware customization enabled
645+ 'usr/lib/vmware-tools/plugins/vmsvc/libdeployPkgPlugin.so': 'here',
646+ 'etc/cloud/cloud.cfg': 'disable_vmware_customization: false\n',
647+ }
648+ },
649+ 'OVF': {
650+ 'ds': 'OVF',
651+ 'mocks': [
652+ {'name': 'blkid', 'ret': 0,
653+ 'out': blkid_out(
654+ [{'DEVNAME': 'vda1', 'TYPE': 'vfat', 'PARTUUID': uuid4()},
655+ {'DEVNAME': 'sr0', 'TYPE': 'iso9660', 'LABEL': ''}])
656+ },
657+ MOCK_VIRT_IS_VMWARE,
658+ ],
659+ 'files': {
660+ 'dev/sr0': 'pretend ovf iso has ' + OVF_MATCH_STRING + '\n',
661+ }
662+ },
663 'ConfigDrive': {
664 'ds': 'ConfigDrive',
665 'mocks': [
666diff --git a/tests/unittests/test_vmware/__init__.py b/tests/unittests/test_vmware/__init__.py
667new file mode 100644
668index 0000000..e69de29
669--- /dev/null
670+++ b/tests/unittests/test_vmware/__init__.py
671diff --git a/tests/unittests/test_vmware/test_custom_script.py b/tests/unittests/test_vmware/test_custom_script.py
672new file mode 100644
673index 0000000..2d9519b
674--- /dev/null
675+++ b/tests/unittests/test_vmware/test_custom_script.py
676@@ -0,0 +1,99 @@
677+# Copyright (C) 2015 Canonical Ltd.
678+# Copyright (C) 2017 VMware INC.
679+#
680+# Author: Maitreyee Saikia <msaikia@vmware.com>
681+#
682+# This file is part of cloud-init. See LICENSE file for license information.
683+
684+from cloudinit import util
685+from cloudinit.sources.helpers.vmware.imc.config_custom_script import (
686+ CustomScriptConstant,
687+ CustomScriptNotFound,
688+ PreCustomScript,
689+ PostCustomScript,
690+)
691+from cloudinit.tests.helpers import CiTestCase, mock
692+
693+
694+class TestVmwareCustomScript(CiTestCase):
695+ def setUp(self):
696+ self.tmpDir = self.tmp_dir()
697+
698+ def test_prepare_custom_script(self):
699+ """
700+ This test is designed to verify the behavior based on the presence of
701+ custom script. Mainly needed for scenario where a custom script is
702+ expected, but was not properly copied. "CustomScriptNotFound" exception
703+ is raised in such cases.
704+ """
705+ # Custom script does not exist.
706+ preCust = PreCustomScript("random-vmw-test", self.tmpDir)
707+ self.assertEqual("random-vmw-test", preCust.scriptname)
708+ self.assertEqual(self.tmpDir, preCust.directory)
709+ self.assertEqual(self.tmp_path("random-vmw-test", self.tmpDir),
710+ preCust.scriptpath)
711+ with self.assertRaises(CustomScriptNotFound):
712+ preCust.prepare_script()
713+
714+ # Custom script exists.
715+ custScript = self.tmp_path("test-cust", self.tmpDir)
716+ util.write_file(custScript, "test-CR-strip/r/r")
717+ postCust = PostCustomScript("test-cust", self.tmpDir)
718+ self.assertEqual("test-cust", postCust.scriptname)
719+ self.assertEqual(self.tmpDir, postCust.directory)
720+ self.assertEqual(custScript, postCust.scriptpath)
721+ self.assertFalse(postCust.postreboot)
722+ postCust.prepare_script()
723+ # Check if all carraige returns are stripped from script.
724+ self.assertFalse("/r" in custScript)
725+
726+ def test_rc_local_exists(self):
727+ """
728+ This test is designed to verify the different scenarios associated
729+ with the presence of rclocal.
730+ """
731+ # test when rc local does not exist
732+ postCust = PostCustomScript("test-cust", self.tmpDir)
733+ with mock.patch.object(CustomScriptConstant, "RC_LOCAL", "/no/path"):
734+ rclocal = postCust.find_rc_local()
735+ self.assertEqual("", rclocal)
736+
737+ # test when rc local exists
738+ rclocalFile = self.tmp_path("vmware-rclocal", self.tmpDir)
739+ util.write_file(rclocalFile, "# Run post-reboot guest customization",
740+ omode="w")
741+ with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocalFile):
742+ rclocal = postCust.find_rc_local()
743+ self.assertEqual(rclocalFile, rclocal)
744+ self.assertTrue(postCust.has_previous_agent, rclocal)
745+
746+ # test when rc local is a symlink
747+ rclocalLink = self.tmp_path("dummy-rclocal-link", self.tmpDir)
748+ util.sym_link(rclocalFile, rclocalLink, True)
749+ with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocalLink):
750+ rclocal = postCust.find_rc_local()
751+ self.assertEqual(rclocalFile, rclocal)
752+
753+ def test_execute_post_cust(self):
754+ """
755+ This test is to identify if rclocal was properly populated to be
756+ run after reboot.
757+ """
758+ customscript = self.tmp_path("vmware-post-cust-script", self.tmpDir)
759+ rclocal = self.tmp_path("vmware-rclocal", self.tmpDir)
760+ # Create a temporary rclocal file
761+ open(customscript, "w")
762+ util.write_file(rclocal, "tests\nexit 0", omode="w")
763+ postCust = PostCustomScript("vmware-post-cust-script", self.tmpDir)
764+ with mock.patch.object(CustomScriptConstant, "RC_LOCAL", rclocal):
765+ # Test that guest customization agent is not installed initially.
766+ self.assertFalse(postCust.postreboot)
767+ self.assertIs(postCust.has_previous_agent(rclocal), False)
768+ postCust.install_agent()
769+
770+ # Assert rclocal has been modified to have guest customization
771+ # agent.
772+ self.assertTrue(postCust.postreboot)
773+ self.assertTrue(postCust.has_previous_agent, rclocal)
774+
775+# vi: ts=4 expandtab
776diff --git a/tests/unittests/test_vmware_config_file.py b/tests/unittests/test_vmware_config_file.py
777index 0f8cda9..036f687 100644
778--- a/tests/unittests/test_vmware_config_file.py
779+++ b/tests/unittests/test_vmware_config_file.py
780@@ -335,5 +335,12 @@ class TestVmwareConfigFile(CiTestCase):
781 self.assertEqual('255.255.0.0', subnet.get('netmask'),
782 'Subnet netmask')
783
784+ def test_custom_script(self):
785+ cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg")
786+ conf = Config(cf)
787+ self.assertIsNone(conf.custom_script_name)
788+ cf._insertKey("CUSTOM-SCRIPT|SCRIPT-NAME", "test-script")
789+ conf = Config(cf)
790+ self.assertEqual("test-script", conf.custom_script_name)
791
792 # vi: ts=4 expandtab
793diff --git a/tools/ds-identify b/tools/ds-identify
794index ee5e05a..4c59d7b 100755
795--- a/tools/ds-identify
796+++ b/tools/ds-identify
797@@ -83,6 +83,7 @@ _DI_LOGGED=""
798 # set DI_MAIN='noop' in environment to source this file with no main called.
799 DI_MAIN=${DI_MAIN:-main}
800
801+DI_BLKID_OUTPUT=""
802 DI_DEFAULT_POLICY="search,found=all,maybe=all,notfound=${DI_DISABLED}"
803 DI_DEFAULT_POLICY_NO_DMI="search,found=all,maybe=all,notfound=${DI_ENABLED}"
804 DI_DMI_CHASSIS_ASSET_TAG=""
805@@ -91,6 +92,7 @@ DI_DMI_SYS_VENDOR=""
806 DI_DMI_PRODUCT_SERIAL=""
807 DI_DMI_PRODUCT_UUID=""
808 DI_FS_LABELS=""
809+DI_ISO9660_DEVS=""
810 DI_KERNEL_CMDLINE=""
811 DI_VIRT=""
812 DI_PID_1_PRODUCT_NAME=""
813@@ -181,32 +183,43 @@ block_dev_with_label() {
814 return 0
815 }
816
817-read_fs_labels() {
818- cached "${DI_FS_LABELS}" && return 0
819+read_fs_info() {
820+ cached "${DI_BLKID_OUTPUT}" && return 0
821 # do not rely on links in /dev/disk which might not be present yet.
822 # note that older blkid versions do not report DEVNAME in 'export' output.
823- local out="" ret=0 oifs="$IFS" line="" delim=","
824- local labels=""
825 if is_container; then
826 # blkid will in a container, or at least currently in lxd
827 # not provide useful information.
828 DI_FS_LABELS="$UNAVAILABLE:container"
829- else
830- out=$(blkid -c /dev/null -o export) || {
831- ret=$?
832- error "failed running [$ret]: blkid -c /dev/null -o export"
833- return $ret
834- }
835- IFS="$CR"
836- set -- $out
837- IFS="$oifs"
838- for line in "$@"; do
839- case "${line}" in
840- LABEL=*) labels="${labels}${line#LABEL=}${delim}";;
841- esac
842- done
843- DI_FS_LABELS="${labels%${delim}}"
844+ DI_ISO9660_DEVS="$UNAVAILABLE:container"
845+ return
846 fi
847+ local oifs="$IFS" line="" delim=","
848+ local ret=0 out="" labels="" dev="" label="" ftype="" isodevs=""
849+ out=$(blkid -c /dev/null -o export) || {
850+ ret=$?
851+ error "failed running [$ret]: blkid -c /dev/null -o export"
852+ DI_FS_LABELS="$UNAVAILABLE:error"
853+ DI_ISO9660_DEVS="$UNAVAILABLE:error"
854+ return $ret
855+ }
856+ IFS="$CR"
857+ set -- $out
858+ IFS="$oifs"
859+ for line in "$@" ""; do
860+ case "${line}" in
861+ DEVNAME=*) dev=${line#DEVNAME=};;
862+ LABEL=*) label="${line#LABEL=}";
863+ labels="${labels}${line#LABEL=}${delim}";;
864+ TYPE=*) ftype=${line#TYPE=};;
865+ "") if [ "$ftype" = "iso9660" ]; then
866+ isodevs="${isodevs} ${dev}=$label"
867+ fi
868+ ftype=""; devname=""; label="";
869+ esac
870+ done
871+ DI_FS_LABELS="${labels%${delim}}"
872+ DI_ISO9660_DEVS="${isodevs# }"
873 }
874
875 cached() {
876@@ -214,10 +227,6 @@ cached() {
877 }
878
879
880-has_cdrom() {
881- [ -e "${PATH_ROOT}/dev/cdrom" ]
882-}
883-
884 detect_virt() {
885 local virt="${UNAVAILABLE}" r="" out=""
886 if [ -d /run/systemd ]; then
887@@ -621,14 +630,13 @@ ovf_vmware_guest_customization() {
888 [ "${DI_VIRT}" = "vmware" ] || return 1
889
890 # we have to have the plugin to do vmware customization
891- local found="" pkg="" pre="/usr/lib"
892+ local found="" pkg="" pre="${PATH_ROOT}/usr/lib"
893 for pkg in vmware-tools open-vm-tools; do
894 if [ -f "$pre/$pkg/plugins/vmsvc/libdeployPkgPlugin.so" ]; then
895 found="$pkg"; break;
896 fi
897 done
898 [ -n "$found" ] || return 1
899-
900 # vmware customization is disabled by default
901 # (disable_vmware_customization=true). If it is set to false, then
902 # user has requested customization.
903@@ -644,20 +652,55 @@ ovf_vmware_guest_customization() {
904 return 1
905 }
906
907+is_cdrom_ovf() {
908+ local dev="$1" label="$2"
909+ # skip devices that don't look like cdrom paths.
910+ case "$dev" in
911+ /dev/sr[0-9]|/dev/hd[a-z]) :;;
912+ *) debug 1 "skipping iso dev $d"
913+ return 1;;
914+ esac
915+
916+ # fast path known 'OVF' labels
917+ [ "$label" = "OVF-TRANSPORT" -o "$label" = "ovf-transport" ] && return 0
918+
919+ # explicitly skip known labels of other types. rd_rdfe is azure.
920+ case "$label" in
921+ config-2|rd_rdfe_stable*) return 1;;
922+ esac
923+
924+ local idstr="http://schemas.dmtf.org/ovf/environment/1"
925+ grep --quiet --ignore-case "$idstr" "${PATH_ROOT}$dev"
926+}
927+
928 dscheck_OVF() {
929- local p=""
930 check_seed_dir ovf ovf-env.xml && return "${DS_FOUND}"
931
932+ [ "${DI_VIRT}" = "none" ] && return ${DS_NOT_FOUND}
933+
934+ # Azure provides ovf. Skip false positive by dis-allowing.
935+ is_azure_chassis && return $DS_NOT_FOUND
936+
937+ local isodevs="${DI_ISO9660_DEVS}"
938+ case "$isodevs" in
939+ ""|$UNAVAILABLE:*) return ${DS_NOT_FOUND};;
940+ esac
941+
942+ # DI_ISO9660_DEVS is <device>=label, like /dev/sr0=OVF-TRANSPORT
943+ for tok in $isodevs; do
944+ is_cdrom_ovf "${tok%%=*}" "${tok#*=}" && return $DS_FOUND
945+ done
946+
947 if ovf_vmware_guest_customization; then
948 return ${DS_FOUND}
949 fi
950
951- has_cdrom || return ${DS_NOT_FOUND}
952+ return ${DS_NOT_FOUND}
953+}
954
955- # FIXME: currently just return maybe if there is a cdrom
956- # ovf iso9660 transport does not specify an fs label.
957- # better would be to check if
958- return ${DS_MAYBE}
959+is_azure_chassis() {
960+ local azure_chassis="7783-7084-3265-9085-8269-3286-77"
961+ dmi_chassis_asset_tag_matches "${azure_chassis}"
962 }
963
964 dscheck_Azure() {
965@@ -667,8 +710,7 @@ dscheck_Azure() {
966 # UUID="112D211272645f72" LABEL="rd_rdfe_stable.161212-1209"
967 # TYPE="udf">/dev/sr0</device>
968 #
969- local azure_chassis="7783-7084-3265-9085-8269-3286-77"
970- dmi_chassis_asset_tag_matches "${azure_chassis}" && return $DS_FOUND
971+ is_azure_chassis && return $DS_FOUND
972 check_seed_dir azure ovf-env.xml && return ${DS_FOUND}
973
974 [ "${DI_VIRT}" = "microsoft" ] || return ${DS_NOT_FOUND}
975@@ -930,7 +972,7 @@ collect_info() {
976 read_dmi_product_name
977 read_dmi_product_serial
978 read_dmi_product_uuid
979- read_fs_labels
980+ read_fs_info
981 }
982
983 print_info() {
984@@ -942,7 +984,7 @@ _print_info() {
985 local n="" v="" vars=""
986 vars="DMI_PRODUCT_NAME DMI_SYS_VENDOR DMI_PRODUCT_SERIAL"
987 vars="$vars DMI_PRODUCT_UUID PID_1_PRODUCT_NAME DMI_CHASSIS_ASSET_TAG"
988- vars="$vars FS_LABELS KERNEL_CMDLINE VIRT"
989+ vars="$vars FS_LABELS ISO9660_DEVS KERNEL_CMDLINE VIRT"
990 vars="$vars UNAME_KERNEL_NAME UNAME_KERNEL_RELEASE UNAME_KERNEL_VERSION"
991 vars="$vars UNAME_MACHINE UNAME_NODENAME UNAME_OPERATING_SYSTEM"
992 vars="$vars DSNAME DSLIST"

Subscribers

People subscribed via source and target branches