[iwhd patch] Add registration in vSphere

Pete Zaitcev zaitcev at redhat.com
Wed Jun 1 01:11:26 UTC 2011


This patch adds yet another cloud type: VMware vSphere. The RPC calls are
hand-rolled with libxml2 and libcurl, so new new dependencies are necessary.
Documentation is updated.

The implementation is not entirely clean. Two major known issues are:
 - this is not endian clean
 - reported completion percentages are fake

However, it is desired to get something working into hands of Aeolus
people as soon as possible. The issues are to be addressed.

---
 .gitignore           |    1 
 Makefile.am          |    9 
 backend.c            |  256 +++-
 dc-rhev-image.c      |    1 
 dc-vmware-image.c    | 2530 +++++++++++++++++++++++++++++++++++++++++
 doc/registrations.md |   30 
 setup.c              |    6 
 7 files changed, 2816 insertions(+), 17 deletions(-)

diff --git a/.gitignore b/.gitignore
index 297315d..0594918 100644
--- a/.gitignore
+++ b/.gitignore
@@ -16,6 +16,7 @@
 /config.status
 /configure
 /dc-rhev-image
+/dc-vmware-image
 /gnulib-tests
 /iwhd
 /iwhd-*.tar.gz
diff --git a/Makefile.am b/Makefile.am
index 76a1c64..1b6b5f8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -21,7 +21,7 @@ SUBDIRS = lib . gnulib-tests t man po
 ACLOCAL_AMFLAGS = -I m4
 
 # iwhd is short for Image WareHouse Daemon.
-bin_PROGRAMS = iwhd dc-rhev-image
+bin_PROGRAMS = iwhd dc-rhev-image dc-vmware-image
 
 EXTRA_DIST =		\
   dc-condor-image	\
@@ -62,6 +62,7 @@ noinst_HEADERS = \
 EXTRA_iwhd_SOURCES = qlexer.l
 
 dc_rhev_image_SOURCES = dc-rhev-image.c
+dc_vmware_image_SOURCES = dc-vmware-image.c
 
 VERSION_no_hyphen = $$(echo $(VERSION)|tr - _)
 
@@ -157,6 +158,12 @@ dc_rhev_image_LDADD =	\
   $(LIBXML_LIBS)	\
   $(UUID_LIB)
 
+dc_vmware_image_CPPFLAGS = $(LIBXML_CFLAGS) -I$(top_srcdir)/lib
+dc_vmware_image_LDADD =	\
+  lib/libiwhd.a		\
+  $(CURL_LIB)		\
+  $(LIBXML_LIBS)
+
 MOSTLYCLEANFILES += qlexer.c
 MAINTAINERCLEANFILES += qlexer.c
 EXTRA_DIST += qlexer.c
diff --git a/backend.c b/backend.c
index 10f6bf9..80418bf 100644
--- a/backend.c
+++ b/backend.c
@@ -1344,6 +1344,76 @@ fs_bcreate (const provider_t *prov, const char *bucket)
 	return MHD_HTTP_OK;
 }
 
+static char *json_escape(const char *str)
+{
+	char *escaped_str, *p;
+	int cnt;
+	int i;
+
+	cnt = 0;
+	for (i = 0; str[i]; i++) {
+		if (str[i] == '\\' || str[i] == '"')
+			cnt++;
+		cnt++;
+	}
+	escaped_str = malloc(cnt+1);
+	if (!escaped_str)
+		return NULL;
+	p = escaped_str;
+	for (i = 0; str[i]; i++) {
+		if (str[i] == '\\' || str[i] == '"')
+			*p++ = '\\';
+		*p++ = str[i];
+	}
+	*p++ = '\0';
+	return escaped_str;
+}
+
+static char *
+fs_rhevm_genconf(const char *bucket, const char *key, const char *api_url,
+	const char *api_user, const char *api_secret,
+	const char *nfs_host, const char *nfs_path, const char *nfs_dir)
+{
+	int rc;
+	char *b, *k, *url, *user, *pass, *nhost, *npath, *ndir;
+	char *ret = NULL;
+
+	b = json_escape(bucket);
+	k = json_escape(key);
+	url = json_escape(api_url);
+	user = json_escape(api_user);
+	pass = json_escape(api_secret);
+	nhost = json_escape(nfs_host);
+	npath = json_escape(nfs_path);
+	ndir = json_escape(nfs_dir);
+	if (!b || !k || !url || !user || !pass || !nhost || !npath || !ndir)
+		goto err;
+
+	rc = asprintf(&ret,
+		"{\n"
+		"  \"image\"   : \"%s/%s\",\n"
+		"  \"apiurl\"  : \"%s\",\n"
+		"  \"apiuser\" : \"%s\",\n"
+		"  \"apipass\" : \"%s\",\n"
+		"  \"nfshost\" : \"%s\",\n"
+		"  \"nfspath\" : \"%s\",\n"
+		"  \"nfsdir\" :  \"%s\"\n"
+		"}\n",
+		b, k, url, user, pass, nhost, npath, ndir);
+	if (rc < 0)
+		ret = NULL;	/* for undefined behaviour in asprintf() */
+ err:
+	free(ndir);
+	free(npath);
+	free(nhost);
+	free(pass);
+	free(user);
+	free(url);
+	free(k);
+	free(b);
+	return ret;
+}
+
 static int
 fs_rhevm_register (my_state *ms, const provider_t *prov, const char *next,
 		   Hash_table *args)
