← Back to team overview

cloud-init-dev team mailing list archive

[Merge] ~msaikia/cloud-init:topic-msaikia-vmware-custom-script into cloud-init:master

 

Maitreyee Saikia has proposed merging ~msaikia/cloud-init:topic-msaikia-vmware-custom-script into cloud-init:master.

Requested reviews:
  cloud-init commiters (cloud-init-dev)

For more details, see:
https://code.launchpad.net/~msaikia/cloud-init/+git/cloud-init/+merge/329421

Support for users to upload custom scripts in VMware customization workflow.

In the VMware workflow, we have some options for the user to upload custom scripts for additional customization. Based on user request those custom scripts can be either run before regular customization or after. For post customization scripts, we decide whether to run the scripts just after customization or post reboot.
-- 
Your team cloud-init commiters is requested to review the proposed merge of ~msaikia/cloud-init:topic-msaikia-vmware-custom-script into cloud-init:master.
diff --git a/cloudinit/sources/DataSourceOVF.py b/cloudinit/sources/DataSourceOVF.py
index 73d3877..fa9359a 100644
--- a/cloudinit/sources/DataSourceOVF.py
+++ b/cloudinit/sources/DataSourceOVF.py
@@ -21,6 +21,8 @@ from cloudinit import util
 
 from cloudinit.sources.helpers.vmware.imc.config \
     import Config
+from cloudinit.sources.helpers.vmware.imc.config_custom_script \
+    import PreCustomScript, PostCustomScript
 from cloudinit.sources.helpers.vmware.imc.config_file \
     import ConfigFile
 from cloudinit.sources.helpers.vmware.imc.config_nic \
