RFC: VMware vSphere registration

Pete Zaitcev zaitcev at kotori.zaitcev.us
Thu May 26 00:53:06 UTC 2011


Here's a first cut at the registration. The main known issue is that the
lease is not being updated, so if the uploader cannot complete its job
in 300 seconds, the registration fails. Also, it's not endian-clean.
There's probably more. But it works otherwise.

-- Pete

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
@@ -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-vmware-image.c b/dc-vmware-image.c
new file mode 100644
index 0000000..90d7a18
--- /dev/null
+++ b/dc-vmware-image.c
@@ -0,0 +1,3046 @@
+/*
+ * 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];
+} __attribute__((__packed__));
+
+#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 */
+};
+
+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;
+	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 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 struct v__ServiceContent *poke_svcroot(struct config *cfg,
+   struct session *ses)
+{
+	CURL *curl = ses->curl;
+	struct api_buf apib, apob;
+	struct v__ServiceContent *sc;
+	char *proc, *reply;
+	xmlDocPtr doc;
+	xmlNode *etroot;
+	xmlNode *et;
+	CURLcode rcc;
+	int rc;
+
+	proc = "RetrieveServiceContent";
+	reply = "RetrieveServiceContentResponse";
+
+	memset(&apib, 0, sizeof(struct api_buf));
+	apib.buf = malloc(4000);
+	if (!apib.buf)
+		goto err_alloc;
+	apib.alloc = 4000;
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	rc = asprintf(&apob.buf,
+"<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\r\n"
+"<Envelope xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:vim25=\"urn:vim25\">"
+"  <Body>"
+"    <vim25:%s>"
+"      <vim25:_this type=\"ServiceInstance\" xsi:type=\"vim25:ManagedObjectReference\">ServiceInstance</vim25:_this>"
+"    </vim25:%s>"
+"  </Body>"
+"</Envelope>", proc, proc);
+	if (rc < 0)
+		goto err_alloc;
+	apob.alloc = rc;
+
+	// 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);
+	// curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, "POST");
+	// curl_easy_setopt(curl, CURLOPT_UPLOAD, 1);
+	// curl_easy_setopt(curl, CURLOPT_INFILESIZE, apob.alloc);
+
+	rcc = curl_easy_perform(curl);
+	if (rcc != CURLE_OK) {
+		fprintf(stderr, TAG ": curl failed POST url `%s'\n",
+		     cfg->apiurl);
+		/* XXX url is meaningless, needs RetrieveServiceContent */
+		exit(1);
+	}
+
+	sc = sc_alloc();
+	if (!sc)
+		goto err_alloc;
+
+	doc = xmlReadMemory(apib.buf, apib.used, "rhev-api.xml", NULL, 0);
+	if (doc == NULL) {
+		fprintf(stderr, TAG ": XML parse error, proc %s\n", proc);
+		exit(1);
+	}
+
+	etroot = xmlDocGetRootElement(doc);
+	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);
+	sc->scnode = x_getchild(et, "returnval");
+	sc->scdoc = doc;
+
+	xmlCleanupParser();
+	free(apib.buf);
+	free(apob.buf);
+	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)
+{
+	CURL *curl = ses->curl;
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	proc = "Login";
+	reply = "LoginResponse";
+
+	thisnode = x_getchild(sc->scnode, "sessionManager");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "_this", thisval);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST "SessionManager");
+	xmlAddChild(etproc, et);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "userName", BAD_CAST cfg->apiuser);
+	xmlAddChild(etproc, et);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "password", BAD_CAST cfg->apipass);
+	xmlAddChild(etproc, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+
+	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(1);
+	}
+
+	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");
+	// x_node_print(et);
+
+	xmlCleanupParser();
+
+	// xmlFreeNs(ns_v) -- does freeing Doc free namespaces? XXX
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.buf);
+	xmlFree(thisval);
+	return;
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for SessionManager\n");
+	exit(1);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static void poke_Logout(struct config *cfg, struct session *ses,
+    struct v__ServiceContent *sc)
+{
+	CURL *curl = ses->curl;
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etlogout;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	proc = "Logout";
+	reply = "LogoutResponse";
+
+	thisnode = x_getchild(sc->scnode, "sessionManager");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etlogout = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etlogout);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "_this", thisval);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST "SessionManager");
+	xmlAddChild(etlogout, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	memset(&apib, 0, sizeof(struct api_buf));
+	apib.buf = malloc(4000);
+	if (!apib.buf)
+		goto err_alloc;
+	apib.alloc = 4000;
+
+	/* 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(1);
+	}
+
+	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(1);
+	}
+
+	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);
+	/* Nothing left to verify: LogoutResponse is usually empty. */
+
+	xmlCleanupParser();
+
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.buf);
+	xmlFree(thisval);
+	return;
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for SessionManager\n");
+	exit(1);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\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)
+{
+	CURL *curl = ses->curl;
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	struct v_prop *ret;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlNode *etprop, *etobj, *etspec;
+	xmlDoc *respdoc;
+	xmlNode *etpset;
+	xmlNode *et;
+	CURLcode rcc;
+
+	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;
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "_this", thisval);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST "PropertyCollector");
+	xmlAddChild(etproc, et);
+
+	etspec = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "specSet", NULL);
+	xmlAddChild(etproc, etspec);
+
+	etprop = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "propSet", NULL);
+	xmlAddChild(etspec, etprop);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "type", BAD_CAST obj->type);
+	xmlAddChild(etprop, et);
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "all", BAD_CAST "false");
+	xmlAddChild(etprop, et);
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "pathSet", BAD_CAST path);
+	xmlAddChild(etprop, et);
+
+	etobj = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "objectSet", NULL);
+	xmlAddChild(etspec, etobj);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "obj", BAD_CAST obj->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST obj->type);
+	xmlAddChild(etobj, et);
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "skip", BAD_CAST "false");
+	xmlAddChild(etobj, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+
+	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(1);
+	}
+
+	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);
+
+	xmlCleanupParser();
+
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.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);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\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)
+{
+	CURL *curl = ses->curl;
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	xmlNode *retval;
+	struct v__ManagedObjectReference *ret;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	proc = "FindByDnsName";
+	reply = "FindByDnsNameResponse";
+
+	thisnode = x_getchild(sc->scnode, "searchIndex");
+	if (!thisnode)
+		goto err_notext_sesmgr;
+	thisval = xmlNodeGetContent(thisnode);
+	if (!thisval)
+		goto err_notext_sesmgr;
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "_this", thisval);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST "SearchIndex");
+	xmlAddChild(etproc, et);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "dnsName", BAD_CAST hostname);
+	xmlAddChild(etproc, et);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "vmSearch", BAD_CAST (do_vm?"true":"false"));
+	xmlAddChild(etproc, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+
+	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(1);
+	}
+
+	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");
+	retval = et->children;
+	if (!retval || retval->type!=XML_TEXT_NODE || !retval->content)
+		goto err_notext_retval;
+
+	ret = mor_new("HostSystem", retval->content);
+
+	xmlCleanupParser();
+
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.buf);
+	xmlFree(thisval);
+	return ret;
+
+ err_notext_retval:
+	fprintf(stderr, TAG ": No text for %s\n", reply);
+	exit(1);
+
+ err_notext_sesmgr:
+	fprintf(stderr, TAG ": No text for SearchIndex\n");
+	exit(1);
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\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)
+{
+	CURL *curl = ses->curl;
+	xmlNode *thisnode;
+	xmlChar *thisval;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	struct v__OvfCreateImportSpecResult *ret;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc, *etcisp, *etnetmap;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	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;
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "_this", thisval);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST "OvfManager");
+	xmlAddChild(etproc, et);
+
+	/* Argument 1: ovfDescriptor */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "ovfDescriptor", BAD_CAST ovfstr);
+	xmlAddChild(etproc, et);
+
+	/* Argument 2: resourcePool */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "resourcePool", BAD_CAST rp->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST rp->type);
+	xmlAddChild(etproc, et);
+
+	/* Argument 3: datastore */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "datastore", BAD_CAST ds->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST ds->type);
+	xmlAddChild(etproc, et);
+
+	/* Argument 4: cisp. Bad news, it's struct... with a list of structs. */
+	etcisp = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "cisp", NULL);
+	xmlAddChild(etproc, etcisp);
+
+	/* cisp.locale */
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "locale", BAD_CAST "US");
+	xmlAddChild(etcisp, et);
+
+	/* cisp.deploymentOption -- empty for now */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "deploymentOption", BAD_CAST "");
+	xmlAddChild(etcisp, et);
+
+	/* cisp.entityName */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "entityName", BAD_CAST cfg->push_name);
+	xmlAddChild(etcisp, et);
+
+	/* cisp.hostSystem -- optional, but set it for now. */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "hostSystem", BAD_CAST host->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST host->type);
+	xmlAddChild(etcisp, et);
+
+	/* cisp.networkMapping */
+	etnetmap = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "networkMapping", NULL);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "name", BAD_CAST net_name);
+	xmlAddChild(etnetmap, et);
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "network", BAD_CAST net->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST net->type);
+	xmlAddChild(etnetmap, et);
+
+	xmlAddChild(etcisp, etnetmap);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+/* P3 */ /* fdump("out.spec", apib.buf, apib.used); */
+
+	ret = malloc(sizeof(struct v__OvfCreateImportSpecResult));
+	if (!ret)
+		goto err_alloc;
+	memset(ret, 0, sizeof(struct v__OvfCreateImportSpecResult));
+
+	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(1);
+	}
+
+	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");
+	/*
+	 * 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;
+
+	xmlCleanupParser();
+
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.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)
+{
+	CURL *curl = ses->curl;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	struct v_lease *ret;
+	xmlChar *rettype, *retval;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc, *etspec;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	ret = malloc(sizeof(struct v_lease));
+	if (!ret)
+		goto err_alloc;
+	memset(ret, 0, sizeof(struct v_lease));
+
+	proc = "ImportVApp";
+	reply = "ImportVAppResponse";
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "_this", BAD_CAST rp->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST rp->type);
+	xmlAddChild(etproc, et);
+
+	/* 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);
+	xmlNodeSetName(etspec, BAD_CAST "spec");
+	xmlAddChild(etproc, etspec);
+
+	/* Argument 2: (MOR) folder */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "folder", BAD_CAST vmf->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST vmf->type);
+	xmlAddChild(etproc, et);
+
+	/* Argument 3: (MOR) host */
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "host", BAD_CAST host->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST host->type);
+	xmlAddChild(etproc, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+
+	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(1);
+	}
+
+	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);
+
+	xmlCleanupParser();
+
+	xmlFree(rettype);
+	xmlFree(retval);
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.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__ServiceContent *sc, struct v_lease *lease, int pcs)
+{
+	CURL *curl = ses->curl;
+	char buf100[100];
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	proc = "HttpNfcLeaseProgress";
+	reply = "HttpNfcLeaseProgressResponse";
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "_this", BAD_CAST lease->mor->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST lease->mor->type);
+	xmlAddChild(etproc, et);
+
+	/* Argument 1: percentage */
+	snprintf(buf100, 100, "%d", pcs);
+	et = xmlNewDocNode(reqdoc, ns_v, BAD_CAST "percent", BAD_CAST buf100);
+	xmlAddChild(etproc, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+
+	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(1);
+	}
+
+	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);
+	/* Response is empty. */
+
+	xmlCleanupParser();
+
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.buf);
+	return;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+static void v_lease_complete(struct config *cfg, struct session *ses,
+    struct v__ServiceContent *sc, struct v_lease *lease)
+{
+	CURL *curl = ses->curl;
+	struct api_buf apib, apob;
+	char *proc, *reply;
+	xmlChar *buf;
+	int len;
+	xmlDoc *reqdoc;
+	xmlNsPtr ns_env, ns_enc, ns_xsi, ns_xsd, ns_v;
+	xmlNode *etroot, *etbody, *etproc;
+	xmlDoc *respdoc;
+	xmlNode *et;
+	CURLcode rcc;
+
+	proc = "HttpNfcLeaseComplete";
+	reply = "HttpNfcLeaseCompleteResponse";
+
+	reqdoc = xmlNewDoc(BAD_CAST "1.0");
+	etroot = xmlNewDocNode(reqdoc, NULL, BAD_CAST "Envelope", NULL);
+	ns_env = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/envelope/",
+	    BAD_CAST "SOAP-ENV");
+	ns_enc = xmlNewNs(etroot,
+	    BAD_CAST "http://schemas.xmlsoap.org/soap/encoding/",
+	    BAD_CAST "SOAP-ENC");
+	ns_xsi = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema-instance",
+	    BAD_CAST "xsi");
+	ns_xsd = xmlNewNs(etroot,
+	    BAD_CAST "http://www.w3.org/2001/XMLSchema", BAD_CAST "xsd");
+	ns_v = xmlNewNs(etroot, BAD_CAST "urn:vim25", BAD_CAST "v");
+	xmlSetNs(etroot, ns_env);
+	xmlDocSetRootElement(reqdoc, etroot);
+
+	etbody = xmlNewDocNode(reqdoc, ns_env, BAD_CAST "Body", NULL);
+	xmlAddChild(etroot, etbody);
+
+	etproc = xmlNewDocNode(reqdoc, ns_v, BAD_CAST proc, NULL);
+	xmlAddChild(etbody, etproc);
+
+	et = xmlNewDocNode(reqdoc, ns_v,
+	    BAD_CAST "_this", BAD_CAST lease->mor->__item);
+	xmlNewProp(et, BAD_CAST "type", BAD_CAST lease->mor->type);
+	xmlAddChild(etproc, et);
+
+	xmlDocDumpFormatMemoryEnc(reqdoc, &buf, &len, "UTF-8", 1);
+
+	memset(&apob, 0, sizeof(struct api_buf));
+	apob.buf = (char *) buf;
+	apob.alloc = len;
+
+	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(1);
+	}
+
+	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(1);
+	}
+
+	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);
+	/* Response is empty. */
+
+	xmlCleanupParser();
+
+	xmlFreeDoc(respdoc);
+	xmlFreeDoc(reqdoc);
+	xmlFree(buf);
+	free(apib.buf);
+	return;
+
+ err_alloc:
+	fprintf(stderr, TAG ": No core\n");
+	exit(1);
+}
+
+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,
+    int use_put, const char *url, int tfd)
+{
+	int pipe_up_report[2];
+	pid_t uploader;
+	char status;
+	int wstat;
+	ssize_t rrc;
+	// int rc;
+
+	if (pipe(pipe_up_report) < 0) {
+		fprintf(stderr, TAG ": pipe error: %s\n", strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+
+	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);
+		status = 3;
+		write(pipe_up_report[1], &status, 1);
+		exit(0);
+	}
+
+	close(pipe_up_report[1]);
+	for (;;) {
+		rrc = read(pipe_up_report[0], &status, 1);
+		if (rrc < 0) {
+			fprintf(stderr, TAG ": pipe read error: %s\n",
+			    strerror(errno));
+			exit(EXIT_FAILURE);
+		}
+
+		if (rrc == 0)
+			break;
+
+		if (status == 3) {
+			/* yay, set to 100% */
+		} else {
+			fprintf(stderr, TAG ": bad report (0x%x)\n", status);
+			exit(EXIT_FAILURE);
+		}
+	}
+
+	if (waitpid(uploader, &wstat, 0) < 0) {
+		fprintf(stderr, TAG ": waitpid error: %s\n", strerror(errno));
+		exit(EXIT_FAILURE);
+	}
+	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);
+	}
+
+	close(pipe_up_report[0]);
+}
+
+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;
+	int cgx, gdx;
+	unsigned long inputSectorPointer, outputSectorPointer;
+	unsigned char *inChunk;
+	unsigned char *obuf;
+	size_t cbufsz;
+	unsigned long chunklen;
+	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;
+
+	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 (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 (i = 0; i < grainDirectoryEntries; i++) {
+		if (i)
+			printf(", ");
+		printf("%d", grainDirectory[i]);
+	}
+	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;
+
+	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, 0, lease->url, tfd);
+
+	v_lease_progress(cfg, &ses, sc, lease, 100);
+	v_lease_complete(cfg, &ses, sc, lease);
+
+	/*
+	 * 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(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);
+	}
+
+	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