@@ -1367,7 +1437,6 @@ fs_rhevm_register (my_state *ms, const provider_t *prov, const char *next,
 	char		*s;
 	char		 ami_id_buf[64];
 	regmatch_t	 match[2];
-	int		 rc;
 
 	strcpy(ami_id_buf, "none");
 
@@ -1449,21 +1518,10 @@ fs_rhevm_register (my_state *ms, const provider_t *prov, const char *next,
 
 	ret = MHD_HTTP_INTERNAL_SERVER_ERROR;
 
-	rc = asprintf(&conf_text,
-		    "{\n"
-		    "  \"image\"   : \"%s/%s\",\n"
-		    "  \"apiurl\"  : \"%s\",\n"
-		    "  \"apiuser\" : \"%s\",\n"
-		    "  \"apipass\" : \"%s\",\n"
-		    "  \"nfshost\" : \"%s\",\n"
-		    "  \"nfspath\" : \"%s\",\n"
-		    "  \"nfsdir\" :  \"%s\"\n"
-		    "}\n",
-			ms->bucket, ms->key,
+	conf_text = fs_rhevm_genconf(ms->bucket, ms->key,
 			api_url, api_user, api_secret,
-			nfs_host, nfs_path, nfs_dir
-	    );
-	if (rc < 0) {
+			nfs_host, nfs_path, nfs_dir);
+	if (!conf_text) {
 		error (0, 0, _("no core"));
 		goto cleanup;
 	}
@@ -1697,6 +1755,163 @@ cleanup:
 	return rc;
 }
 
+static int
+fs_vmw_register (my_state *ms, const provider_t *prov, const char *next,
+		 Hash_table *args)
+{
+	char		*api_url;
+	char		*api_user;
+	char		*api_secret;
+	char		*vm_name;
+	char		*vm_host;
+	char		*vm_image = NULL;	/* filename */
+	int		 ret	= MHD_HTTP_BAD_REQUEST;
+	const char	*argv[12];
+	unsigned int	 argc = 0;
+	pid_t		 pid;
+	int		 organ[2];
+	int		 wstat;
+	FILE		*fp;
+	char		 buf[LINE_SIZE];
+	char		*s;
+	char		 ami_id_buf[64];
+	regmatch_t	 match[2];
+
+	strcpy(ami_id_buf, "none");
+
+	if (!regex_ok) {
+		return MHD_HTTP_BAD_REQUEST;
+	}
+
+	DPRINTF("*** vSphere register %s/%s via %s (%s:%d)\n",
+		ms->bucket, ms->key, prov->name, prov->host, prov->port);
+	if (next) {
+		DPRINTF("RHEV-M register with next!=NULL\n");
+		return MHD_HTTP_BAD_REQUEST;
+	}
+
+	if (asprintf(&vm_image, "%s/%s", ms->bucket, ms->key) < 0)
+		return MHD_HTTP_BAD_REQUEST;
+
+	api_url = kv_hash_lookup(args,"api-url");
+	if (!api_url) {
+		/* TBD: This has to have a defined field in provider, too. */
+		error (0, 0, _("missing vSphere API URL (api-url)"));
+		goto cleanup;
+	}
+
+	api_user = kv_hash_lookup(args,"api-key");
+	if (!api_user) {
+		api_user = (char *)prov->username;
+		if (!api_user) {
+			error (0, 0, _("missing vSphere API username (api-key)"));
+			goto cleanup;
+		}
+	}
+
+	api_secret = kv_hash_lookup(args,"api-secret");
+	if (!api_secret) {
+		api_secret = (char *)prov->password;
+		if (!prov->password) {
+			error (0, 0, _("missing vSphere API secret (api-secret)"));
+			goto cleanup;
+		}
+	}
+
+	vm_name = kv_hash_lookup(args,"vm-name");
+	if (!vm_name) {
+		/* Not sure if this is great default, but it's convenient. */
+		vm_name = ms->key;
+	}
+
+	vm_host = kv_hash_lookup(args,"vm-host");
+	if (!vm_host) {
+		error (0, 0, _("missing ESXi host (vm-host)"));
+		goto cleanup;
+	}
+
+	ret = MHD_HTTP_INTERNAL_SERVER_ERROR;
+
+	sprintf(ami_id_buf,"pending %lld",(long long)time(NULL));
+	DPRINTF("temporary ami-id = \"%s\"\n",ami_id_buf);
+	(void)meta_set_value(ms->bucket,ms->key,"ami-id",ami_id_buf);
+
+	const char *cmd = "dc-vmware-image";
+	argv[argc++] = cmd;
+	argv[argc++] = api_url;
+	argv[argc++] = api_user;
+	argv[argc++] = api_secret;	/* XXX Hide in a file again. */
+	argv[argc++] = vm_name;
+	argv[argc++] = vm_image;
+	argv[argc++] = vm_host;
+	argv[argc++] = "-";
+	argv[argc] = NULL;
+	assert (argc < ARRAY_CARDINALITY (argv));
+
+	if (pipe(organ) < 0) {
+		error (0, errno, _("pipe creation failed"));
+		goto cleanup;
+	}
+
+	pid = fork();
+	if (pid < 0) {
+		error (0, errno, _("fork failed"));
+		close(organ[0]);
+		close(organ[1]);
+		goto cleanup;
+	}
+
+	if (pid == 0) {
+		(void)dup2(organ[1],STDOUT_FILENO);
+		(void)dup2(organ[1],STDERR_FILENO);
+		execvp(cmd, (char* const*)argv);
+		error (EXIT_FAILURE, errno, _("failed to run command %s"), cmd);
+	}
+
+	close(organ[1]);
+	fp = fdopen(organ[0],"r");
+	if (!fp) {
+		error (0, 0, _("could not open parent pipe stream"));
+		close(organ[0]);
+		goto cleanup;
+	}
+	while (fgets(buf,sizeof(buf)-1,fp)) {
+		if ((s = strchr(buf, '\n')) != NULL)
+			*s = '\0';
+		if (regexec(&s3_success_pat,buf,2,match,0) == 0) {
+			buf[match[1].rm_eo] = '\0';
+			DPRINTF("found image tag: %s\n",buf+match[1].rm_so);
+			sprintf(ami_id_buf,"OK %.60s",buf+match[1].rm_so);
+			ret = MHD_HTTP_OK;
+		}
+		else {
+			DPRINTF("ignoring line: <%s>\n",buf);
+		}
+	}
+	fclose(fp);
+
+	DPRINTF("waiting for child...\n");
+	if (waitpid(pid,&wstat,0) < 0) {
+		error (0, errno, _("waitpid failed"));
+		goto cleanup;
+	}
+	if (!WIFEXITED(wstat)) {
+		error (0, 0, _("%s is killed (status 0x%x)"), cmd, wstat);
+		goto cleanup;
+	}
+	if (WEXITSTATUS(wstat)) {
+		error (0, 0, _("%s exited with code %d"),
+		       cmd, WEXITSTATUS(wstat));
+		goto cleanup;
+	}
+	DPRINTF("...child exited\n");
+
+cleanup:
+	free(vm_image);
+	(void)meta_set_value(ms->bucket,ms->key,"ami-id",ami_id_buf);
+	return ret;
+}
+
 /***** Function tables. ****/
 
 backend_func_tbl bad_func_tbl = {
@@ -1775,3 +1990,14 @@ backend_func_tbl fs_condor_func_tbl = {
 	fs_bcreate,
 	fs_condor_register,
 };
+
+backend_func_tbl fs_vmw_func_tbl = {
+	"FS-VMW",
+	fs_init,
+	fs_get_child,
+	fs_put_child,
+	bad_cache_child,
+	fs_delete,
+	fs_bcreate,
+	fs_vmw_register,
+};
diff --git a/dc-rhev-image.c b/dc-rhev-image.c
index 84db216..bc76f66 100644
--- a/dc-rhev-image.c
+++ b/dc-rhev-image.c
@@ -989,6 +989,7 @@ static void spitovf(struct config *cfg, struct stor_dom *sd,
 		    tmpovf);
 		exit(EXIT_FAILURE);
 	}
+	xmlTextWriterSetIndent(writer, 1);
 
 	rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL);
 	if (rc < 0) {
diff --git a/dc-vmware-image.c b/dc-vmware-image.c
new file mode 100644
index 0000000..543751e
--- /dev/null
+++ b/dc-vmware-image.c
@@ -0,0 +1,2530 @@
+/*
+ * dc-vmware-image: register an image in vSphere
+ * Copyright (C) 2011 Red Hat, Inc.
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+#include <config.h>
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <sys/wait.h>
+
+#include <curl/curl.h>
+#include <libxml/parser.h>
+#include <libxml/tree.h>
+#include <libxml/encoding.h>
+#include <libxml/xmlwriter.h>
+#include <zlib.h>
+
+#define TAG "dc-vmware-image"
+
+/*
+ * These C structures were officially documented in VMDK specification.
+ * We see no reason to roll our own.
+ */
+typedef uint64_t SectorType;
+typedef unsigned char Bool;
+
+struct MetaDataMarker {
+	SectorType val;
+	uint32_t size;
+	uint32_t type;
+	unsigned char pad[496];
+	unsigned char metadata[0];
+};
+
+#define MARKER_EOS	0
+#define MARKER_GT	1
+#define MARKER_GD	2
+#define MARKER_FOOTER	3
+
+struct GrainMarker {
+	SectorType val;
+	uint32_t size;
+} __attribute__((__packed__));
+
+struct SparseExtentHeader {
+	uint32_t	magicNumber;
+	uint32_t	version;
+	uint32_t	flags;
+	SectorType	capacity;
+	SectorType	grainSize;
+	SectorType	descriptorOffset;
+	SectorType	descriptorSize;
+	uint32_t	numGTEsPerGT;
+	SectorType	rgdOffset;
+	SectorType	gdOffset;
+	SectorType	overHead;
+	Bool		uncleanShutdown;
+	char		singleEndLineChar;
+	char		nonEndLineChar;
+	char		doubleEndLineChar1;
+	char		doubleEndLineChar2;
+	uint16_t	compressAlgorithm;
+	unsigned char pad[433];
+} __attribute__((__packed__));
+
+#define SPARSE_MAGICNUMBER 0x564d444b /* 'V' 'M' 'D' 'K' */
+
+#define COMPRESSION_DEFLATE 1
+
+#define GD_AT_END  0xffffffffffffffff
+
+/*
+ */
+struct config {
+	char *apiurl;
+	char *apiuser;
+	char *apipass;
+
+	char *push_name;
+	char *push_img;
+	char *push_host;
+	char *push_tmp;
+};
+
+struct session {
+	CURL *curl;
+	struct curl_slist *curlhdrs;
+};
+
+struct api_buf {
+	char *buf;
+	size_t alloc;
+	size_t used;	/* a rolling count within [alloc], actually */
+};
+
+enum v__HttpNfcLeaseState {
+	v__HttpNfcLeaseState__initializing = 0,
+	v__HttpNfcLeaseState__ready = 1,
+	v__HttpNfcLeaseState__done = 2,
+	v__HttpNfcLeaseState__error = 3
+};
+
+struct v__ServiceContent {
+	xmlDoc *scdoc;
+	xmlNode *scnode;
+};
+
+struct v__ManagedObjectReference {
+	char *type;
+	char *__item;
+};
+
+struct v_prop {
+	// char *name;
+	xmlNode *value;
+};
+
+struct v__OvfCreateImportSpecResult {
+	xmlDoc *doc;
+	xmlNode *node;
+};
+
+struct v_lease {
+	struct v__ManagedObjectReference *mor;
+	// xmlNode *info;	/* can save in v_lease_parse_info() */
+	char *url;		/* upload URL for our only VMDK */
+};
+
+struct v_rpc_arg {
+	const char *type;
+	const char *value;
+};
+
+static void Usage(void)
+{
+	fprintf(stderr, "Usage: " TAG " apiurl apiuser apipass"
+	    " vmname imgfile vmhost tmpname\n");
+	fprintf(stderr, " apiurl:  vSphere URL - https://vsphere.virt.bos.redhat.com/sdk\n");
+	fprintf(stderr, " apiuser: vSphere login - Administrator at virt.lab.eng.bos.redhat.com\n");
+	fprintf(stderr, " apipass: vSphere password - passw0rd\n");
+	fprintf(stderr, " vmname:  desired VM name - pushtest\n");
+	fprintf(stderr, " imgfile: image file name - ../pushtest.img\n");
+	fprintf(stderr, " vmhost:  selected host - virtlab110.virt.bos.redhat.com\n");
+	fprintf(stderr, " tmpname: temporary file or \"-\" for pipe\n");
+	exit(1);
+}
+
+#if 0
+static void fdump(char *fname, void *buf, size_t len)
+{
+	FILE *fp;
+
+	if ((fp = fopen(fname, "w")) == NULL) {
+		fprintf(stderr, TAG ": cannot open %s\n", fname);
+		exit(1);
+	}
+	fwrite(buf, 1, len, fp);
+	if (ferror(fp)) {
+		fprintf(stderr, TAG ": cannot write %s\n", fname);
+		exit(1);
+	}
+	fclose(fp);
+}
+#endif
+
+#if 0
+static void x_node_print(xmlNode *etroot)
+{
+	xmlNode *et;
+
+	printf("<%s>\n", etroot->name);
+	for (et = etroot->children; et; et = et->next) {
+		if (et->type == XML_ELEMENT_NODE) {
+			printf("  <%s>\n", et->name);
+		} else if (et->type == XML_TEXT_NODE) {
+			printf("  `%s'\n", et->content);
+		}
+	}
+}
+#endif
+
+static struct v__ServiceContent *sc_alloc(void)
+{
+	struct v__ServiceContent *sc;
+	sc = malloc(sizeof(struct v__ServiceContent));
+	if (!sc)
+		return NULL;
+	memset(sc, 0, sizeof(struct v__ServiceContent));
+	return sc;
+}
+
+static void sc_free(struct v__ServiceContent *sc)
+{
+	xmlFreeDoc(sc->scdoc);
+	// sc->scnode should be freed by freeing the doc, we hope.
+	free(sc);
+}
+
+#if 0
+static char *x_strdup(const char *val)
+{
+	char *ret;
+
+	ret = strdup(val);
+	if (!ret)
+		goto err_alloc;
+	return ret;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+#endif
+
+static char *x_gettext(xmlNode *et)
+{
+	xmlNode *retval;
+	char *ret;
+
+	retval = et->children;
+	if (!retval || retval->type!=XML_TEXT_NODE || !retval->content)
+		goto err_notext_retval;
+
+	ret = strdup((const char *)retval->content);
+	if (!ret)
+		goto err_alloc;
+	return ret;
+
+ err_notext_retval:
+	fprintf(stderr, TAG ": No text in node `%s'\n", et->name);
+	exit(1);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static struct v_prop *prop_new(xmlNode *etval)
+{
+	struct v_prop *ret;
+
+	ret = malloc(sizeof(struct v_prop));
+	if (!ret)
+		goto err_alloc;
+	ret->value = xmlCopyNode(etval, 1);
+	if (!ret->value)
+		goto err_alloc;
+	return ret;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static void prop_free(struct v_prop *p)
+{
+	xmlFreeNode(p->value);
+	free(p);
+}
+
+static int api_dbcb(CURL *curl, curl_infotype type, char *text, size_t size,
+    void *arg)
+{
+	enum { IN_TEXT, IN_EOL } state;
+	const char *tag;
+	const char *s, *line;
+	char *p;
+
+	switch (type) {
+	case CURLINFO_TEXT:
+		tag = "**";
+		break;
+	case CURLINFO_HEADER_IN:
+		tag = "<<";
+		break;
+	case CURLINFO_HEADER_OUT:
+		tag = ">>";
+		break;
+	default:	/* ignoring data */
+#if 1
+		p = strndup(text, size);
+		if (p) {
+			fprintf(stderr, "== %s\n", p);
+			free(p);
+		}
+#endif
+		return 0;
+	}
+
+	state = IN_TEXT;
+	line = text;
+	for (s = text; s < text + size && *s != 0; s++) {
+		if (state == IN_TEXT) {
+			if (*s == '\r' || *s == '\n') {
+				p = strndup(line, s-line);
+				if (p) {
+					fprintf(stderr, "%s %s\n", tag, p);
+					free(p);
+				}
+				state = IN_EOL;
+			}
+		} else {
+			if (*s != '\r' && *s != '\n') {
+				state = IN_TEXT;
+				line = s;
+			}
+		}
+	}
+	if (state == IN_TEXT && s != line) {
+		p = strndup(line, s-line);
+		if (p) {
+			fprintf(stderr, "%s %s\n", tag, p);
+			free(p);
+		}
+	}
+	return 0;
+}
+
+static size_t api_wcb(void *ptr, size_t bsz, size_t nmemb, void *arg)
+{
+	struct api_buf *bp = arg;
+	char *mem;
+	size_t len;
+
+	if (bp->alloc - bp->used < nmemb) {
+		len = (((bp->used + nmemb) / 2000) + 1) * 2000;
+		mem = realloc(bp->buf, len);
+		if (!mem)
+			return 0;
+		bp->buf = mem;
+		bp->alloc = len;
+	}
+	memcpy(bp->buf + bp->used, ptr, nmemb);
+	bp->used += nmemb;
+	return nmemb;
+}
+
+static size_t api_rcb(void *ptr, size_t bsz, size_t nmemb, void *arg)
+{
+	struct api_buf *bp = arg;
+	size_t count;
+
+	count = bp->alloc - bp->used;
+	if (count > nmemb)
+		count = nmemb;
+	if (count) {
+		memcpy(ptr, bp->buf + bp->used, count);
+		bp->used += count;
+	}
+	return count;
+}
+
+static void cfg_parse(struct config *cfg, int argc, char **argv)
+{
+
+	memset(cfg, 0, sizeof(struct config));
+
+	if (argc != 8)
+		Usage();
+	cfg->apiurl = argv[1];
+	cfg->apiuser = argv[2];
+	cfg->apipass = argv[3];
+	cfg->push_name = argv[4];	// "pushtest";
+	cfg->push_img = argv[5];	// may need to basename
+	cfg->push_host = argv[6];	// "virtlab110.virt.bos.redhat.com"
+	if (argv[7][0] != '-') {
+		/* Use "-" for a pipe */
+		cfg->push_tmp = argv[7];
+	}
+}
+
+static bool curlx_add_header(struct curl_slist **headers, const char *hdr)
+{
+	struct curl_slist *h;
+
+	h = curl_slist_append(*headers, hdr);
+	if (!h)
+		return false;
+	*headers = h;
+	return true;
+}
+
+/* No idea why DV never implemented this, but implemented xmlGetProp. */
+static xmlNode *xmlGetChild(xmlNode *etroot, const char *name)
+{
+	xmlNode *et;
+
+	for (et = etroot->children; et; et = et->next) {
+		if (et->type != XML_ELEMENT_NODE)
+			continue;
+		if (strcmp((const char *)et->name, name) != 0)
+			continue;
+		return et;
+	}
+	return NULL;
+}
+
+static xmlNode *x_getchild(xmlNode *etroot, const char *name)
+{
+	xmlNode *et;
+	const xmlChar *p;
+
+	et = xmlGetChild(etroot, name);
+	if (!et) {
+		p = xmlGetNodePath(etroot);
+		if (!p)
+			p = etroot->name;
+		fprintf(stderr, TAG ": No child `%s' in node `%s'\n", name, p);
+		exit(1);
+	}
+	return et;
+}
+
+static xmlNode *x_addchild(xmlDoc *doc, xmlNs *ns, xmlNode *parent,
+    const char *type, const char *value)
+{
+	xmlNode *child;
+
+	child = xmlNewDocNode(doc, ns, BAD_CAST type, BAD_CAST value);
+	if (!child)
+		goto err_alloc;
+	xmlAddChild(parent, child);
+	return child;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(EXIT_FAILURE);
+}
+
+static xmlNode *x_addchild_1prop(xmlDoc *doc, xmlNs *ns, xmlNode *parent,
+    const char *type, const char *value,
+    const char *pr1_name, const char *pr1_value)
+{
+	xmlNode *child;
+
+	child = xmlNewDocNode(doc, ns, BAD_CAST type, BAD_CAST value);
+	if (!child)
+		goto err_alloc;
+	if (!xmlNewProp(child, BAD_CAST pr1_name, BAD_CAST pr1_value))
+		goto err_alloc;
+	xmlAddChild(parent, child);
+	return child;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(EXIT_FAILURE);
+}
+
+static void mor_free(struct v__ManagedObjectReference *p)
+{
+	free(p->type);
+	free(p->__item);
+	free(p);
+}
+
+static struct v__ManagedObjectReference *mor_new(const char *type,
+    const xmlChar *val)
+{
+	struct v__ManagedObjectReference *p;
+
+	p = malloc(sizeof(struct v__ManagedObjectReference));
+	if (!p)
+		goto err_struc;
+	p->type = strdup(type);
+	if (!p->type)
+		goto err_type;
+	p->__item = strdup((const char *)val);
+	if (!p->__item)
+		goto err_item;
+	return p;
+
+ err_item:
+	free(p->type);
+ err_type:
+	free(p);
+ err_struc:
+	return NULL;
+}
+
+/*
+ * The propval can contain several objects, we return one by type.
+ */
+static struct v__ManagedObjectReference *val2mor(xmlNode *propval,
+    const char *type_match)
+{
+	xmlNode *etmor;
+	xmlNode *etval;
+	xmlChar *type;
+	struct v__ManagedObjectReference *ret;
+
+	for (etmor = propval->children; etmor; etmor = etmor->next) {
+		if (etmor->type != XML_ELEMENT_NODE)
+			continue;
+		if (strcmp((char *)etmor->name, "ManagedObjectReference") != 0)
+			continue;
+		type = xmlGetProp(etmor, (xmlChar *)"type");
+		if (!type)
+			continue;
+		if (strcmp((char *)type, type_match) != 0) {
+			xmlFree(type);
+			continue;
+		}
+
+		etval = etmor->children;
+		if (!etval || etval->type != XML_TEXT_NODE || !etval->content) {
+			fprintf(stderr, TAG ": empty MOR of `%s' in `%s'\n",
+			    type_match, propval->name);
+			exit(1);
+		}
+
+		ret = mor_new((const char *)type, etval->content);
+		xmlFree(type);
+		break;
+	}
+
+	if (!etmor) {
+		fprintf(stderr, TAG ": no MOR of `%s' in `%s'\n",
+		    type_match, propval->name);
+		exit(1);
+	}
+
+	return ret;
+}
+
+static struct v__ManagedObjectReference *text2mor(const char *type,
+    xmlNode *et)
+{
+	xmlNode *retval;
+	struct v__ManagedObjectReference *p;
+
+	retval = et->children;
+	if (!retval || retval->type!=XML_TEXT_NODE || !retval->content)
+		goto err_notext_retval;
+
+	p = mor_new(type, retval->content);
+	if (!p)
+		goto err_malloc;
+	return p;
+
+ err_notext_retval:
+	fprintf(stderr, TAG ": No text in node `%s'\n", et->name);
+	exit(1);
+
+ err_malloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+	// return NULL;
+}
+
+static void x_genxmlhdr(xmlDoc **pdoc, xmlNs **pe, xmlNs **pv, xmlNode **proot)
+{
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot;
+
+	/*
+	 * N.B. Unused namespaces xsi and xsd are required to be present.
+	 */
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	if (!reqdoc)
+		goto err_alloc;
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	if (!etroot)
+		goto err_alloc;
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	if (!ns_env)
+		goto err_alloc;
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	if (!ns_enc)
+		goto err_alloc;
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	if (!ns_xsi)
+		goto err_alloc;
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	if (!ns_xsd)
+		goto err_alloc;
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	if (!ns_v)
+		goto err_alloc;
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	*pdoc = reqdoc;
+	*pe = ns_env;
+	*pv = ns_v;
+	*proot = etroot;
+	return;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(EXIT_FAILURE);
+}
+
+static xmlDoc *v_runcurl(struct config *cfg, struct session *ses,
+    const unsigned char *xmlbuf, int xmllen, const char *proc)
+{
+	CURL *curl = ses->curl;
+	struct api_buf apib, apob;
+	xmlDoc *respdoc;
+	CURLcode rcc;
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) xmlbuf;
+	apob.alloc = xmllen;
+
+	memset(&apib, 0, sizeof(struct api_buf));
+	apib.buf = malloc(4000);
+	if (!apib.buf)
+		goto err_alloc;
+	apib.alloc = 4000;
+
+	// curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, api_dbcb);
+	/* if (verbose)
+		curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); */
+	// curl_easy_setopt(curl, CURLOPT_HTTPGET, 1);
+
+	curl_easy_setopt(curl, CURLOPT_URL, cfg->apiurl);
+	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, ses->curlhdrs);
+	curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1);
+	curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, api_wcb);
+	curl_easy_setopt(curl, CURLOPT_WRITEDATA, &apib);
+	curl_easy_setopt(curl, CURLOPT_POST, 1);
+	curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, apob.alloc);
+	curl_easy_setopt(curl, CURLOPT_READFUNCTION, api_rcb);
+	curl_easy_setopt(curl, CURLOPT_READDATA, &apob);
+
+	rcc = curl_easy_perform(curl);
+	if (rcc != CURLE_OK) {
+		fprintf(stderr, TAG ": curl failed POST url `%s', proc %s\n",
+		     cfg->apiurl, proc);
+		exit(EXIT_FAILURE);
+	}
+/* P3 */ /* fdump("out.spec", apib.buf, apib.used); */
+
+	respdoc = xmlReadMemory(apib.buf, apib.used, "rhev-api.xml", NULL, 0);
+	if (respdoc == NULL) {
+		fprintf(stderr, TAG ": XML parse error, proc %s\n", proc);
+		exit(EXIT_FAILURE);
+	}
+
+	free(apib.buf);
+	return respdoc;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core (%s)\n", proc);
+	exit(EXIT_FAILURE);
+}
+
+/*
+ * The general VMware call.
+ * Takes a list of arguments and returns the reply node (document implied).
+ * This means that the ret->doc has to be freed, not just ret.
+ */
+static xmlNode *v_rpc(struct config *cfg, struct session *ses,
+    const char *proc, int argc, struct v_rpc_arg argv[])
+{
+	char *reply;
+	xmlChar *buf;
+	int len;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlDoc *respdoc;
+	xmlNode *etreply;
+	xmlNode *et;
+	int i;
+
+	if (asprintf(&reply, "%sResponse", proc) < 0)
+		goto err_alloc;
+
+	x_genxmlhdr(&reqdoc, &ns_env, &ns_v, &etroot);
+
+	etbody = x_addchild(reqdoc, ns_env, etroot, "Body", NULL);
+	etproc = x_addchild(reqdoc, ns_v, etbody, proc, NULL);
+	if (argc) {
+		/*
+		 * The value of "_this" is an unpacked MOR string, and "type"
+		 * is the type of the object that MOR represents. gSOAP posts 
+		 * also xsi:type="ManagedObjectReference", but VMware eats
+		 * the argument without, so we don't.
+		 */
+		x_addchild_1prop(reqdoc, ns_v, etproc, "_this", argv[0].value,
+		    "type", argv[0].type);
+	}
+	for (i = 1; i < argc; i++)
+		x_addchild(reqdoc, ns_v, etproc, argv[i].type, argv[i].value);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	respdoc = v_runcurl(cfg, ses, buf, len, proc);
+
+	et = xmlDocGetRootElement(respdoc);
+	if (strcmp((const char *)et->name, "Envelope") != 0) {
+		fprintf(stderr, TAG ": API root is `%s', proc %s\n",
+		    et->name, proc);
+		exit(EXIT_FAILURE);
+	}
+
+	et = x_getchild(et, "Body");
+	etreply = x_getchild(et, reply);
+
+	/* Not entirely clear what this is for. */
+	// xmlCleanupParser();
+
+	// xmlFreeNs(ns_v) -- does freeing Doc free namespaces? XXX
+
+	if (respdoc != etreply->doc) {
+		fprintf(stderr, TAG ": IE, inconsistent XML tree (%s)\n", proc);
+		exit(1);
+	}
+	// xmlFreeDoc(respdoc);
+
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(reply);
+	return etreply;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core (%s)\n", proc);
+	exit(1);
+}
+
+static struct v__ServiceContent *poke_svcroot(struct config *cfg,
+   struct session *ses)
+{
+	struct v_rpc_arg argv[1];
+	struct v__ServiceContent *sc;
+	xmlNode *et;
+
+	sc = sc_alloc();
+	if (!sc)
+		goto err_alloc;
+
+	argv[0].type = "ServiceInstance";
+	argv[0].value = "ServiceInstance";
+	et = v_rpc(cfg, ses, "RetrieveServiceContent", 1, argv);
+
+	sc->scnode = x_getchild(et, "returnval");
+	sc->scdoc = et->doc;
+
+	return sc;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static void poke_Login(struct config *cfg, struct session *ses,
+    struct v__ServiceContent *sc)
+{
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct v_rpc_arg argv[3];
+	xmlNode *et;
+
+	thisnode = x_getchild(sc->scnode, "sessionManager");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	argv[0].type = "SessionManager";
+	argv[0].value = (char *) thisval;
+	argv[1].type = "userName";
+	argv[1].value = cfg->apiuser;
+	argv[2].type = "password";
+	argv[2].value = cfg->apipass;
+	et = v_rpc(cfg, ses, "Login", 3, argv);
+
+	et = x_getchild(et, "returnval");
+	// x_node_print(et);
+
+	xmlFreeDoc(et->doc);
+	xmlFree(thisval);
+	return;
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for SessionManager\n");
+	exit(1);
+}
+
+static void poke_Logout(struct config *cfg, struct session *ses,
+    struct v__ServiceContent *sc)
+{
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct v_rpc_arg argv[1];
+	xmlNode *et;
+
+	thisnode = x_getchild(sc->scnode, "sessionManager");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	argv[0].type = "SessionManager";
+	argv[0].value = (char *) thisval;
+	et = v_rpc(cfg, ses, "Logout", 1, argv);
+
+	/* Nothing left to verify: LogoutResponse is usually empty. */
+
+	xmlFreeDoc(et->doc);
+	xmlFree(thisval);
+	return;
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for SessionManager\n");
+	exit(1);
+}
+
+/*
+ * Retrieve a property specified by "path" (more like a name actually).
+ */
+static struct v_prop *v_retr_prop(struct config *cfg,
+    struct session *ses, struct v__ServiceContent *sc,
+    struct v__ManagedObjectReference *obj, const char *path)
+{
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	const char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	struct v_prop *ret;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlNode *etprop, *etobj, *etspec;
+	xmlDoc *respdoc;
+	xmlNode *etpset;
+	xmlNode *et;
+
+	proc = "RetrieveProperties";
+	reply = "RetrievePropertiesResponse";
+
+	thisnode = x_getchild(sc->scnode, "propertyCollector");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	x_genxmlhdr(&reqdoc, &ns_env, &ns_v, &etroot);
+
+	etbody = x_addchild(reqdoc, ns_env, etroot, "Body", NULL);
+	etproc = x_addchild(reqdoc, ns_v, etbody, proc, NULL);
+
+	x_addchild_1prop(reqdoc, ns_v, etproc, "_this", (char *)thisval,
+		    "type", "PropertyCollector");
+	etspec = x_addchild(reqdoc, ns_v, etproc, "specSet", NULL);
+
+	etprop = x_addchild(reqdoc, ns_v, etspec, "propSet", NULL);
+	x_addchild(reqdoc, ns_v, etprop, "type", obj->type);
+	x_addchild(reqdoc, ns_v, etprop, "all", "false");
+	x_addchild(reqdoc, ns_v, etprop, "pathSet", path);
+
+	etobj = x_addchild(reqdoc, ns_v, etspec, "objectSet", NULL);
+	x_addchild_1prop(reqdoc, ns_v, etobj, "obj", obj->__item,
+	    "type", obj->type);
+	x_addchild(reqdoc, ns_v, etobj, "skip", "false");
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	respdoc = v_runcurl(cfg, ses, buf, len, proc);
+
+	etroot = xmlDocGetRootElement(respdoc);
+	if (strcmp((const char *)etroot->name, "Envelope") != 0) {
+		fprintf(stderr, TAG ": API root is `%s'\n", etroot->name);
+		exit(EXIT_FAILURE);
+	}
+	et = x_getchild(etroot, "Body");
+	et = x_getchild(et, reply);
+	et = x_getchild(et, "returnval");
+	etpset = x_getchild(et, "propSet");
+
+	et = x_getchild(etpset, "name");
+	et = et->children;
+	if (!et || et->type!=XML_TEXT_NODE || !et->content)
+		goto err_no_name;
+
+	if (strcmp((char *)et->content, path) != 0) {
+		fprintf(stderr,
+		    TAG ": %s((%s)%s) got %s for %s\n",
+		    proc, obj->type, obj->__item, et->content, path);
+		exit(1);
+	}
+
+	et = x_getchild(etpset, "val");
+	ret = prop_new(et);
+
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	xmlFree(thisval);
+	return ret;
+
+ err_no_name:
+	fprintf(stderr, TAG ": No \"name\" tag in returned property\n");
+	exit(1);
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for propertyCollector\n");
+	exit(1);
+}
+
+static struct v__ManagedObjectReference *v_find_by_name(struct config *cfg,
+    struct session *ses, struct v__ServiceContent *sc,
+    char *hostname, int do_vm)
+{
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct v_rpc_arg argv[3];
+	xmlNode *retval;
+	struct v__ManagedObjectReference *ret;
+	xmlNode *et;
+
+	thisnode = x_getchild(sc->scnode, "searchIndex");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	argv[0].type = "SearchIndex";
+	argv[0].value = (char *) thisval;
+	argv[1].type = "dnsName";
+	argv[1].value = hostname;
+	argv[2].type = "vmSearch";
+	argv[2].value = do_vm?"true":"false";
+	et = v_rpc(cfg, ses, "FindByDnsName", 3, argv);
+
+	et = x_getchild(et, "returnval");
+	retval = et->children;
+	if (!retval || retval->type!=XML_TEXT_NODE || !retval->content)
+		goto err_notext_retval;
+
+	ret = mor_new("HostSystem", retval->content);
+
+	xmlFreeDoc(et->doc);
+	xmlFree(thisval);
+	return ret;
+
+ err_notext_retval:
+	fprintf(stderr, TAG ": No text for FindByDnsNameResponse\n");
+	exit(1);
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for SearchIndex\n");
+	exit(1);
+}
+
+/*
+ * In ImportVM: "This operation only works if the folder's childType includes
+ * VirtualMachine."
+ */
+static struct v__ManagedObjectReference *v_find_vm_folder(struct config *cfg,
+    struct session *ses, struct v__ServiceContent *sc,
+    struct v__ManagedObjectReference *h)
+{
+#if 0 /* sample version -- needs at least 1 VM to exist. */
+	struct v_prop *prop;
+	struct v__ManagedObjectReference *vm;
+	struct v__ManagedObjectReference *ret;
+
+	prop = v_retr_prop(cfg, ses, sc, h, "vm");
+	vm = val2mor(prop->value, "VirtualMachine");
+	prop_free(prop);
+
+	prop = v_retr_prop(cfg, ses, sc, vm, "parent");
+	/* XXX Verify that prop->value has type="Folder" */
+	// type = xmlGetProp(etmor, BAD_CAST "type");
+	ret = text2mor("Folder", prop->value);
+	prop_free(prop);
+
+	mor_free(vm);
+	return ret;
+#endif
+#if 1 /* from-the-top version -- needs no VMs, but returns "root" VM folder */
+	struct v_prop *prop;
+	struct v__ManagedObjectReference *dc;
+	xmlNode *et;
+	struct v__ManagedObjectReference *root;
+	struct v__ManagedObjectReference *ret;
+
+	et = x_getchild(sc->scnode, "rootFolder");
+	root = text2mor("Folder", et);
+
+	prop = v_retr_prop(cfg, ses, sc, root, "childEntity");
+	dc = val2mor(prop->value, "Datacenter");
+	prop_free(prop);
+
+	prop = v_retr_prop(cfg, ses, sc, dc, "vmFolder");
+	ret = text2mor("Folder", prop->value);
+	prop_free(prop);
+
+	mor_free(root);
+	mor_free(dc);
+	return ret;
+#endif
+}
+
+static struct v__OvfCreateImportSpecResult *v_create_import_spec(
+    struct config *cfg, struct session *ses, struct v__ServiceContent *sc,
+    struct v__ManagedObjectReference *host, char *ovfstr,
+    struct v__ManagedObjectReference *rp,	/* ResourcePool */
+    struct v__ManagedObjectReference *ds,	/* Datastore */
+    struct v__ManagedObjectReference *net,	/* Network */
+    char *net_name)
+{
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	const char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	struct v__OvfCreateImportSpecResult *ret;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_v;
+	xmlNode *etroot, *etbody, *etproc, *etcisp, *etnetmap;
+	xmlDoc *respdoc;
+	xmlNode *et;
+
+	proc = "CreateImportSpec";
+	reply = "CreateImportSpecResponse";
+
+	thisnode = x_getchild(sc->scnode, "ovfManager");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	x_genxmlhdr(&reqdoc, &ns_env, &ns_v, &etroot);
+
+	etbody = x_addchild(reqdoc, ns_env, etroot, "Body", NULL);
+	etproc = x_addchild(reqdoc, ns_v, etbody, proc, NULL);
+
+	x_addchild_1prop(reqdoc, ns_v, etproc, "_this", (char *)thisval,
+		    "type", "OvfManager");
+
+	/* Argument 1: ovfDescriptor */
+	x_addchild(reqdoc, ns_v, etproc, "ovfDescriptor", ovfstr);
+
+	/* Argument 2: resourcePool */
+	x_addchild_1prop(reqdoc, ns_v, etproc, "resourcePool", rp->__item,
+	    "type", rp->type);
+
+	/* Argument 3: datastore */
+	x_addchild_1prop(reqdoc, ns_v, etproc, "datastore", ds->__item,
+	    "type", ds->type);
+
+	/* Argument 4: cisp. Bad news, it's struct... with a list of structs. */
+	etcisp = x_addchild(reqdoc, ns_v, etproc, "cisp", NULL);
+
+	/* cisp.locale */
+	x_addchild(reqdoc, ns_v, etcisp, "locale", "US");
+
+	/* cisp.deploymentOption -- empty for now */
+	x_addchild(reqdoc, ns_v, etcisp, "deploymentOption", "");
+
+	/* cisp.entityName */
+	x_addchild(reqdoc, ns_v, etcisp, "entityName", cfg->push_name);
+
+	/* cisp.hostSystem -- optional, but set it for now. */
+	x_addchild_1prop(reqdoc, ns_v, etcisp, "hostSystem", host->__item,
+	    "type", host->type);
+
+	/* cisp.networkMapping. Worse news, another level down. */
+	etnetmap = x_addchild(reqdoc, ns_v, etcisp, "networkMapping", NULL);
+	x_addchild(reqdoc, ns_v, etnetmap, "name", net_name);
+	x_addchild_1prop(reqdoc, ns_v, etnetmap, "network", net->__item,
+	     "type", net->type);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	respdoc = v_runcurl(cfg, ses, buf, len, proc);
+
+	etroot = xmlDocGetRootElement(respdoc);
+	if (strcmp((const char *)etroot->name, "Envelope") != 0) {
+		fprintf(stderr, TAG ": API root is `%s'\n", etroot->name);
+		exit(EXIT_FAILURE);
+	}
+
+	ret = malloc(sizeof(struct v__OvfCreateImportSpecResult));
+	if (!ret)
+		goto err_alloc;
+	memset(ret, 0, sizeof(struct v__OvfCreateImportSpecResult));
+
+	et = x_getchild(etroot, "Body");
+	et = x_getchild(et, reply);
+	et = x_getchild(et, "returnval");
+	/*
+	 * Discarding <fileItem> here from the same level as the spec.
+	 * In theory, we need these because they carry a relation between
+	 * the requested filenames of VMDKs and the deviceId, the latter
+	 * being referenced in lease->info. Only by saving <fileItem> we
+	 * can find which file to upload. Fortunately, we only have one VMDK.
+	 */
+	ret->node = x_getchild(et, "importSpec");
+	ret->doc = respdoc;
+
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	xmlFree(thisval);
+	return ret;
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for OvfManager\n");
+	exit(1);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static struct v_lease *v_import_vapp(struct config *cfg, struct session *ses,
+    struct v__ServiceContent *sc,
+    struct v__ManagedObjectReference *rp,	/* ResourcePool */
+    struct v__OvfCreateImportSpecResult *sr,
+    struct v__ManagedObjectReference *vmf,
+    struct v__ManagedObjectReference *host)
+{
+	const char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	struct v_lease *ret;
+	xmlChar *rettype, *retval;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_v;
+	xmlNode *etroot, *etbody, *etproc, *etspec;
+	xmlDoc *respdoc;
+	xmlNode *et;
+
+	ret = malloc(sizeof(struct v_lease));
+	if (!ret)
+		goto err_alloc;
+	memset(ret, 0, sizeof(struct v_lease));
+
+	proc = "ImportVApp";
+	reply = "ImportVAppResponse";
+
+	x_genxmlhdr(&reqdoc, &ns_env, &ns_v, &etroot);
+
+	etbody = x_addchild(reqdoc, ns_env, etroot, "Body", NULL);
+	etproc = x_addchild(reqdoc, ns_v, etbody, proc, NULL);
+
+	x_addchild_1prop(reqdoc, ns_v, etproc, "_this", rp->__item,
+		    "type", rp->type);
+
+	/* Argument 1: (ImportSpec) spec */
+	/*
+	 * This still does not do what we want: the copied namespace
+	 * is retained (e.g. etspec will have an xmlns:xsi attribute).
+	 * But it works, so whatever.
+	 */
+	etspec = xmlDocCopyNode(sr->node, reqdoc, 1);
+	if (!etspec)
+		goto err_alloc;
+	xmlNodeSetName(etspec, BAD_CAST "spec");
+	xmlAddChild(etproc, etspec);
+
+	/* Argument 2: (MOR) folder */
+	x_addchild_1prop(reqdoc, ns_v, etproc, "folder", vmf->__item,
+	    "type", vmf->type);
+
+	/* Argument 3: (MOR) host */
+	x_addchild_1prop(reqdoc, ns_v, etproc, "host", host->__item,
+	    "type", host->type);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	respdoc = v_runcurl(cfg, ses, buf, len, proc);
+
+	etroot = xmlDocGetRootElement(respdoc);
+	if (strcmp((const char *)etroot->name, "Envelope") != 0) {
+		fprintf(stderr, TAG ": API root is `%s'\n", etroot->name);
+		exit(EXIT_FAILURE);
+	}
+	et = x_getchild(etroot, "Body");
+	et = x_getchild(et, reply);
+	et = x_getchild(et, "returnval");
+
+	/*
+	 * The doc promise a MOR but actually it's a string attribute.
+	 * So, custom-extract the type and construct a MOR.
+	 */
+	rettype = xmlGetProp(et, BAD_CAST "type");
+	if (!rettype)
+		goto err_norettype;
+	if (strcmp((char *)rettype, "HttpNfcLease") != 0) {
+		fprintf(stderr, TAG ": bad type in returnval"
+		    ": expected `HttpNfcLease' got `%s'\n", rettype);
+		xmlFree(rettype);
+		exit(1);
+	}
+
+	retval = xmlNodeGetContent(et);
+	if (!retval)
+		goto err_notext_retval;
+	ret->mor = mor_new((const char *)rettype, retval);
+
+	xmlFree(rettype);
+	xmlFree(retval);
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	return ret;
+
+ err_notext_retval:
+	fprintf(stderr, TAG ": No text in returnval (%s)\n", proc);
+	exit(1);
+
+ err_norettype:
+	fprintf(stderr, TAG ": No type in returnval (%s)\n", proc);
+	exit(1);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+/* This is basename(), but we handroll it to make sure, due to BSD. */
+static const char *image_name(const char *path)
+{
+	const char *name;
+
+	if (!(name = strrchr(path, '/')) || name[1]==0)
+		name = path;
+	else
+		name++;
+	return name;
+}
+
+/*
+ * The vol_size is supposed to be the uncompressed size, I think.
+ */
+static char *genovf(const char *base_name, off_t vol_size,
+    char *vm_name, char *net_name)
+{
+	const char fileid1[] = "file1";
+	off_t vmdk_size;
+	char buf100[100];
+	xmlTextWriterPtr writer;
+	xmlBufferPtr buf;
+	int instid, ide_id;
+	char *ret;
+#if 0 /* P3 debugging */
+	FILE *fp;
+#endif
+	int rc;
+
+	ide_id = instid = 0;
+
+	/*
+	 * We need to create OVF before the image is compressed (on the fly
+	 * as it's being uploaded). And one of the fields in the image appears
+	 * to be the size of the image being transferred. For now we simply
+	 * use the uncompressed size.
+	 */
+	vmdk_size = vol_size;
+
+	buf = xmlBufferCreateSize(15000);
+	if (!buf)
+		goto err_alloc;
+
+	writer = xmlNewTextWriterMemory(buf, 0);
+	if (!writer)
+		goto err_alloc;
+	xmlTextWriterSetIndent(writer, 1);
+
+	rc = xmlTextWriterStartDocument(writer, NULL, "UTF-8", NULL);
+	if (rc < 0) {
+		fprintf(stderr, TAG ": Error in xmlTextWriterStartDocument\n");
+		exit(EXIT_FAILURE);
+	}
+
+	/* N.B. "Envelope", not "ovf:Envelope" like in RHEV */
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Envelope");
+	if (rc < 0) goto err_xml;
+	// rc = xmlTextWriterWriteAttribute(writer,
+	//     BAD_CAST "ovf:version", BAD_CAST "0.9");
+	// if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns",
+	    BAD_CAST "http://schemas.dmtf.org/ovf/envelope/1");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns:cim",
+	    BAD_CAST "http://schemas.dmtf.org/ovf/envelope/1/common");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns:ovf",
+	    BAD_CAST "http://schemas.dmtf.org/ovf/envelope/1");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns:rasd",
+	    BAD_CAST "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns:vmw",
+	    BAD_CAST "http://www.vmware.com/schema/ovf");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns:vssd",
+	    BAD_CAST "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "xmlns:xsi",
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance");
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "References");
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "File");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%s", base_name);
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:href", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:id", BAD_CAST fileid1);
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%llu", (long long) vmdk_size);
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:size", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <File> */
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <References> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "DiskSection");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Info", BAD_CAST "Virtual disk information");
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Disk");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%llu",
+	    (long long)((vol_size + (1024*1024*1024) - 1) / (1024*1024*1024)));
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:capacity", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	/* RHEV-M says "MegaBytes" */
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:capacityAllocationUnits", BAD_CAST "byte * 2^30");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:diskId", BAD_CAST "vmdisk1");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:fileRef", BAD_CAST fileid1);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:format", BAD_CAST "http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%llu", (long long) vol_size / 4 * 3); /* guess */
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:populatedSize", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Disk> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterEndElement(writer);	/* close <DiskSection> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "NetworkSection");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Info", BAD_CAST "The list of logical networks");
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Network");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:name", BAD_CAST net_name);
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "The %s network", net_name);
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Description", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Network> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterEndElement(writer);	/* close <NetworkSection> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "VirtualSystem");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:id", BAD_CAST vm_name);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Info", BAD_CAST "A virtual machine");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Name", BAD_CAST vm_name);
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer,
+	    BAD_CAST "OperatingSystemSection");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:id", BAD_CAST "101");	/* XXX What is this? */
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "vmw:osType", BAD_CAST "otherLinux64Guest");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Info",
+	    BAD_CAST "The kind of installed guest operating system");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Description", BAD_CAST "Other Linux (64-bit)");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <OperatingSystemSection> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer,
+	    BAD_CAST "VirtualHardwareSection");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "Info",
+	    BAD_CAST "Virtual hardware requirements");
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "System");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "vssd:ElementName", BAD_CAST "Virtual Hardware Family");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "vssd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "vssd:VirtualSystemIdentifier", BAD_CAST vm_name);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "vssd:VirtualSystemType", BAD_CAST "vmx-07");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <System> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Item");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AllocationUnits", BAD_CAST "hertz * 10^6");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Description", BAD_CAST "Number of Virtual CPUs");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ElementName", BAD_CAST "1 virtual CPU(s)");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceType", BAD_CAST "3");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:VirtualQuantity", BAD_CAST "1");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Item> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Item");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AllocationUnits", BAD_CAST "byte * 2^20");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Description", BAD_CAST "Memory Size");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ElementName", BAD_CAST "384MB of memory");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceType", BAD_CAST "4");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:VirtualQuantity", BAD_CAST "384");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Item> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Item");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Address", BAD_CAST "0");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Description", BAD_CAST "IDE Controller");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ElementName", BAD_CAST "IDE 0");
+	if (rc < 0) goto err_xml;
+	ide_id = instid;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceType", BAD_CAST "5");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Item> */
+	if (rc < 0) goto err_xml;
+
+	/*
+	 * Ignoring floppies, hope their BIOS boots without.
+	 *      <Item ovf:required="false">
+	 *        <rasd:AddressOnParent>0</rasd:AddressOnParent>
+	 *        <rasd:AutomaticAllocation>false</rasd:AutomaticAllocation>
+	 *        <rasd:Description>Floppy Drive</rasd:Description>
+	 *        <rasd:ElementName>Floppy drive 1</rasd:ElementName>
+	 *        <rasd:InstanceID>5</rasd:InstanceID>
+	 *        <rasd:ResourceType>14</rasd:ResourceType>
+	 *      </Item>
+	 */
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Item");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AddressOnParent", BAD_CAST "0");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ElementName", BAD_CAST "Hard disk 1");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:HostResource", BAD_CAST "ovf:/disk/vmdisk1");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", ide_id);
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Parent", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceType", BAD_CAST "17");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Item> */
+	if (rc < 0) goto err_xml;
+
+	/*
+	 * In VMware CD is hooked to its own IDE controller, so this may
+	 * fail to work properly.
+	 * We use one Parent and different AddressOnParent.
+	 */
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Item");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteAttribute(writer,
+	    BAD_CAST "ovf:required", BAD_CAST "false");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AddressOnParent", BAD_CAST "1");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AutomaticAllocation", BAD_CAST "false");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ElementName", BAD_CAST "CD/DVD Drive 1");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", ide_id);
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Parent", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceType", BAD_CAST "15");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Item> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterStartElement(writer, BAD_CAST "Item");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AddressOnParent", BAD_CAST "7");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:AutomaticAllocation", BAD_CAST "true");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Connection", BAD_CAST net_name);
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "E1000 ethernet adapter on \"%s\"", net_name);
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:Description", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ElementName", BAD_CAST "Network adapter 1");
+	if (rc < 0) goto err_xml;
+	snprintf(buf100, 100, "%d", instid);    instid++;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:InstanceID", BAD_CAST buf100);
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceSubType", BAD_CAST "E1000");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterWriteElement(writer,
+	    BAD_CAST "rasd:ResourceType", BAD_CAST "10");
+	if (rc < 0) goto err_xml;
+	rc = xmlTextWriterEndElement(writer);	/* close <Item> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterEndElement(writer);	/* close <VirtualHardwareSection> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterEndElement(writer);	/* close <VirtualSystem> */
+	if (rc < 0) goto err_xml;
+
+	rc = xmlTextWriterEndDocument(writer);
+	if (rc < 0) {
+		fprintf(stderr, "ERROR Error in xmlTextWriterEndDocument\n");
+		exit(EXIT_FAILURE);
+	}
+
+	xmlFreeTextWriter(writer);
+
+#if 0 /* P3 debugging */
+	if ((fp = fopen("out.ovf", "w")) == NULL) {
+		fprintf(stderr, TAG ": cannot dump to %s\n", "out.ovf");
+		exit(1);
+	}
+	xmlBufferDump(fp, buf);
+	fclose(fp);
+#endif
+
+	ret = malloc(buf->use+1);
+	if (!ret)
+		goto err_alloc;
+	memcpy(ret, buf->content, buf->use);
+	ret[buf->use] = 0;
+	xmlBufferFree(buf);
+	return ret;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+
+ err_xml:
+	fprintf(stderr, TAG ": Failed to form XML\n");
+	exit(EXIT_FAILURE);
+}
+
+/*
+ * Update lease with the fetched HttpNcfLeaseInfo.
+ */
+static int v_lease_parse_info(struct v_lease *lease, struct v_prop *prop)
+{
+	xmlNode *et;
+
+	et = x_getchild(prop->value, "deviceUrl");
+	et = x_getchild(et, "url");
+	lease->url = x_gettext(et);
+	return 0;
+}
+
+static int v_lease_state(struct config *cfg, struct session *ses,
+    struct v__ServiceContent *sc, struct v_lease *lease)
+{
+	struct v_prop *prop;
+	char *status;
+	int ret;
+
+	prop = v_retr_prop(cfg, ses, sc, lease->mor, "state");
+	status = x_gettext(prop->value);
+
+	if (strcmp(status, "initializing") == 0)
+		ret = v__HttpNfcLeaseState__initializing;
+	else if (strcmp(status, "ready") == 0)
+		ret = v__HttpNfcLeaseState__ready;
+	else if (strcmp(status, "done") == 0)
+		ret = v__HttpNfcLeaseState__done;
+	else if (strcmp(status, "error") == 0)
+		ret = v__HttpNfcLeaseState__error;
+	else
+		ret = -1;
+
+	free(status);
+	prop_free(prop);
+	return ret;
+}
+
+static void v_lease_progress(struct config *cfg, struct session *ses,
+    struct v__ManagedObjectReference *ls, int pcs)
+{
+	struct v_rpc_arg argv[2];
+	char bufpc[4];
+	xmlNode *et;
+
+	snprintf(bufpc, sizeof(bufpc), "%d", pcs);
+
+	argv[0].type = ls->type;
+	argv[0].value = ls->__item;
+	argv[1].type = "percent";
+	argv[1].value = bufpc;
+	et = v_rpc(cfg, ses, "HttpNfcLeaseProgress", 2, argv);
+
+	/* Response is empty. */
+
+	xmlFreeDoc(et->doc);
+	return;
+}
+
+static void v_lease_complete(struct config *cfg, struct session *ses,
+    struct v__ManagedObjectReference *ls)
+{
+	struct v_rpc_arg argv[1];
+	xmlNode *et;
+
+	argv[0].type = ls->type;
+	argv[0].value = ls->__item;
+	et = v_rpc(cfg, ses, "HttpNfcLeaseComplete", 1, argv);
+
+	/* Response is empty. */
+
+	xmlFreeDoc(et->doc);
+	return;
+}
+
+static int v_upload(struct config *cfg, struct session *ses,
+    int use_put, const char *url, int tfd)
+{
+	FILE *fp;
+	CURL *curl;		/* new handle */
+	struct curl_slist *curlhdrs = NULL;
+	CURLcode rcc;
+
+	/*
+	 * Not possible to determine the size of pipe.
+	 */
+#if 0 /* fsize */
+	struct stat statb;
+	curl_off_t fsize;
+
+	if (fstat(tfd, &statb) < 0) {
+		fprintf(stderr, TAG ": failed to stat temp: %s\n",
+		    strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	fsize = statb.st_size;
+#endif
+
+	fp = fdopen(tfd, "r");
+	if (!fp) {
+		fprintf(stderr, TAG ": fdopen of pipe error: %s\n",
+		    strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+
+	/*
+	 * Use new session - in order to update the progress indicator.
+	 * Session cookies should not be necessary, the URL is the key.
+	 */
+	curl = curl_easy_init();
+
+	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
+	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
+	// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1);
+
+	curl_easy_setopt(curl, CURLOPT_URL, url);
+	curl_easy_setopt(curl, CURLOPT_FAILONERROR, 1);
+	curl_easy_setopt(curl, CURLOPT_READFUNCTION, fread);
+	curl_easy_setopt(curl, CURLOPT_READDATA, fp);
+	if (use_put) {
+		curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
+#if 0 /* fsize */
+		curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, fsize);
+#endif
+	} else {
+		curl_easy_setopt(curl, CURLOPT_POST, 1);
+#if 0 /* fsize */
+		curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE, fsize);
+#endif
+	}
+
+#if 1 /* fsize */
+	/*
+	 * We know that VMware is not HTTP 1.0, so just set chunked.
+	 * Also, libcurl takes care of properly chunking for us.
+	 */
+	if (!curlx_add_header(&curlhdrs,
+	    "Transfer-Encoding: chunked"))
+		goto err_alloc;
+#endif
+	/*
+	 * This is especially important if !use_put, because curl sets
+	 * "Content-Type: application/x-www-form-urlencoded" otherwise.
+	 * (actually VMware ignores this, so, I mean, important in general).
+	 */
+	if (!curlx_add_header(&curlhdrs,
+	    "Content-Type: application/octet-stream"))
+		goto err_alloc;
+	curl_easy_setopt(curl, CURLOPT_HTTPHEADER, curlhdrs);
+
+	rcc = curl_easy_perform(curl);
+	if (rcc != CURLE_OK) {
+		fprintf(stderr, TAG ": curl failed upload url `%s'\n", url);
+		exit(EXIT_FAILURE);
+	}
+
+	curl_easy_cleanup(curl);
+	curl_slist_free_all(curlhdrs);
+	fclose(fp);
+	return 0;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static void run_upload(struct config *cfg, struct session *ses, bool use_put,
+    const char *url, int tfd, struct v__ManagedObjectReference *ls)
+{
+	pid_t uploader;
+	int wstat;
+	int fake_pct;
+	time_t now, last;
+	int interval;
+	struct timespec tm;
+	int rc;
+
+	fflush(stdout);
+	fflush(stderr);
+
+	uploader = fork();
+	if (uploader < 0) {
+		fprintf(stderr, TAG ": fork error: %s\n", strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	if (uploader == 0) {
+		v_upload(cfg, ses, 0, url, tfd);
+		exit(0);
+	}
+
+	fake_pct = 0;
+	interval = 1;
+	last = time(NULL);
+
+	for (;;) {
+		rc = waitpid(uploader, &wstat, WNOHANG);
+		if (rc < 0) {
+			fprintf(stderr,
+			    TAG ": waitpid error: %s\n", strerror(errno));
+			exit(EXIT_FAILURE);
+		}
+		if (rc != 0) {
+			if (!WIFEXITED(wstat)) {
+				fprintf(stderr,
+				    TAG ": uploader is killed (status 0x%x)\n",
+				    wstat);
+				exit(EXIT_FAILURE);
+			}
+			if (WEXITSTATUS(wstat)) {
+				/*
+				 * Don't print anything here. An error code
+				 * means an error message from the child, which
+				 * is precious. Let it be captured in ami-id
+				 * instead of our useless message.
+				 */
+				/* fprintf(stderr, "uploader exited with code %d\n",
+				    WEXITSTATUS(wstat)); */
+				exit(EXIT_FAILURE);
+			}
+			break;
+		}
+
+		tm.tv_sec = interval;
+		tm.tv_nsec = 0;
+		nanosleep(&tm, NULL);
+		/* XXX Use SIGCHLD */
+		if (interval < 20)
+			interval *= 2;
+
+		now = time(NULL);
+		if (now >= last + 150) {
+			/* P3 */ printf("done %d%%\n", fake_pct);
+			if (fake_pct < 98)
+				fake_pct++;
+			v_lease_progress(cfg, ses, ls, fake_pct);
+			last = now;
+		}
+	}
+}
+
+static unsigned long divro(unsigned long num, unsigned long divisor)
+{
+	unsigned long ret;
+
+	ret = num / divisor;
+	if (ret * divisor != num)
+		ret++;
+	return ret;
+}
+
+static bool is_zero(const char *p, size_t len)
+{
+	while (len) {
+		--len;
+		if (*p++ != 0)
+			return false;
+	}
+	return true;
+}
+
+static off_t file_size(const char *fname)
+{
+	struct stat statb;
+
+	if (stat(fname, &statb) < 0) {
+		fprintf(stderr, TAG ": failed to stat `%s': %s\n",
+		    fname, strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	/*
+	 * This is a little nasty, but we need to know the file size.
+	 * If we ever want to process a pipe or a socket, size has to
+	 * be in argv somewhere.
+	 */
+	if (!S_ISREG(statb.st_mode)) {
+		fprintf(stderr, TAG ": not a regular file `%s'\n", fname);
+		exit(EXIT_FAILURE);
+	}
+	return statb.st_size;
+}
+
+/* See https://github.com/imcleod/VMDK-stream-converter */
+static void convert(const char *fname, off_t vol_size, int vfd)
+{
+	void *sectb;
+	int ifd;
+	unsigned long infileSectors, infileCylinders;
+	unsigned int grainSectors;
+	unsigned long totalGrains, totalGrainTables;
+	unsigned long grainDirectorySectors, grainDirectoryEntries;
+	char *image_descriptor_pad;
+	size_t image_descriptor_len;
+	struct SparseExtentHeader sehb;
+	struct GrainMarker *gm;
+	struct MetaDataMarker gtmarker, gdmarker, eosmarker, footmarker;
+	unsigned int *currentGrainTable, *grainDirectory;
+	unsigned int cgx, gdx;
+	unsigned long inputSectorPointer, outputSectorPointer;
+	unsigned char *inChunk;
+	unsigned char *obuf;
+	size_t cbufsz;
+	unsigned long chunklen;
+	unsigned long pct;
+	int i;
+	ssize_t rrc, wrc;
+	int rc;
+
+	sectb = malloc(512);
+	if (!sectb)
+		goto err_alloc;
+
+	ifd = open(fname, O_RDONLY);
+	if (ifd == -1) {
+		fprintf(stderr, TAG ": failed to open %s: %s\n",
+		    fname, strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+
+	infileSectors = (vol_size + 511) / 512;
+	grainSectors = 128;	/* Why? Just because. Ask Ian. */
+	totalGrains = divro(infileSectors, grainSectors);
+	totalGrainTables = divro(totalGrains, 512);
+	grainDirectorySectors = divro(totalGrainTables*4, 512);
+	grainDirectoryEntries = grainDirectorySectors*128;
+	printf("DEBUG: Number of entries (overhead value in header) in Grain Directory - (%ld)\n", (long)grainDirectoryEntries);
+
+	infileCylinders = divro(infileSectors, (63*255));
+
+	image_descriptor_len = 512;
+	image_descriptor_pad = malloc(image_descriptor_len);
+	if (!image_descriptor_pad)
+		goto err_alloc;
+	memset(image_descriptor_pad, 0, image_descriptor_len);
+
+	snprintf(image_descriptor_pad, image_descriptor_len,
+		"# Description file created by Image Warehouse\n"
+		"version=1\n"
+		"# Believe this is random\n"
+		"CID=7e5b80a7 \n"
+		"# Indicates no parent\n"
+		"parentCID=ffffffff \n"
+		"createType=\"streamOptimized\" \n"
+		"\n"
+		"# Extent description\n"
+		"RDONLY #SECTORS# SPARSE \"call-me-stream.vmdk\"\n"
+		"\n"
+		"# The Disk Data Base \n"
+		"#DDB\n"
+		"\n"
+		"ddb.adapterType = \"lsilogic\"\n"
+		"# %lu / 63 / 255 rounded up\n"
+		"ddb.geometry.cylinders = \"%lu\"\n"
+		"ddb.geometry.heads = \"255\"\n"
+		"ddb.geometry.sectors = \"63\"\n"
+		"# Believe this is random\n"
+		"ddb.longContentID = \"8f15b3d0009d9a3f456ff7b28d324d2a\"\n"
+		"ddb.virtualHWVersion = \"7\"\n",
+	    infileSectors, infileCylinders);
+
+	memset(&sehb, 0, sizeof(sehb));
+	sehb.magicNumber = SPARSE_MAGICNUMBER;
+	sehb.version = 3;
+	sehb.flags |= (3 << 16);
+	sehb.capacity = infileSectors;
+	sehb.grainSize = 128;
+	sehb.descriptorOffset = sizeof(sehb)/512;
+	sehb.descriptorSize = image_descriptor_len/512;
+	sehb.numGTEsPerGT = 512;
+	sehb.rgdOffset = 0;
+	sehb.gdOffset = GD_AT_END;
+	sehb.overHead = 128;	/* "128 is almost guaranteed to be enough" */
+	sehb.uncleanShutdown = 0;
+	sehb.singleEndLineChar = '\n';
+	sehb.nonEndLineChar = ' ';
+	sehb.doubleEndLineChar1 = '\r';
+	sehb.doubleEndLineChar2 = '\n';
+	sehb.compressAlgorithm = COMPRESSION_DEFLATE;
+
+	wrc = write(vfd, &sehb, sizeof(sehb));
+	if (wrc < 0)
+		goto err_write;
+	wrc = write(vfd, image_descriptor_pad, image_descriptor_len);
+	if (wrc < 0)
+		goto err_write;
+
+	/*
+	 * Don't just lseek here, we're going to pipe this out if we can.
+	 */
+	memset(sectb, 0, 512);
+	for (i = (sizeof(sehb)+image_descriptor_len)/512; i < 128; i++) {
+		wrc = write(vfd, sectb, 512);
+		if (wrc < 0)
+			goto err_write;
+	}
+	outputSectorPointer = 128;
+
+	memset(&gtmarker, 0, sizeof(struct MetaDataMarker));
+	gtmarker.val = 4;
+	gtmarker.size = 0;
+	gtmarker.type = MARKER_GT;
+
+	memset(&gdmarker, 0, sizeof(struct MetaDataMarker));
+	gdmarker.val = grainDirectorySectors;
+	gdmarker.size = 0;
+	gdmarker.type = MARKER_GD;
+
+	memset(&eosmarker, 0, sizeof(struct MetaDataMarker));
+	eosmarker.type = MARKER_EOS;
+
+	memset(&footmarker, 0, sizeof(struct MetaDataMarker));
+	footmarker.val = 1;
+	footmarker.size = 0;
+	footmarker.type = MARKER_FOOTER;
+
+	cbufsz = compressBound(grainSectors * 512);
+	obuf = malloc(sizeof(struct GrainMarker) + cbufsz + 512);
+	if (!obuf)
+		goto err_alloc;
+
+	inputSectorPointer = 0;
+
+	inChunk = malloc(grainSectors * 512);
+	if (!inChunk)
+		goto err_alloc;
+
+	grainDirectory = malloc(grainDirectoryEntries * sizeof(unsigned int));
+	if (!grainDirectory)
+		goto err_alloc;
+	memset(grainDirectory, 0, grainDirectoryEntries * sizeof(unsigned int));
+	gdx = 0;
+	currentGrainTable = malloc(512 * sizeof(unsigned int));
+	if (!currentGrainTable)
+		goto err_alloc;
+	memset(currentGrainTable, 0, 512 * sizeof(unsigned int));
+	cgx = 0;
+
+	pct = 0;
+	for (;;) {
+		rrc = read(ifd, inChunk, grainSectors * 512);
+		if (rrc < 0)
+			goto err_read;
+		if (rrc == 0)
+			break;
+
+		if (gdx >= grainDirectoryEntries) {
+			/* Should never happen, see the calculations above. */
+			fprintf(stderr, TAG ": GD overflow (%d)\n", gdx);
+			exit(EXIT_FAILURE);
+		}
+
+		if (is_zero((char *)inChunk, rrc)) {
+			currentGrainTable[cgx++] = 0;
+		} else {
+			currentGrainTable[cgx++] = outputSectorPointer;
+
+			chunklen = cbufsz;
+			rc = compress(obuf + sizeof(struct GrainMarker),
+			    &chunklen, inChunk, rrc);
+			if (rc != Z_OK || chunklen == 0)
+				goto err_compress;
+
+			gm = (struct GrainMarker *) obuf;
+			gm->val = inputSectorPointer;
+			gm->size = chunklen;
+
+			chunklen += sizeof(struct GrainMarker);
+			chunklen = (chunklen + 511) & ~511;
+
+			wrc = write(vfd, obuf, chunklen);
+			if (wrc < 0)
+				goto err_write;
+			if ((size_t)wrc < chunklen)
+				goto err_short;
+			outputSectorPointer += wrc / 512;
+		}
+
+		if (cgx == 512) {
+			if (!is_zero((char *)currentGrainTable,
+			    512 * sizeof(int))) {
+				wrc = write(vfd, &gtmarker, 512);
+				if (wrc < 0)
+					goto err_write;
+				wrc = write(vfd, &currentGrainTable,
+				    512 * sizeof(int));
+				if (wrc < 0)
+					goto err_write;
+				grainDirectory[gdx++] = outputSectorPointer + 1;
+				outputSectorPointer += 5;
+			} else {
+				grainDirectory[gdx++] = 0;
+			}
+			memset(currentGrainTable, 0, 512 * sizeof(int));
+			cgx = 0;
+		}
+
+		inputSectorPointer += rrc / 512;
+	}
+	if (cgx) {
+		if (!is_zero((char *)currentGrainTable, cgx * sizeof(int))) {
+			wrc = write(vfd, &gtmarker, 512);
+			if (wrc < 0)
+				goto err_write;
+			wrc = write(vfd, &currentGrainTable,
+			    512 * sizeof(unsigned int));
+			if (wrc < 0)
+				goto err_write;
+			grainDirectory[gdx++] = outputSectorPointer + 1;
+			outputSectorPointer += 5;
+		}
+	}
+
+	wrc = write(vfd, &gdmarker, 512);
+	if (wrc < 0)
+		goto err_write;
+	outputSectorPointer += 1;
+
+	printf("Grain directory length (%d)\n", gdx);
+	printf("Grain directory: ");
+	for (gdx = 0; gdx < grainDirectoryEntries; gdx++) {
+		if (gdx)
+			printf(", ");
+		printf("%d", grainDirectory[gdx]);
+	}
+	printf("]\n");
+
+	wrc = write(vfd, grainDirectory, grainDirectoryEntries * sizeof(int));
+	if (wrc < 0)
+		goto err_write;
+	// outputSectorPointer += grainDirectorySectors;
+
+	wrc = write(vfd, &footmarker, sizeof(struct MetaDataMarker));
+	if (wrc < 0)
+		goto err_write;
+
+	memset(&sehb, 0, sizeof(sehb));
+	sehb.magicNumber = SPARSE_MAGICNUMBER;
+	sehb.version = 3;
+	sehb.flags |= (3 << 16);
+	sehb.capacity = infileSectors;
+	sehb.grainSize = 128;
+	sehb.descriptorOffset = sizeof(sehb)/512;
+	sehb.descriptorSize = image_descriptor_len/512;
+	sehb.numGTEsPerGT = 512;
+	sehb.rgdOffset = 0;
+	sehb.gdOffset = outputSectorPointer;	/* saved above */
+	sehb.overHead = 128;	/* "128 is almost guaranteed to be enough" */
+	sehb.uncleanShutdown = 0;
+	sehb.singleEndLineChar = '\n';
+	sehb.nonEndLineChar = ' ';
+	sehb.doubleEndLineChar1 = '\r';
+	sehb.doubleEndLineChar2 = '\n';
+	sehb.compressAlgorithm = COMPRESSION_DEFLATE;
+
+	wrc = write(vfd, &sehb, sizeof(sehb));
+	if (wrc < 0)
+		goto err_write;
+
+	/* Is this just me, or eosmarker is just a sector full of zeroes? */
+	wrc = write(vfd, &eosmarker, sizeof(struct MetaDataMarker));
+	if (wrc < 0)
+		goto err_write;
+
+	close(ifd);
+	free(obuf);
+	free(currentGrainTable);
+	free(grainDirectory);
+	free(image_descriptor_pad);
+	free(sectb);
+	return;
+
+ err_compress:
+	fprintf(stderr, TAG ": zlib error\n");
+	exit(EXIT_FAILURE);
+
+ err_write:
+	fprintf(stderr, TAG ": VMDK write error: %s\n", strerror(errno));
+	exit(EXIT_FAILURE);
+
+ err_short:
+	fprintf(stderr, TAG ": VMDK short write\n");
+	exit(EXIT_FAILURE);
+
+ err_read:
+	fprintf(stderr, TAG ": image read error: %s\n", strerror(errno));
+	exit(EXIT_FAILURE);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(EXIT_FAILURE);
+}
+
+static void push(struct config *cfg, off_t vol_size, int tfd)
+{
+	struct session ses;
+	CURL *curl;
+	struct v__ServiceContent *sc;
+	const char *base_name;
+	struct v_prop *prop;
+	struct v__ManagedObjectReference *h;	/* HostSystem */
+	struct v__ManagedObjectReference *net;	/* Network */
+	char *net_name;
+	char *ovf;
+	struct v__ManagedObjectReference *host_parent;	/* ResourcePool */
+	struct v__ManagedObjectReference *rp;	/* ResourcePool */
+	struct v__ManagedObjectReference *ds;	/* Datastore */
+	struct v__OvfCreateImportSpecResult *sr;
+	struct v__ManagedObjectReference *vmf;	/* Folder */
+	struct v_lease *lease;			/* HttpNfcLease */
+	enum v__HttpNfcLeaseState state;
+	int i;
+
+	memset(&ses, 0, sizeof(struct session));
+	ses.curl = curl = curl_easy_init();
+	curl_easy_setopt(curl, CURLOPT_COOKIEFILE, "");
+	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0);
+	curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0);
+	curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, api_dbcb);
+
+	/* It is essential to specify vim25, because we need ovfManager. */
+	if (!curlx_add_header(&ses.curlhdrs, "SOAPAction: \"urn:vim25/4.1\""))
+		goto err_alloc;
+	if (!curlx_add_header(&ses.curlhdrs,
+	    "Content-Type: text/xml; charset=utf-8"))
+		goto err_alloc;
+
+	// curl_easy_setopt(curl, CURLOPT_VERBOSE, 1); /* P3 */
+	sc = poke_svcroot(cfg, &ses);
+	poke_Login(cfg, &ses, sc);
+
+	h = v_find_by_name(cfg, &ses, sc, cfg->push_host, 0);
+	/* printf("host %s\n", h->__item); */
+
+	prop = v_retr_prop(cfg, &ses, sc, h, "network");
+	net = val2mor(prop->value, "Network");
+	prop_free(prop);
+
+	prop = v_retr_prop(cfg, &ses, sc, net, "name");
+	net_name = x_gettext(prop->value);
+	prop_free(prop);
+
+	vmf = v_find_vm_folder(cfg, &ses, sc, h);
+
+	base_name = image_name(cfg->push_img);
+	ovf = genovf(base_name, vol_size, cfg->push_name, net_name);
+
+	prop = v_retr_prop(cfg, &ses, sc, h, "parent");
+	host_parent = text2mor("ComputeResource", prop->value);
+	prop_free(prop);
+
+	prop = v_retr_prop(cfg, &ses, sc, host_parent, "resourcePool");
+	rp = text2mor("ResourcePool", prop->value);
+	prop_free(prop);
+
+	prop = v_retr_prop(cfg, &ses, sc, h, "datastore");
+	/* prop at this point is a list of MORs for all datastores */
+	ds = val2mor(prop->value, "Datastore");
+	prop_free(prop);
+	/* P3 */ printf("ds (%s) %s\n", ds->type, ds->__item);
+
+	sr = v_create_import_spec(cfg, &ses, sc, h, ovf, rp, ds, net, net_name);
+	// /* P3 */ x_node_print(sr->node);
+
+	lease = v_import_vapp(cfg, &ses, sc, rp, sr, vmf, h);
+	/* P3 */ printf("lease %s\n", lease->mor->__item);
+
+	i = 0;
+	for (;;) {
+		state = v_lease_state(cfg, &ses, sc, lease);
+		if (state == v__HttpNfcLeaseState__ready)
+			break;
+		if (state == v__HttpNfcLeaseState__error)
+			goto err_lease;
+		if (++i >= 20) {
+			fprintf(stderr, TAG ": Lease fails to get ready (%d)\n",
+			    state);
+			exit(EXIT_FAILURE);
+		}
+		sleep(3);
+	}
+	/* P3 */ printf("Lease is ready\n");
+
+	/*
+	 * The info property is only valid when the lease is in the ready state.
+	 */
+	prop = v_retr_prop(cfg, &ses, sc, lease->mor, "info");
+	v_lease_parse_info(lease, prop);
+	prop_free(prop);
+
+	/*
+	 * The URL may have a wildcard '*' for a host. Process that. XXX
+	 * The PUT method is used when <fileItem> has <create> field true. XXX
+	 */
+	run_upload(cfg, &ses, false, lease->url, tfd, lease->mor);
+
+	v_lease_progress(cfg, &ses, lease->mor, 100);
+	v_lease_complete(cfg, &ses, lease->mor);
+
+	/*
+	 * We think we may be switching to MOR eventually, so we use
+	 * a selector for compatibility ("name").
+	 */
+	printf("IMAGE name:%s\n", cfg->push_name);
+
+	poke_Logout(cfg, &ses, sc);
+
+	mor_free(lease->mor);  free(lease->url);  free(lease);
+	xmlFreeDoc(sr->doc);  free(sr);
+	mor_free(ds);
+	mor_free(rp);
+	mor_free(host_parent);
+	free(net_name);
+	mor_free(vmf);
+	mor_free(net);
+	mor_free(h);
+	sc_free(sc);
+	curl_easy_cleanup(ses.curl);
+	curl_slist_free_all(ses.curlhdrs);
+	return;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(EXIT_FAILURE);
+
+ err_lease:
+	fprintf(stderr, TAG ": Lease entered an error state\n");
+	{
+		xmlNode *et;
+		char *err;
+		prop = v_retr_prop(cfg, &ses, sc, lease->mor, "error");
+		et = x_getchild(prop->value, "localizedMessage");
+		err = x_gettext(et);
+		fprintf(stderr, TAG ": Lease message: %s\n", err);
+	}
+	exit(EXIT_FAILURE);
+}
+
+static void push_file(struct config *cfg, const char *tmpname)
+{
+	int fd;
+	off_t vol_size;
+
+	vol_size = file_size(cfg->push_img);
+
+	fd = open(tmpname, O_CREAT|O_TRUNC|O_WRONLY, 0666);
+	if (fd == -1) {
+		fprintf(stderr, TAG ": failed to create `%s': %s\n",
+		    tmpname, strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	convert(cfg->push_img, vol_size, fd);
+	close(fd);
+
+	if ((fd = open(tmpname, O_RDONLY)) == -1) {
+		fprintf(stderr, TAG ": unable to reopen `%s': %s\n",
+		    tmpname, strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	push(cfg, vol_size, fd);
+	close(fd);
+
+	/* The tmpname is not unlinked, so it can be examined for debugging. */
+}
+
+/*
+ * This is quite inefficient, because it pipes the bulk of the data
+ * to be uploaded through a system pipe, instead of just storing it in
+ * a circular buffer or something. We do it for expediency, to preserve
+ * the old code that worked with temporary files.
+ */
+static void push_pipe(struct config *cfg)
+{
+	off_t vol_size;
+	int pipe_cvt_bulk[2];
+	int wstat;
+	pid_t converter;
+
+	vol_size = file_size(cfg->push_img);
+
+	if (pipe(pipe_cvt_bulk) < 0) {
+		fprintf(stderr, TAG ": pipe error: %s\n", strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+
+	fflush(stdout);
+	fflush(stderr);
+
+	converter = fork();
+	if (converter < 0) {
+		fprintf(stderr, TAG ": fork error: %s\n", strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	if (converter == 0) {
+		close(pipe_cvt_bulk[0]);
+		convert(cfg->push_img, vol_size, pipe_cvt_bulk[1]);
+		exit(0);
+	}
+
+	close(pipe_cvt_bulk[1]);
+	push(cfg, vol_size, pipe_cvt_bulk[0]);
+
+	if (waitpid(converter, &wstat, 0) < 0) {
+		fprintf(stderr, TAG ": waitpid error: %s\n", strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	if (!WIFEXITED(wstat)) {
+		fprintf(stderr, TAG ": converter is killed (status 0x%x)\n",
+		    wstat);
+		exit(EXIT_FAILURE);
+	}
+	if (WEXITSTATUS(wstat)) {
+		fprintf(stderr, "converter exited with code %d\n",
+		    WEXITSTATUS(wstat));
+		exit(EXIT_FAILURE);
+	}
+
+	close(pipe_cvt_bulk[0]);
+}
+
+int main(int argc, char **argv, char **envp)
+{
+	struct config cfg;
+
+	/*
+	 * SSL in curl blows up in uploader subprocess, because in Fedora
+	 * libcurl is built with NSS, and NSS insists on initializing PKCS11
+	 * (which we don't even need), and PKCS11 is defined to reject forks.
+	 * The correct approach would be curl_global_cleanup() after fork().
+	 * Alas, that breaks because NS_Initialize fails on second run.
+	 * So, layering violation it is.
+	 */
+	setenv("NSS_STRICT_NOFORK", "DISABLED", 1);
+	if (curl_global_init(CURL_GLOBAL_ALL) != 0)
+		exit(2);
+
+	cfg_parse(&cfg, argc, argv);
+	if (cfg.push_tmp) {
+		push_file(&cfg, cfg.push_tmp);
+	} else {
+		push_pipe(&cfg);
+	}
+
+        curl_global_cleanup();
+	return 0;
+}
diff --git a/doc/registrations.md b/doc/registrations.md
index cfcf8bb..82f9df7 100644
--- a/doc/registrations.md
+++ b/doc/registrations.md
@@ -146,4 +146,32 @@ renames it into /mnt/falcon-in/.
 VMware vSphere
 --------------
 
-Not implemented yet.
+Registration in vSphere requires no presets. Unfortunately, one still has
+to select the appropriate "VM host" among those that given vSphere manages.
+The VM image is going to be registered in vSphere as a whole, but uploaded
+to the specific host.
+
+[
+   {
+      "name": "main",
+      "type": "fs-vmw",
+      "path": "_fs",
+   }
+]
+
+Registration call:
+
+ curl -d op=register -d site=main \
+  -d api-url=https://vsphere.virt.bos.redhat.com/sdk \
+  -d api-key=Administrator at virt.lab.eng.bos.redhat.com \
+  -d api-secret=donotusepassw0rd \
+  -d vm-name=dummy_img_3 \
+  -d vm-host=virtlab110.virt.bos.redhat.com \
+  http://localhost:9090/buk1/dummy_img_3
+
+The VM name has to be unique in the vSphere environment. If not set, the
+image's key is used.
+
+The ami-id contains a pattern like "OK name:dummy_img_3". The "name:"
+selector is supposed to provide a measure of compatibility for the future,
+in case we ever decide to return a MOR or UUID.
diff --git a/setup.c b/setup.c
index 8a46977..caeb430 100644
--- a/setup.c
+++ b/setup.c
@@ -68,6 +68,7 @@ extern backend_func_tbl	cf_func_tbl;
 extern backend_func_tbl	fs_func_tbl;
 extern backend_func_tbl	fs_rhevm_func_tbl;
 extern backend_func_tbl	fs_condor_func_tbl;
+extern backend_func_tbl	fs_vmw_func_tbl;
 
 static json_t		*config		= NULL;
 static Hash_table	*prov_hash	= NULL;
@@ -340,6 +341,9 @@ convert_provider (int i, provider_t *out)
 	else if (!strcasecmp(out->type,"fs-condor")) {
 		out->func_tbl = &fs_condor_func_tbl;
 	}
+	else if (!strcasecmp(out->type,"fs-vmw")) {
+		out->func_tbl = &fs_vmw_func_tbl;
+	}
 	else {
 		out->func_tbl = &bad_func_tbl;
 	}
@@ -444,6 +448,8 @@ add_provider (Hash_table *h)
         prov->func_tbl = &fs_rhevm_func_tbl;
     else if (!strcasecmp(prov->type,"fs-condor"))
         prov->func_tbl = &fs_condor_func_tbl;
+    else if (!strcasecmp(prov->type,"fs-vmw"))
+        prov->func_tbl = &fs_vmw_func_tbl;
     else
         prov->func_tbl = &bad_func_tbl;
 


More information about the iwhd-devel mailing list