From cd995a58b07a91d7804d9fdec5545a5fe11e9db9 Mon Sep 17 00:00:00 2001 From: John Wolfe Date: Thu, 10 Nov 2022 12:01:14 -0800 Subject: [PATCH] Make Linux perl based customization work with the cloud-init workflow. 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 | 6 + .../lib/include/deployPkg/linuxDeployment.h | 21 +- open-vm-tools/libDeployPkg/linuxDeployment.c | 194 ++++++++++++++++-- .../services/plugins/deployPkg/deployPkg.c | 47 ++++- 4 files changed, 244 insertions(+), 24 deletions(-) diff --git a/open-vm-tools/lib/include/conf.h b/open-vm-tools/lib/include/conf.h index 282b9e0c0..cad1563b6 100644 --- a/open-vm-tools/lib/include/conf.h +++ b/open-vm-tools/lib/include/conf.h @@ -587,6 +587,12 @@ */ #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. ****************************************************************************** diff --git a/open-vm-tools/lib/include/deployPkg/linuxDeployment.h b/open-vm-tools/lib/include/deployPkg/linuxDeployment.h index ea8b292c6..a145cc364 100644 --- a/open-vm-tools/lib/include/deployPkg/linuxDeployment.h +++ b/open-vm-tools/lib/include/deployPkg/linuxDeployment.h @@ -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); + + + /* *------------------------------------------------------------------------------ * diff --git a/open-vm-tools/libDeployPkg/linuxDeployment.c b/open-vm-tools/libDeployPkg/linuxDeployment.c index b1f9c8dd0..e80599020 100644 --- a/open-vm-tools/libDeployPkg/linuxDeployment.c +++ b/open-vm-tools/libDeployPkg/linuxDeployment.c @@ -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)); diff --git a/open-vm-tools/services/plugins/deployPkg/deployPkg.c b/open-vm-tools/services/plugins/deployPkg/deployPkg.c index 4d70765a5..b1f0324bf 100644 --- a/open-vm-tools/services/plugins/deployPkg/deployPkg.c +++ b/open-vm-tools/services/plugins/deployPkg/deployPkg.c @@ -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)) {