]> git.ipfire.org Git - thirdparty/open-vm-tools.git/commitdiff
Make Linux perl based customization work with the cloud-init workflow.
authorJohn Wolfe <jwolfe@vmware.com>
Thu, 10 Nov 2022 20:01:14 +0000 (12:01 -0800)
committerJohn Wolfe <jwolfe@vmware.com>
Thu, 10 Nov 2022 20:01:14 +0000 (12:01 -0800)
To resolve issues seen where users want to set a vm's networking
and apply cloud-init userdata together before the vm is booted, the
deployPkg plugin has been modified to wait for cloud-init
execution to finish.  This allows cloud-init to finish execution
completely before the customization process triggers a reboot
of the guest.

This change is solely in the deployPkg plugin side, so a user can get
this change by upgrading their open-vm-tools in the guest/template.
Crossport of change 10318445 and 10330918 from main to vmtools-prod-cpd.

open-vm-tools/lib/include/conf.h
open-vm-tools/lib/include/deployPkg/linuxDeployment.h
open-vm-tools/libDeployPkg/linuxDeployment.c
open-vm-tools/services/plugins/deployPkg/deployPkg.c

index 282b9e0c0d2dc4daaf66f32ae889301e99b451c4..cad1563b6d3f15fe0ef5894eb253ce6c31637072 100644 (file)
  */
 #define CONFNAME_DEPLOYPKG_ENABLE_CUST "enable-customization"
 
+/**
+ * How long does guest customization wait until cloud-init execution done
+ * Valid value range: 0 ~ 1800
+ */
+#define CONFNAME_DEPLOYPKG_WAIT_CLOUDINIT_TIMEOUT "wait-cloudinit-timeout"
+
 /*
  * END deployPkg goodies.
  ******************************************************************************
index ea8b292c6dc51fc2dffe0cff0dac724ee227a600..a145cc364f3a966169a105b46a52818f7d2d599d 100644 (file)
@@ -1,5 +1,5 @@
 /*********************************************************
- * Copyright (C) 2009-2019 VMware, Inc. All rights reserved.
+ * Copyright (C) 2009-2019, 2022 VMware, Inc. All rights reserved.
  *
  * This program is free software; you can redistribute it and/or modify it
  * under the terms of the GNU Lesser General Public License as published
@@ -64,6 +64,25 @@ IMGCUST_API void
 DeployPkg_SetProcessTimeout(uint16 timeout);
 
 
+/*
+ *------------------------------------------------------------------------------
+ *
+ * DeployPkg_SetWaitForCloudinitDoneTimeout
+ *
+ *     Set the timeout value of customization process waits for cloud-init
+ *     execution done before trigger reboot and after connect network adapters.
+ *
+ * @param timeout [in]
+ *     timeout value to be used for waiting for cloud-init execution done
+ *
+ *------------------------------------------------------------------------------
+ */
+
+IMGCUST_API void
+DeployPkg_SetWaitForCloudinitDoneTimeout(uint16 timeout);
+
+
+
 /*
  *------------------------------------------------------------------------------
  *
index b1f9c8dd0bf80c1267826b73a95ce12c723fb945..e805990202c1f761d40f6a6fac1292a283a2fd25 100644 (file)
@@ -98,6 +98,10 @@ VM_EMBED_VERSION(SYSIMAGE_VERSION_EXT_STR);
 
 // the maximum length of cloud-init version stdout
 #define MAX_LENGTH_CLOUDINIT_VERSION 256
+// the maximum length of cloud-init status stdout
+#define MAX_LENGTH_CLOUDINIT_STATUS 256
+// the default timeout of waiting for cloud-init execution done
+#define DEFAULT_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE 30
 
 /*
  * Constant definitions
@@ -136,6 +140,16 @@ typedef enum USE_CLOUDINIT_ERROR_CODE {
    USE_CLOUDINIT_IGNORE,
 } USE_CLOUDINIT_ERROR_CODE;
 
+// the user-visible cloud-init application status code
+typedef enum CLOUDINIT_STATUS_CODE {
+   CLOUDINIT_STATUS_NOT_RUN = 0,
+   CLOUDINIT_STATUS_RUNNING,
+   CLOUDINIT_STATUS_DONE,
+   CLOUDINIT_STATUS_ERROR,
+   CLOUDINIT_STATUS_DISABLED,
+   CLOUDINIT_STATUS_UNKNOWN,
+} CLOUDINIT_STATUS_CODE;
+
 /*
  * Linked list definition
  *
@@ -191,6 +205,8 @@ static char* gDeployError = NULL;
 LogFunction sLog = NoLogging;
 static uint16 gProcessTimeout = DEPLOYPKG_PROCESSTIMEOUT_DEFAULT;
 static bool gProcessTimeoutSetByLauncher = false;
+static uint16 gWaitForCloudinitDoneTimeout =
+   DEFAULT_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE;
 
 // .....................................................................................
 
@@ -226,6 +242,31 @@ DeployPkg_SetProcessTimeout(uint16 timeout)
    }
 }
 
+/*
+ *------------------------------------------------------------------------------
+ *
+ * DeployPkg_SetWaitForCloudinitDoneTimeout
+ *
+ *     Set the timeout value of customization process waits for cloud-init
+ *     execution done before trigger reboot and after connect network adapters.
+ *     Tools deployPkg plugin reads this timeout value from tools.conf and
+ *     checks if the timeout value is valid, then calls this API to set the
+ *     valid timeout value to gWaitForCloudinitDoneTimeout.
+ *
+ * @param timeout [in]
+ *     timeout value to be used for waiting for cloud-init execution done
+ *
+ *------------------------------------------------------------------------------
+ */
+
+void
+DeployPkg_SetWaitForCloudinitDoneTimeout(uint16 timeout)
+{
+   gWaitForCloudinitDoneTimeout = timeout;
+   sLog(log_debug, "Wait for cloud-init execution done timeout value: %d.",
+        gWaitForCloudinitDoneTimeout);
+}
+
 // .....................................................................................
 
 /**
@@ -1183,13 +1224,14 @@ CopyFileToDirectory(const char* srcPath, const char* destPath,
  * - cloud-init is enabled.
  *
  * @param   [IN]  dirPath  Path where the package is extracted.
+ * @param   [IN]  ignoreCloudInit  whether ignore cloud-init workflow.
  * @returns the error code to use cloud-init work flow
  *
  *----------------------------------------------------------------------------
  * */
 
 static USE_CLOUDINIT_ERROR_CODE
