Skip to content

Commit

Permalink
Make Linux perl based customization work with the cloud-init workflow.
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
johnwvmw committed Nov 10, 2022
1 parent f7009c5 commit cd995a5
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 24 deletions.
6 changes: 6 additions & 0 deletions open-vm-tools/lib/include/conf.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
******************************************************************************
Expand Down
21 changes: 20 additions & 1 deletion open-vm-tools/lib/include/deployPkg/linuxDeployment.h
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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);



/*
*------------------------------------------------------------------------------
*
Expand Down
194 changes: 175 additions & 19 deletions open-vm-tools/libDeployPkg/linuxDeployment.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
*
Expand Down Expand Up @@ -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;

// .....................................................................................

Expand Down Expand Up @@ -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);
}

// .....................................................................................

/**
Expand Down Expand Up @@ -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";
Expand All @@ -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
Expand All @@ -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,
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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));
Expand Down
Loading

0 comments on commit cd995a5

Please sign in to comment.