[libvirt] [PATCH 0/2] ESX: Implement virDomainScreenshot

Hello, The following patches implement virDomainScreenshot for the ESX driver. The screenshot VI API call was added in version 4.0, therefore, I had to make changes to esx_vi_generator.py in order to allow VI method calls for versions newer than 2.5. This was done by adding an optional "apiVersion" token to the method header definition in the .input file. If it is not specified, "2.5" will be used so existing VI calls will work "as is" without any code updates. Regards, Dawid Zamirski (2): ESX: Allow method calls for VI version > 2.5 ESX: Implement virDomainScreenshot src/esx/esx_driver.c | 173 +++++++++++++++++++++++++++++++++++++++++ src/esx/esx_vi.c | 26 ++++++- src/esx/esx_vi.h | 2 +- src/esx/esx_vi_generator.input | 19 ++++- src/esx/esx_vi_generator.py | 20 +++-- src/esx/esx_vi_methods.c | 12 +-- 6 files changed, 238 insertions(+), 14 deletions(-) -- 1.9.0

Currently, ESX driver can only issue VI method calls available in version 2.5. To send method calls available in newer versions, a SOAPAction header needs to be set in the following format: 'SOAPAction: "urn:vim25/<version_number>"' This patch modifies the Python code generator to optionally read 'apiVersion' token from .input file which is then passed to the ESX_VI_METHOD macro. If the apiVersion is not specified, "2.5" is passed by default. Finally, the esx_VI_Context_Execute function takes this argument and conditionally sets SOAPAction header, performs CURL request, and then unsets it again so that any subsequest 2.5 call gets 2.5 formatted XML response. In conclusion, this patch allows to make method calls for VI API greater than v2.5 wthout braking existing v2.5 calls. --- src/esx/esx_vi.c | 26 +++++++++++++++++++++++++- src/esx/esx_vi.h | 2 +- src/esx/esx_vi_generator.py | 20 +++++++++++++++----- src/esx/esx_vi_methods.c | 12 +++++++----- 4 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/esx/esx_vi.c b/src/esx/esx_vi.c index 6188139..393a009 100644 --- a/src/esx/esx_vi.c +++ b/src/esx/esx_vi.c @@ -1224,7 +1224,7 @@ esxVI_Context_LookupManagedObjectsByHostSystemIp(esxVI_Context *ctx, int esxVI_Context_Execute(esxVI_Context *ctx, const char *methodName, const char *request, esxVI_Response **response, - esxVI_Occurrence occurrence) + esxVI_Occurrence occurrence, const char *apiVersion) { int result = -1; virBuffer buffer = VIR_BUFFER_INITIALIZER; @@ -1232,6 +1232,8 @@ esxVI_Context_Execute(esxVI_Context *ctx, const char *methodName, char *xpathExpression = NULL; xmlXPathContextPtr xpathContext = NULL; xmlNodePtr responseNode = NULL; + char *versionHeader = NULL; + struct curl_slist *origHeader = NULL; if (!request || !response || *response) { virReportError(VIR_ERR_INTERNAL_ERROR, "%s", _("Invalid argument")); @@ -1251,8 +1253,29 @@ esxVI_Context_Execute(esxVI_Context *ctx, const char *methodName, curl_easy_setopt(ctx->curl->handle, CURLOPT_POSTFIELDS, request); curl_easy_setopt(ctx->curl->handle, CURLOPT_POSTFIELDSIZE, strlen(request)); + if (!STRPREFIX(apiVersion, "2.5")) { + origHeader = ctx->curl->headers; + + virBufferAsprintf(&buffer, "SOAPAction: \"urn:vim25/%s\"", apiVersion); + versionHeader = virBufferContentAndReset(&buffer); + + ctx->curl->headers = curl_slist_append(ctx->curl->headers, + versionHeader); + curl_easy_setopt(ctx->curl->handle, CURLOPT_HTTPHEADER, + ctx->curl->headers); + } + (*response)->responseCode = esxVI_CURL_Perform(ctx->curl, ctx->url); + if (origHeader) { + curl_slist_free_all(origHeader->next); + origHeader->next = NULL; + + ctx->curl->headers = origHeader; + curl_easy_setopt(ctx->curl->handle, CURLOPT_HTTPHEADER, + ctx->curl->headers); + } + virMutexUnlock(&ctx->curl->lock); if ((*response)->responseCode < 0) { @@ -1403,6 +1426,7 @@ esxVI_Context_Execute(esxVI_Context *ctx, const char *methodName, } VIR_FREE(xpathExpression); + VIR_FREE(versionHeader); xmlXPathFreeContext(xpathContext); return result; diff --git a/src/esx/esx_vi.h b/src/esx/esx_vi.h index 7cc2f71..15c2529 100644 --- a/src/esx/esx_vi.h +++ b/src/esx/esx_vi.h @@ -245,7 +245,7 @@ int esxVI_Context_LookupManagedObjectsByHostSystemIp(esxVI_Context *ctx, const char *hostSystemIpAddress); int esxVI_Context_Execute(esxVI_Context *ctx, const char *methodName, const char *request, esxVI_Response **response, - esxVI_Occurrence occurrence); + esxVI_Occurrence occurrence, const char *apiVersion); diff --git a/src/esx/esx_vi_generator.py b/src/esx/esx_vi_generator.py index 0b75f18..f571b36 100755 --- a/src/esx/esx_vi_generator.py +++ b/src/esx/esx_vi_generator.py @@ -188,11 +188,12 @@ class Parameter(Member): class Method: - def __init__(self, name, parameters, returns): + def __init__(self, name, parameters, returns, apiVersion): self.name = name self.parameters = [] self.autobind_parameter = None self.returns = returns + self.apiVersion = apiVersion for parameter in parameters: if parameter.autobind_name is None: @@ -282,7 +283,7 @@ class Method: for parameter in self.parameters: source += parameter.generate_serialize_code() - source += "})\n\n\n\n" + source += "}, %s)\n\n\n\n" % self.apiVersion return source @@ -1276,6 +1277,8 @@ def parse_method(block): # expected format: method <name> [returns <type> <occurrence>] header_items = block[0][1].split() + apiVersion = 2.5 + if len(header_items) < 2: report_error("line %d: invalid block header" % (number)) @@ -1285,11 +1288,18 @@ def parse_method(block): returns = None if len(header_items) > 2: - if header_items[2] != "returns": + if header_items[2] not in ["returns", "apiVersion"]: report_error("line %d: invalid block header" % (number)) - else: + elif header_items[2] == "returns": returns = Parameter(type=header_items[3], name="output", occurrence=header_items[4]) + if len(header_items) > 5: + if header_items[5] != "apiVersion": + report_error("line %d: invalid block header" % (number)) + else: + apiVersion = header_items[6] + else: + apiVersion = header_items[3] parameters = [] @@ -1306,7 +1316,7 @@ def parse_method(block): parameters.append(Parameter(type=items[0], name=items[1], occurrence=items[2])) - return Method(name=name, parameters=parameters, returns=returns) + return Method(name=name, parameters=parameters, returns=returns, apiVersion=apiVersion) diff --git a/src/esx/esx_vi_methods.c b/src/esx/esx_vi_methods.c index 0fdd0cd..f7d30d0 100644 --- a/src/esx/esx_vi_methods.c +++ b/src/esx/esx_vi_methods.c @@ -83,7 +83,6 @@ } - #define ESX_VI__METHOD__DESERIALIZE_OUTPUT__OptionalItem(_type, _suffix) \ if (response->node && \ esxVI_##_type##_Deserialize##_suffix(response->node, output) < 0) { \ @@ -102,12 +101,13 @@ #define ESX_VI__METHOD(_name, _this_from_service, _parameters, _output_type, \ _deserialize_suffix, _occurrence, _validate, \ - _serialize) \ + _serialize, _apiVersion) \ int \ esxVI_##_name _parameters \ { \ int result = -1; \ const char *methodName = #_name; \ + const char *apiVersion = #_apiVersion; \ virBuffer buffer = VIR_BUFFER_INITIALIZER; \ char *request = NULL; \ esxVI_Response *response = NULL; \ @@ -134,7 +134,8 @@ request = virBufferContentAndReset(&buffer); \ \ if (esxVI_Context_Execute(ctx, methodName, request, &response, \ - esxVI_Occurrence_##_occurrence) < 0) { \ + esxVI_Occurrence_##_occurrence, \ + apiVersion) < 0) { \ goto cleanup; \ } \ \ @@ -240,7 +241,8 @@ esxVI_RetrieveServiceContent(esxVI_Context *ctx, } if (esxVI_Context_Execute(ctx, "RetrieveServiceContent", request, - &response, esxVI_Occurrence_RequiredItem) < 0 || + &response, esxVI_Occurrence_RequiredItem, + "2.5") < 0 || esxVI_ServiceContent_Deserialize(response->node, serviceContent) < 0) { goto cleanup; } @@ -279,7 +281,7 @@ ESX_VI__METHOD(ValidateMigration, /* special _this */, ESX_VI__METHOD__PARAMETER__SERIALIZE_LIST(String, testType) ESX_VI__METHOD__PARAMETER__SERIALIZE(ManagedObjectReference, pool) ESX_VI__METHOD__PARAMETER__SERIALIZE(ManagedObjectReference, host) -}) +}, 2.5) -- 1.9.0

2014-03-27 23:24 GMT+01:00 Dawid Zamirski <dzamirski@dattobackup.com>:
Currently, ESX driver can only issue VI method calls available in version 2.5. To send method calls available in newer versions, a SOAPAction header needs to be set in the following format:
'SOAPAction: "urn:vim25/<version_number>"'
This patch modifies the Python code generator to optionally read 'apiVersion' token from .input file which is then passed to the ESX_VI_METHOD macro. If the apiVersion is not specified, "2.5" is passed by default. Finally, the esx_VI_Context_Execute function takes this argument and conditionally sets SOAPAction header, performs CURL request, and then unsets it again so that any subsequest 2.5 call gets 2.5 formatted XML response.
In conclusion, this patch allows to make method calls for VI API greater than v2.5 wthout braking existing v2.5 calls. --- src/esx/esx_vi.c | 26 +++++++++++++++++++++++++- src/esx/esx_vi.h | 2 +- src/esx/esx_vi_generator.py | 20 +++++++++++++++----- src/esx/esx_vi_methods.c | 12 +++++++----- 4 files changed, 48 insertions(+), 12 deletions(-)
diff --git a/src/esx/esx_vi.c b/src/esx/esx_vi.c index 6188139..393a009 100644 --- a/src/esx/esx_vi.c +++ b/src/esx/esx_vi.c
@@ -1251,8 +1253,29 @@ esxVI_Context_Execute(esxVI_Context *ctx, const char *methodName, curl_easy_setopt(ctx->curl->handle, CURLOPT_POSTFIELDS, request); curl_easy_setopt(ctx->curl->handle, CURLOPT_POSTFIELDSIZE, strlen(request));
+ if (!STRPREFIX(apiVersion, "2.5")) { + origHeader = ctx->curl->headers; + + virBufferAsprintf(&buffer, "SOAPAction: \"urn:vim25/%s\"", apiVersion); + versionHeader = virBufferContentAndReset(&buffer); + + ctx->curl->headers = curl_slist_append(ctx->curl->headers, + versionHeader); + curl_easy_setopt(ctx->curl->handle, CURLOPT_HTTPHEADER, + ctx->curl->headers); + } + (*response)->responseCode = esxVI_CURL_Perform(ctx->curl, ctx->url);
+ if (origHeader) { + curl_slist_free_all(origHeader->next); + origHeader->next = NULL; + + ctx->curl->headers = origHeader; + curl_easy_setopt(ctx->curl->handle, CURLOPT_HTTPHEADER, + ctx->curl->headers); + } + virMutexUnlock(&ctx->curl->lock);
This works but is inefficient if you do many calls to non-2.5 functions. I the context of this series this is okay as you're only calling a single non-2.5 function. As a more general approach I would add multiple header lists to the esxVI_CURL struct, one for each API version we want to call. Also the esxVI_CURL struct would keep that of which header list is currently in use. Then esxVI_Context_Execute would only have to change the used header list if the API version is different from the last call. The header lists for the different API version would be created beforehand and not in esxVI_Context_Execute. -- Matthias Bolte http://photron.blogspot.com

This patch implements virDomainScreenshot support for ESX versions newer than 4.0. It adds CreateScreenshot_Task (apiVersion 4.0) and DeleteDatastoreFile_Task (v2.5) then uses those to implement esxDomainScreenshot function. The DeleteDatastoreFile_Task is used to remove the screenshot file that is left on the server after invoking CreateScreenshot_Task call. --- src/esx/esx_driver.c | 173 +++++++++++++++++++++++++++++++++++++++++ src/esx/esx_vi_generator.input | 19 ++++- 2 files changed, 190 insertions(+), 2 deletions(-) diff --git a/src/esx/esx_driver.c b/src/esx/esx_driver.c index ff44881..e825aaf 100644 --- a/src/esx/esx_driver.c +++ b/src/esx/esx_driver.c @@ -22,6 +22,7 @@ */ #include <config.h> +#include <fcntl.h> #include "internal.h" #include "domain_conf.h" @@ -44,6 +45,8 @@ #include "esx_vi.h" #include "esx_vi_methods.h" #include "esx_util.h" +#include "configmake.h" +#include "fdstream.h" #include "virstring.h" #include "viruri.h" @@ -2501,6 +2504,175 @@ esxDomainGetState(virDomainPtr domain, +static char * +esxDomainScreenshot(virDomainPtr domain, + virStreamPtr st ATTRIBUTE_UNUSED, + unsigned int screen ATTRIBUTE_UNUSED, + unsigned int flags) +{ + esxPrivate *priv = domain->conn->privateData; + esxVI_String *propertyNameList = NULL; + esxVI_ObjectContent *virtualMachine = NULL; + esxVI_VirtualMachinePowerState powerState; + esxVI_ManagedObjectReference *task = NULL; + esxVI_TaskInfoState taskInfoState; + char *taskInfoErrorMessage = NULL; + esxVI_TaskInfo *taskInfo = NULL; + virBuffer buffer = VIR_BUFFER_INITIALIZER; + char *screenshotPath = NULL; + char *dataStoreName = NULL; + char *directoryName = NULL; + char *directoryAndFileName = NULL; + char *url = NULL; + char *screenData = NULL; + unsigned long long screenSize = 0; + char *tmp = NULL; + char *tmpdir = NULL; + int tmp_fd = -1; + char *ret = NULL; + bool unlink_tmp = false; + + virCheckFlags(0, NULL); + + if (esxVI_EnsureSession(priv->primary) < 0) { + return NULL; + } + + if (priv->primary->apiVersion < esxVI_APIVersion_40) { + virReportError(VIR_ERR_OPERATION_UNSUPPORTED, + _("Operation not supported in VI API version %s, " + "minimum version is 4.0"), + priv->primary->service->about->apiVersion); + } + + if (esxVI_String_AppendValueToList(&propertyNameList, + "runtime.powerState") < 0 || + esxVI_LookupVirtualMachineByUuidAndPrepareForTask + (priv->primary, domain->uuid, propertyNameList, &virtualMachine, + priv->parsedUri->autoAnswer) < 0 || + esxVI_GetVirtualMachinePowerState(virtualMachine, &powerState) < 0) { + goto cleanup; + } + + if (powerState != esxVI_VirtualMachinePowerState_PoweredOn) { + virReportError(VIR_ERR_OPERATION_INVALID, "%s", + _("Domain is not powered on")); + goto cleanup; + } + + if (esxVI_CreateScreenshot_Task(priv->primary, virtualMachine->obj, + &task) < 0 || + esxVI_WaitForTaskCompletion(priv->primary, task, domain->uuid, + esxVI_Occurrence_RequiredItem, + priv->parsedUri->autoAnswer, &taskInfoState, + &taskInfoErrorMessage) < 0) { + goto cleanup; + } + + if (taskInfoState != esxVI_TaskInfoState_Success) { + virReportError(VIR_ERR_INTERNAL_ERROR, _("Could not create screenshot: %s"), + taskInfoErrorMessage); + goto cleanup; + } + + if (esxVI_LookupTaskInfoByTask(priv->primary, task, &taskInfo) < 0 || + esxVI_String_CastValueFromAnyType(taskInfo->result, &screenshotPath)) { + goto cleanup; + } + + if (esxUtil_ParseDatastorePath(screenshotPath, &dataStoreName, + &directoryName, &directoryAndFileName) < 0) { + goto cleanup; + } + + virBufferAsprintf(&buffer, "%s://%s:%d/folder/", priv->parsedUri->transport, + domain->conn->uri->server, domain->conn->uri->port); + virBufferURIEncodeString(&buffer, directoryAndFileName); + virBufferAddLit(&buffer, "?dcPath="); + virBufferURIEncodeString(&buffer, priv->primary->datacenterPath); + virBufferAddLit(&buffer, "&dsName="); + virBufferURIEncodeString(&buffer, dataStoreName); + + if (virBufferError(&buffer)) { + virReportOOMError(); + goto cleanup; + } + + url = virBufferContentAndReset(&buffer); + + if (esxVI_CURL_Download(priv->primary->curl, url, &screenData, 0, + &screenSize) < 0) { + goto cleanup; + } + + tmpdir = virGetEnvBlockSUID("TMPDIR"); + if (!tmpdir) tmpdir = "/tmp"; + + if (virAsprintf(&tmp, "%s/esx.screendump.XXXXXX", + tmpdir) < 0) { + goto cleanup; + } + + if ((tmp_fd = mkostemp(tmp, O_CLOEXEC)) == -1) { + virReportSystemError(errno, _("mkostemp(\"%s\") failed"), tmp); + goto endjob; + } + unlink_tmp = true; + + if (safewrite(tmp_fd, screenData, screenSize) < 0) { + virReportSystemError(errno, _("unable to write data to '%s'"), tmp); + goto endjob; + } + + if (VIR_CLOSE(tmp_fd) < 0) { + virReportSystemError(errno, _("unable to close %s"), tmp); + goto endjob; + } + + if (virFDStreamOpenFile(st, tmp, 0, 0, O_RDONLY) < 0) { + virReportError(VIR_ERR_OPERATION_FAILED, "%s", + _("unable to open stream")); + goto endjob; + } + + if (VIR_STRDUP(ret, "image/png") < 0) { + goto endjob; + } + + esxVI_ManagedObjectReference_Free(&task); + esxVI_DeleteDatastoreFile_Task(priv->primary, screenshotPath, + priv->primary->datacenter->_reference, &task); + + endjob: + VIR_FORCE_CLOSE(tmp_fd); + + if (unlink_tmp) + unlink(tmp); + + VIR_FREE(tmp); + + cleanup: + if (!url) { + virBufferFreeAndReset(&buffer); + } + + esxVI_String_Free(&propertyNameList); + esxVI_ObjectContent_Free(&virtualMachine); + esxVI_ManagedObjectReference_Free(&task); + esxVI_TaskInfo_Free(&taskInfo); + VIR_FREE(taskInfoErrorMessage); + VIR_FREE(screenshotPath); + VIR_FREE(screenData); + VIR_FREE(dataStoreName); + VIR_FREE(directoryName); + VIR_FREE(directoryAndFileName); + VIR_FREE(url); + + return ret; +} + + + static int esxDomainSetVcpusFlags(virDomainPtr domain, unsigned int nvcpus, unsigned int flags) @@ -5221,6 +5393,7 @@ static virDriver esxDriver = { .domainGetMemoryParameters = esxDomainGetMemoryParameters, /* 0.8.6 */ .domainGetInfo = esxDomainGetInfo, /* 0.7.0 */ .domainGetState = esxDomainGetState, /* 0.9.2 */ + .domainScreenshot = esxDomainScreenshot, /* 1.2.3 */ .domainSetVcpus = esxDomainSetVcpus, /* 0.7.0 */ .domainSetVcpusFlags = esxDomainSetVcpusFlags, /* 0.8.5 */ .domainGetVcpusFlags = esxDomainGetVcpusFlags, /* 0.8.5 */ diff --git a/src/esx/esx_vi_generator.input b/src/esx/esx_vi_generator.input index 22c114e..ebc718a 100644 --- a/src/esx/esx_vi_generator.input +++ b/src/esx/esx_vi_generator.input @@ -1,5 +1,5 @@ # -# Definitions of vSphere API 2.5 enumeration and objects types used as input +# Definitions of vSphere API enumeration and objects types used as input # for the esx_vi_generator.py script. # # This format is line-based, so end-of-line is important. @@ -33,11 +33,13 @@ # # Method definition: # -# method <name> [returns <type> <occurrence>] +# method <name> [returns <type> <occurrence>] [apiVersion <version>] # <type> <name> <occurrence> # ... # end # +# If apiVersion is not specified, it defaults to 2.5 +# # The _this parameter can have a type attached to it: # # _this:<member> @@ -1317,6 +1319,11 @@ method CreateFilter returns ManagedObjectReference r end +method CreateScreenshot_Task returns ManagedObjectReference r apiVersion 4.0 + ManagedObjectReference _this r +end + + method CreateSnapshot_Task returns ManagedObjectReference r ManagedObjectReference _this r String name r @@ -1376,6 +1383,14 @@ method Logout end +method DeleteDatastoreFile_Task returns ManagedObjectReference r + ManagedObjectReference _this:fileManager r + String name r + ManagedObjectReference datacenter o +end + + + method MakeDirectory ManagedObjectReference _this:fileManager r String name r -- 1.9.0

2014-03-27 23:24 GMT+01:00 Dawid Zamirski <dzamirski@dattobackup.com>:
Hello,
The following patches implement virDomainScreenshot for the ESX driver. The screenshot VI API call was added in version 4.0, therefore, I had to make changes to esx_vi_generator.py in order to allow VI method calls for versions newer than 2.5. This was done by adding an optional "apiVersion" token to the method header definition in the .input file. If it is not specified, "2.5" will be used so existing VI calls will work "as is" without any code updates.
Hi, let me start this with saying sorry... I basically had a working virDomainScreenshot implementation for ESX sitting on my harddrive for 2 years by now, but I never managed to post it for review. I posted it now [1]. As I mentioned in the other thread I think your implementation has two disadvantages: using a temporary file and requiring ESX 4.0. Actually the 4.0 requirement is not that bad, as 4.0 is already somewhat old again, but if it can be avoid I'd like to do that. But I like the way you handled the SOAPAction header problem and will review that in more detail. [1] https://www.redhat.com/archives/libvir-list/2014-March/msg01915.html -- Matthias Bolte http://photron.blogspot.com
participants (2)
-
Dawid Zamirski
-
Matthias Bolte