@@ -127,10 +129,26 @@ class DataSourceOVF(sources.DataSource):
                 set_customization_status(
                     GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
                     GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
+                util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
                 enable_nics(nics)
                 return False
-            finally:
-                util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
+            if markerid and not markerexists:
+                try:
+                    customscript = conf.custom_script_name
+                    LOG.debug(customscript)
+                    if customscript:
+                        LOG.debug("Executing pre-customization script")
+                        precust = PreCustomScript(customscript, dirpath)
+                        precust.execute()
+                except Exception as e:
+                    LOG.debug("Error executing pre-customization script")
+                    LOG.exception(e)
+                    set_customization_status(
+                        GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
+                        GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
+                    util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
+                    enable_nics(nics)
+                    return False
             try:
                 LOG.debug("Applying the Network customization")
                 nicConfigurator = NicConfigurator(conf.nics)
@@ -141,6 +159,7 @@ class DataSourceOVF(sources.DataSource):
                 set_customization_status(
                     GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
                     GuestCustEventEnum.GUESTCUST_EVENT_NETWORK_SETUP_FAILED)
+                util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
                 enable_nics(nics)
                 return False
             if markerid and not markerexists:
@@ -159,6 +178,24 @@ class DataSourceOVF(sources.DataSource):
                     set_customization_status(
                         GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
                         GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
+                    util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
+                    enable_nics(nics)
+                    return False
+                try:
+                    customscript = conf.custom_script_name
+                    if customscript:
+                        LOG.debug("Executing/preparing post-customization \
+                                  script")
+                        postcust = PostCustomScript(customscript, dirpath)
+                        postcust.execute()
+                except Exception as e:
+                    LOG.debug("Error executing/preparing post-customization \
+                              script")
+                    LOG.exception(e)
+                    set_customization_status(
+                        GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
+                        GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
+                    util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
                     enable_nics(nics)
                     return False
             if markerid:
@@ -170,6 +207,7 @@ class DataSourceOVF(sources.DataSource):
                     set_customization_status(
                         GuestCustStateEnum.GUESTCUST_STATE_RUNNING,
                         GuestCustEventEnum.GUESTCUST_EVENT_CUSTOMIZE_FAILED)
+                    util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
                     enable_nics(nics)
                     return False
 
@@ -177,6 +215,7 @@ class DataSourceOVF(sources.DataSource):
             set_customization_status(
                 GuestCustStateEnum.GUESTCUST_STATE_DONE,
                 GuestCustErrorEnum.GUESTCUST_ERROR_SUCCESS)
+            util.del_dir(os.path.dirname(vmwareImcConfigFilePath))
             enable_nics(nics)
         else:
             np = {'iso': transport_iso9660,
diff --git a/cloudinit/sources/helpers/vmware/imc/config.py b/cloudinit/sources/helpers/vmware/imc/config.py
index 49d441d..2eaeff3 100644
--- a/cloudinit/sources/helpers/vmware/imc/config.py
+++ b/cloudinit/sources/helpers/vmware/imc/config.py
@@ -100,4 +100,8 @@ class Config(object):
         """Returns marker id."""
         return self._configFile.get(Config.MARKERID, None)
 
+    @property
+    def custom_script_name(self):
+        """Return the name of custom (pre/post) script."""
+        return self._configFile.get(Config.CUSTOM_SCRIPT, None)
 # vi: ts=4 expandtab
diff --git a/cloudinit/sources/helpers/vmware/imc/config_custom_script.py b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
new file mode 100644
index 0000000..094a563
--- /dev/null
+++ b/cloudinit/sources/helpers/vmware/imc/config_custom_script.py
@@ -0,0 +1,164 @@
+# Copyright (C) 2017 Canonical Ltd.
+# Copyright (C) 2017 VMware Inc.
+#
+# Author: Maitreyee Saikia <msaikia@xxxxxxxxxx>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+import logging
+import os
+import shutil
+import stat
+
+from cloudinit import util
+
+LOG = logging.getLogger(__name__)
+
+
+class CustomScriptNotFound(Exception):
+    pass
+
+
+class CustomScriptConstant(object):
+    RC_LOCAL = "/etc/rc.local"
+    POST_CUST_TMP_DIR = "/root/.customization"
+    POST_CUST_RUN_SCRIPT_NAME = "post-customize-guest.sh"
+    POST_CUST_RUN_SCRIPT = os.path.join(POST_CUST_TMP_DIR,
+                                        POST_CUST_RUN_SCRIPT_NAME)
+    POST_REBOOT_PENDING_MARKER = "/.guest-customization-post-reboot-pending"
+
+
+class RunCustomScript(object):
+    def __init__(self, scriptname, directory):
+        self.scriptname = scriptname
+        self.directory = directory
+        self.scriptpath = os.path.join(directory, scriptname)
+
+    def prepare_script(self):
+        if os.path.exists(self.scriptpath):
+            tempscript = self.scriptpath + ".tmp"
+            # Strip any CR characters from the decoded script
+            inF = open(self.scriptpath, "r")
+            data = inF.read().replace("\r", "")
+            inF.close()
+            outF = open(tempscript, "w")
+            outF.write(data)
+            outF.close()
+            shutil.move(tempscript, self.scriptpath)
+            st = os.stat(self.scriptpath)
+            os.chmod(self.scriptpath, st.st_mode | stat.S_IEXEC)
+        else:
+            LOG.debug("Custom script doesnot exist")
+            raise CustomScriptNotFound("Script %s not found!! "
+                                       "Cannot execute custom script!",
+                                       self.scriptpath)
+
+
+class PreCustomScript(RunCustomScript):
+    def execute(self):
+        """Executing custom script with precustomization argument."""
+        LOG.info("Executing pre-customization script")
+        self.prepare_script()
+        util.subp(["/bin/sh", self.scriptpath, "precustomization"])
+
+
+class PostCustomScript(RunCustomScript):
+    def __init__(self, scriptname, directory):
+        super(PostCustomScript, self).__init__(scriptname, directory)
+        # Determine when to run custom script.
+        self.prereboot = True
+
+    def _install_post_reboot_agent(self, rclocal):
+        """
+        Install post-reboot agent for running custom script after reboot.
+        @param: path to rc local.
+        """
+        LOG.info("Installing post-reboot customization from %s to %s",
+                 self.directory, rclocal)
+        LOG.info("Checking rc.local for previous customization agent "
+                 "installation")
+        if not self.has_previous_agent(rclocal):
+            LOG.info("Adding post-reboot customization agent to rc.local")
+            # installing agent
+            rclocaltemp = CustomScriptConstant.RC_LOCAL + ".tmp"
+            excludestring = "exit 0"
+            out, _ = util.subp(["/bin/grep", "-v", excludestring, rclocal])
+            util.write_file(rclocaltemp, out, omode="w")
+            data = "\n".join([
+                "",
+                "# Run post-reboot guest customization",
+                "/bin/sh " + CustomScriptConstant.POST_CUST_RUN_SCRIPT,
+                "exit 0",
+                ""])
+            st = os.stat(rclocal)
+            # "x" flag should be set
+            mode = st.st_mode | stat.S_IEXEC
+            util.write_file(rclocaltemp, data, mode, "a")
+            shutil.move(rclocaltemp, rclocal)
+        else:
+            LOG.info("Post-reboot guest customization agent is already "
+                     "registered in rc.local")
+        LOG.info("Installing post-reboot customization agent finished: %s",
+                 self.prereboot)
+
+    def has_previous_agent(self, rclocal):
+        searchstring = "# Run post-reboot guest customization"
+        if searchstring in open(rclocal).read():
+            return True
+        return False
+
+    def find_rc_local(self):
+        """
+        Determine if rc local is present.
+        """
+        rclocal = ""
+        if os.path.exists(CustomScriptConstant.RC_LOCAL):
+            LOG.info("rc.local detected.")
+            # resolving in case of symlink
+            rclocal = os.path.realpath(CustomScriptConstant.RC_LOCAL)
+            LOG.info("rc.local resolved to %s", rclocal)
+        else:
+            LOG.warning("Can't find rc.local, post-customization "
+                        "will be run before reboot")
+        return rclocal
+
+    def install_agent(self):
+        rclocal = self.find_rc_local()
+        if rclocal:
+            self._install_post_reboot_agent(rclocal)
+            self.prereboot = False
+
+    def execute(self):
+        """
+        This method executes post-customization script before or after reboot
+        based on the presence of rc local.
+        """
+        LOG.info("Executing post-customization script")
+        self.prepare_script()
+        self.install_agent()
+        if self.prereboot:
+            LOG.warning("Executing post-customization script inline")
+            util.subp(["/bin/sh", self.scriptpath, "postcustomization"])
+        else:
+            LOG.info("Scheduling post-customization script")
+            if not os.path.isdir(CustomScriptConstant.POST_CUST_TMP_DIR):
+                os.mkdir(CustomScriptConstant.POST_CUST_TMP_DIR)
+            # Script "post-customize-guest.sh" and user uploaded script are
+            # are present in the same directory and needs to copied to a temp
+            # directory to be executed post reboot. User uploaded script is
+            # saved as customize.sh in the temp directory.
+            # post-customize-guest.sh excutes customize.sh after reboot.
+            LOG.info("Copying post-customization script")
+            util.copy(self.scriptpath,
+                      CustomScriptConstant.POST_CUST_TMP_DIR + "/customize.sh")
+            LOG.info("Copying script to run post-customization script")
+            util.copy(
+                os.path.join(self.directory,
+                             CustomScriptConstant.POST_CUST_RUN_SCRIPT_NAME),
+                CustomScriptConstant.POST_CUST_RUN_SCRIPT)
+            LOG.info("Creating post-reboot pending marker")
+            util.del_file(CustomScriptConstant.POST_REBOOT_PENDING_MARKER)
+            util.subp(["/bin/touch",
+                       CustomScriptConstant.POST_REBOOT_PENDING_MARKER])
+
+# vi: ts=4 expandtab
diff --git a/tests/unittests/test_vmware/__init__.py b/tests/unittests/test_vmware/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/unittests/test_vmware/__init__.py
diff --git a/tests/unittests/test_vmware/test_custom_script.py b/tests/unittests/test_vmware/test_custom_script.py
new file mode 100644
index 0000000..b66312c
--- /dev/null
+++ b/tests/unittests/test_vmware/test_custom_script.py
@@ -0,0 +1,80 @@
+# Copyright (C) 2015 Canonical Ltd.
+# Copyright (C) 2017 VMware INC.
+#
+# Author: Maitreyee Saikia <msaikia@xxxxxxxxxx>
+#
+# This file is part of cloud-init. See LICENSE file for license information.
+
+from ..helpers import CiTestCase
+from cloudinit import util
+from cloudinit.sources.helpers.vmware.imc.config_custom_script import (
+    CustomScriptConstant,
+    CustomScriptNotFound,
+    PreCustomScript,
+    PostCustomScript
+)
+
+
+class TestVmwareCustomScript(CiTestCase):
+
+    def test_prepare_custom_script(self):
+        # custom script does not exist
+        preCust = PreCustomScript("random-vmw-test", "/tmp")
+        self.assertEqual("random-vmw-test", preCust.scriptname)
+        self.assertEqual("/tmp", preCust.directory)
+        self.assertEqual("/tmp/random-vmw-test", preCust.scriptpath)
+        with self.assertRaises(CustomScriptNotFound):
+            preCust.prepare_script()
+
+        # custom script exists
+        util.write_file("/tmp/test-cust", "test-CR-strip/r/r")
+        postCust = PostCustomScript("test-cust", "/tmp")
+        self.assertEqual("test-cust", postCust.scriptname)
+        self.assertEqual("/tmp", postCust.directory)
+        self.assertEqual("/tmp/test-cust", postCust.scriptpath)
+        self.assertTrue(postCust.prereboot)
+        postCust.prepare_script()
+        self.assertFalse("/r" in "/tmp/test-cust")
+        util.del_file("/tmp/test-cust")
+
+    def test_rc_local_exists(self):
+        # test when rc local does not exist
+        CustomScriptConstant.RC_LOCAL = "/no/path/exist"
+        postCust = PostCustomScript("test-cust", "/tmp")
+        rclocal = postCust.find_rc_local()
+        self.assertEqual("", rclocal)
+
+        # test when rc local exists
+        rclocalFile = "/tmp/vmware-rclocal"
+        util.write_file(rclocalFile, "# Run post-reboot guest customization",
+                        omode="w")
+        CustomScriptConstant.RC_LOCAL = rclocalFile
+        rclocal = postCust.find_rc_local()
+        self.assertEqual(rclocalFile, rclocal)
+        self.assertTrue(postCust.has_previous_agent, rclocal)
+
+        # test when rc local is a symlink
+        util.sym_link(rclocalFile, "/tmp/dummy-rclocal-link", True)
+        CustomScriptConstant.RC_LOCAL = "/tmp/dummy-rclocal-link"
+        rclocal = postCust.find_rc_local()
+        self.assertEqual(rclocalFile, rclocal)
+        util.del_file("/tmp/dummy-rclocal-link")
+        util.del_file(rclocalFile)
+
+    def test_execute_post_cust(self):
+        customscript = "/tmp/vmware-post-cust-script"
+        rclocal = "/tmp/vmware-rclocal"
+        open(customscript, "w")
+        util.write_file(rclocal, "tests\nexit 0", omode="w")
+        CustomScriptConstant.RC_LOCAL = rclocal
+
+        postCust = PostCustomScript("vmware-post-cust-script", "/tmp")
+        self.assertTrue(postCust.prereboot)
+        self.assertIs(postCust.has_previous_agent(rclocal), False)
+        postCust.install_agent()
+        self.assertFalse(postCust.prereboot)
+        self.assertTrue(postCust.has_previous_agent, rclocal)
+        util.del_file(customscript)
+        util.del_file(rclocal)
+
+# vi: ts=4 expandtab
diff --git a/tests/unittests/test_vmware_config_file.py b/tests/unittests/test_vmware_config_file.py
index 03b36d3..a2d7ded 100644
--- a/tests/unittests/test_vmware_config_file.py
+++ b/tests/unittests/test_vmware_config_file.py
@@ -117,5 +117,12 @@ class TestVmwareConfigFile(CiTestCase):
         conf = Config(cf)
         self.assertTrue(conf.reset_password, "reset password")
 
+    def test_custom_script(self):
+        cf = ConfigFile("tests/data/vmware/cust-dhcp-2nic.cfg")
+        conf = Config(cf)
+        self.assertIsNone(conf.custom_script_name)
+        cf._insertKey("CUSTOM-SCRIPT|SCRIPT-NAME", "test-script")
+        conf = Config(cf)
+        self.assertEqual("test-script", conf.custom_script_name)
 
 # vi: ts=4 expandtab

Follow ups