-UseCloudInitWorkflow(const char* dirPath)
+UseCloudInitWorkflow(const char* dirPath, bool ignoreCloudInit)
 {
    static const char cfgName[] = "cust.cfg";
    static const char metadataName[] = "metadata";
@@ -1198,24 +1240,29 @@ UseCloudInitWorkflow(const char* dirPath)
    char cloudInitCommandOutput[MAX_LENGTH_CLOUDINIT_VERSION];
    int forkExecResult;
 
-   if (NULL == dirPath) {
-      return USE_CLOUDINIT_INTERNAL_ERROR;
-   }
-
-   // check if cust.cfg file exists
-   if (!CheckFileExist(dirPath, cfgName)) {
-      return USE_CLOUDINIT_NO_CUST_CFG;
-   }
-
    forkExecResult = ForkExecAndWaitCommand(cloudInitCommand,
                                            false,
                                            cloudInitCommandOutput,
                                            sizeof(cloudInitCommandOutput));
    if (forkExecResult != 0) {
-      sLog(log_info, "cloud-init is not installed.");
+      sLog(log_info, "Cloud-init is not installed.");
       return USE_CLOUDINIT_NOT_INSTALLED;
    } else {
-      sLog(log_info, "cloud-init is installed.");
+      sLog(log_info, "Cloud-init is installed.");
+      if (ignoreCloudInit) {
+         sLog(log_info,
+              "Ignoring cloud-init workflow according to header flags.");
+         return USE_CLOUDINIT_IGNORE;
+      }
+   }
+
+   if (NULL == dirPath) {
+      return USE_CLOUDINIT_INTERNAL_ERROR;
+   }
+
+   // check if cust.cfg file exists
+   if (!CheckFileExist(dirPath, cfgName)) {
+      return USE_CLOUDINIT_NO_CUST_CFG;
    }
 
    // If cloud-init metadata exists, check if cloud-init support to handle
@@ -1225,12 +1272,12 @@ UseCloudInitWorkflow(const char* dirPath)
    if (CheckFileExist(dirPath, metadataName)) {
       int major, minor;
       GetCloudinitVersion(cloudInitCommandOutput, &major, &minor);
-      sLog(log_info, "metadata exists, check cloud-init version...");
+      sLog(log_info, "Metadata exists, check cloud-init version...");
       if (major < CLOUDINIT_SUPPORT_RAW_DATA_MAJOR_VERSION ||
           (major == CLOUDINIT_SUPPORT_RAW_DATA_MAJOR_VERSION &&
            minor < CLOUDINIT_SUPPORT_RAW_DATA_MINOR_VERSION)) {
           sLog(log_info,
-               "cloud-init version %d.%d is older than required version %d.%d",
+               "Cloud-init version %d.%d is older than required version %d.%d.",
                major,
                minor,
                CLOUDINIT_SUPPORT_RAW_DATA_MAJOR_VERSION,
@@ -1249,6 +1296,113 @@ UseCloudInitWorkflow(const char* dirPath)
 }
 
 
+/**
+ *
+ * Function which gets the current cloud-init execution status.
+ * The status messages are copied from cloud-init offcial upstream
+ * https://github.com/canonical/cloud-init/blob/main/cloudinit/cmd/status.py
+ * These status messages are consistent since year 2017
+ *
+ * @returns the status code of cloud-init application
+ *
+ **/
+
+static CLOUDINIT_STATUS_CODE
+GetCloudinitStatus() {
+   // Cloud-init execution status messages
+   static const char* NOT_RUN = "not run";
+   static const char* RUNNING = "running";
+   static const char* DONE = "done";
+   static const char* ERROR = "error";
+   static const char* DISABLED = "disabled";
+
+   static const char cloudinitStatusCmd[] = "/usr/bin/cloud-init status";
+   char cloudinitStatusCmdOutput[MAX_LENGTH_CLOUDINIT_STATUS];
+   int forkExecResult;
+
+   forkExecResult = ForkExecAndWaitCommand(cloudinitStatusCmd,
+                                           false,
+                                           cloudinitStatusCmdOutput,
+                                           MAX_LENGTH_CLOUDINIT_STATUS);
+   if (forkExecResult != 0) {
+      sLog(log_info, "Unable to get cloud-init status.");
+      return CLOUDINIT_STATUS_UNKNOWN;
+   } else {
+      if (strstr(cloudinitStatusCmdOutput, NOT_RUN) != NULL) {
+         sLog(log_info, "Cloud-init status is '%s'.", NOT_RUN);
+         return CLOUDINIT_STATUS_NOT_RUN;
+      } else if (strstr(cloudinitStatusCmdOutput, RUNNING) != NULL) {
+         sLog(log_info, "Cloud-init status is '%s'.", RUNNING);
+         return CLOUDINIT_STATUS_RUNNING;
+      } else if (strstr(cloudinitStatusCmdOutput, DONE) != NULL) {
+         sLog(log_info, "Cloud-init status is '%s'.", DONE);
+         return CLOUDINIT_STATUS_DONE;
+      } else if (strstr(cloudinitStatusCmdOutput, ERROR) != NULL) {
+         sLog(log_info, "Cloud-init status is '%s'.", ERROR);
+         return CLOUDINIT_STATUS_ERROR;
+      } else if (strstr(cloudinitStatusCmdOutput, DISABLED) != NULL) {
+         sLog(log_info, "Cloud-init status is '%s'.", DISABLED);
+         return CLOUDINIT_STATUS_DISABLED;
+      } else {
+         sLog(log_warning, "Cloud-init status is unknown.");
+         return CLOUDINIT_STATUS_UNKNOWN;
+      }
+   }
+}
+
+
+/**
+ *
+ * Function which waits for cloud-init execution done.
+ *
+ * This function is called only when below conditions are fulfilled:
+ * - cloud-init is installed
+ * - guest os reboot is not skipped (so traditional GOSC workflow only)
+ * - deployment processed successfully in guest
+ *
+ * Default waiting timeout is DEFAULT_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE seconds,
+ * when the timeout is reached, reboot will be triggered no matter what the
+ * cloud-init execution status is then.
+ * The timeout can be overwritten by the value which is set in tools.conf,
+ * if 0 is set in tools.conf, no waiting will be performed.
+ *
+ **/
+
+static void
+WaitForCloudinitDone() {
+   const int CheckStatusInterval = 5;
+   int timeoutSec = 0;
+   int elapsedSec = 0;
+   CLOUDINIT_STATUS_CODE cloudinitStatus = CLOUDINIT_STATUS_UNKNOWN;
+
+   // No waiting when gWaitForCloudinitDoneTimeout is set to 0
+   if (gWaitForCloudinitDoneTimeout == 0) {
+      return;
+   }
+
+   timeoutSec = gWaitForCloudinitDoneTimeout;
+
+   while (1) {
+      if (elapsedSec == timeoutSec) {
+         sLog(log_info, "Timed out waiting for cloud-init execution done.");
+         return;
+      }
+      if (elapsedSec % CheckStatusInterval == 0) {
+         cloudinitStatus = GetCloudinitStatus();
+         // CLOUDINIT_STATUS_NOT_RUN and CLOUDINIT_STATUS_RUNNING represent
+         // cloud-init execution has not finished
+         if (cloudinitStatus != CLOUDINIT_STATUS_NOT_RUN &&
+             cloudinitStatus != CLOUDINIT_STATUS_RUNNING) {
+            sLog(log_info, "Cloud-init execution is not on-going.");
+            return;
+         }
+      }
+      sleep(1);
+      elapsedSec++;
+   }
+}
+
+
 /**
  *
  * Function which cleans up the deployment directory imcDirPath.
@@ -1316,6 +1470,7 @@ Deploy(const char* packageName)
    char *imcDirPath = NULL;
    USE_CLOUDINIT_ERROR_CODE useCloudInitWorkflow = USE_CLOUDINIT_IGNORE;
    int imcDirPathSize = 0;
+   bool ignoreCloudInit = false;
    TransitionState(NULL, INPROGRESS);
 
    // Notify the vpx of customization in-progress state
@@ -1400,11 +1555,8 @@ Deploy(const char* packageName)
       }
    }
 
-   if (!(flags & VMWAREDEPLOYPKG_HEADER_FLAGS_IGNORE_CLOUD_INIT)) {
-      useCloudInitWorkflow = UseCloudInitWorkflow(imcDirPath);
-   } else {
-      sLog(log_info, "Ignoring cloud-init.");
-   }
+   ignoreCloudInit = flags & VMWAREDEPLOYPKG_HEADER_FLAGS_IGNORE_CLOUD_INIT;
+   useCloudInitWorkflow = UseCloudInitWorkflow(imcDirPath, ignoreCloudInit);
 
    sLog(log_info, "UseCloudInitWorkflow return: %d", useCloudInitWorkflow);
 
@@ -1511,6 +1663,10 @@ Deploy(const char* packageName)
 
    //Reset the guest OS
    if (!sSkipReboot && !deploymentResult) {
+      if (useCloudInitWorkflow != USE_CLOUDINIT_NOT_INSTALLED) {
+         sLog(log_info, "Do not trigger reboot if cloud-init is executing.");
+         WaitForCloudinitDone();
+      }
       pid_t pid = fork();
       if (pid == -1) {
          sLog(log_error, "Failed to fork: '%s'.", strerror(errno));
index 4d70765a5188e0f5c7b3f3a90d9b891b05a0fc49..b1f0324bfd572cb8dcbbd926e0777b8a952b07e1 100644 (file)
@@ -59,6 +59,9 @@ using namespace ImgCustCommon;
 
 // Using 3600s as the upper limit of timeout value in tools.conf.
 #define MAX_TIMEOUT_FROM_TOOLCONF 3600
+// Using 1800s as the upper limit of waiting for cloud-init execution done
+// timeout value in tools.conf.
+#define MAX_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE 1800
 
 static char *DeployPkgGetTempDir(void);
 
@@ -90,6 +93,7 @@ DeployPkgDeployPkgInGuest(ToolsAppCtx *ctx,    // IN: app context
    ToolsDeployPkgError ret = TOOLSDEPLOYPKG_ERROR_SUCCESS;
 #ifndef _WIN32
    int processTimeout;
+   int waitForCloudinitDoneTimeout;
 #endif
 
    /*
@@ -156,10 +160,10 @@ DeployPkgDeployPkgInGuest(ToolsAppCtx *ctx,    // IN: app context
     * Using 0 as the default value of CONFNAME_DEPLOYPKG_PROCESSTIMEOUT in tools.conf
     */
    processTimeout =
-        VMTools_ConfigGetInteger(ctx->config,
-                                 CONFGROUPNAME_DEPLOYPKG,
-                                 CONFNAME_DEPLOYPKG_PROCESSTIMEOUT,
-                                 0);
+      VMTools_ConfigGetInteger(ctx->config,
+                               CONFGROUPNAME_DEPLOYPKG,
+                               CONFNAME_DEPLOYPKG_PROCESSTIMEOUT,
+                               0);
    if (processTimeout > 0 && processTimeout <= MAX_TIMEOUT_FROM_TOOLCONF) {
       DeployPkgLog_Log(log_debug, "[%s] %s in tools.conf: %d",
                        CONFGROUPNAME_DEPLOYPKG,
@@ -174,6 +178,41 @@ DeployPkgDeployPkgInGuest(ToolsAppCtx *ctx,    // IN: app context
       DeployPkgLog_Log(log_debug, "The valid timeout value range: 1 ~ %d",
                        MAX_TIMEOUT_FROM_TOOLCONF);
    }
+
+   /*
+    * Get timeout of waiting for cloud-init execution done from tools.conf.
+    * Only when a valid 'timeout' got from tools.conf, deployPkg will call
+    * DeployPkg_SetWaitForCloudinitDoneTimeout to overwrite the default timeout
+    * of waiting for cloud-init execution done.
+    * The valid value range is from 0 to MAX_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE.
+    * Return an invalid value -1 if CONFNAME_DEPLOYPKG_WAIT_CLOUDINIT_TIMEOUT is
+    * not set in tools.conf.
+    */
+   waitForCloudinitDoneTimeout =
+      VMTools_ConfigGetInteger(ctx->config,
+                               CONFGROUPNAME_DEPLOYPKG,
+                               CONFNAME_DEPLOYPKG_WAIT_CLOUDINIT_TIMEOUT,
+                               -1);
+   if (waitForCloudinitDoneTimeout >= 0 &&
+       waitForCloudinitDoneTimeout <= MAX_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE) {
+      DeployPkgLog_Log(log_debug, "[%s] %s in tools.conf: %d",
+                       CONFGROUPNAME_DEPLOYPKG,
+                       CONFNAME_DEPLOYPKG_WAIT_CLOUDINIT_TIMEOUT,
+                       waitForCloudinitDoneTimeout);
+      DeployPkg_SetWaitForCloudinitDoneTimeout(waitForCloudinitDoneTimeout);
+   } else {
+      if (waitForCloudinitDoneTimeout != -1) {
+         DeployPkgLog_Log(log_debug,
+                          "Ignore invalid value %d from tools.conf [%s] %s",
+                          waitForCloudinitDoneTimeout,
+                          CONFGROUPNAME_DEPLOYPKG,
+                          CONFNAME_DEPLOYPKG_WAIT_CLOUDINIT_TIMEOUT);
+      }
+      DeployPkgLog_Log(log_debug, "The valid [%s] %s value range: 0 ~ %d",
+                       CONFGROUPNAME_DEPLOYPKG,
+                       CONFNAME_DEPLOYPKG_WAIT_CLOUDINIT_TIMEOUT,
+                       MAX_TIMEOUT_WAIT_FOR_CLOUDINIT_DONE);
+   }
 #endif
 
    if (0 != DeployPkg_DeployPackageFromFile(pkgFile)) {