[libvirt] [PATCH] [0/4] Managed save APIs
As posted earlier, I have implemented the small set of managed save APIs, where libvirt stores the domain state itself and can then recover that state when the domain is started up. I think the code is complete, but not really tested (I still need to debug a failure which seems unrelated), with the exception of the virsh commands which probably need to be extended for convenience. Also I implemented it only for the qemu driver, I would not be surprized if an ESX backend could be implemented since there is no file path in this API. More documentation is needed too. Thanks Chris Lalancette who wrote a large part of this code ! Daniel -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/
Add managed save API entry points virDomainManagedSave() is to be run on a running domain. Once the call complete, as in virDomainSave() the domain is stopped upon completion, but there is no restore counterpart as any order to start the domain from the API would load the state from the managed file, similary if the domain is autostarted when libvirtd starts. Once a domain has restarted his managed save image is destroyed, basically managed save image can only exist for a stopped domain, for a running domain that would be by definition outdated data. * include/libvirt/libvirt.h.in src/libvirt.c src/libvirt_public.syms: adds the new entry points virDomainManagedSave(), virDomainHasManagedSaveImage() and virDomainManagedSaveRemove() * src/driver.h src/esx/esx_driver.c src/lxc/lxc_driver.c src/opennebula/one_driver.c src/openvz/openvz_driver.c src/phyp/phyp_driver.c src/qemu/qemu_driver.c src/vbox/vbox_tmpl.c src/remote/remote_driver.c src/test/test_driver.c src/uml/uml_driver.c src/xen/xen_driver.c: add corresponding new internal drivers entry points diff --git a/include/libvirt/libvirt.h.in b/include/libvirt/libvirt.h.in index 6d8552f..431e485 100644 --- a/include/libvirt/libvirt.h.in +++ b/include/libvirt/libvirt.h.in @@ -639,6 +639,16 @@ int virDomainRestore (virConnectPtr conn, const char *from); /* + * Managed domain save + */ +int virDomainManagedSave (virDomainPtr dom, + unsigned int flags); +int virDomainHasManagedSaveImage(virDomainPtr dom, + unsigned int flags); +int virDomainManagedSaveRemove(virDomainPtr dom, + unsigned int flags); + +/* * Domain core dump */ int virDomainCoreDump (virDomainPtr domain, diff --git a/src/driver.h b/src/driver.h index 8f86463..7e2536d 100644 --- a/src/driver.h +++ b/src/driver.h @@ -402,6 +402,15 @@ typedef int (*virDrvDomainEventDeregisterAny)(virConnectPtr conn, int callbackID); +typedef int + (*virDrvDomainManagedSave)(virDomainPtr domain, unsigned int flags); + +typedef int + (*virDrvDomainHasManagedSaveImage)(virDomainPtr domain, unsigned int flags); + +typedef int + (*virDrvDomainManagedSaveRemove)(virDomainPtr domain, unsigned int flags); + /** * _virDriver: * @@ -499,6 +508,9 @@ struct _virDriver { virDrvDomainMigrateSetMaxDowntime domainMigrateSetMaxDowntime; virDrvDomainEventRegisterAny domainEventRegisterAny; virDrvDomainEventDeregisterAny domainEventDeregisterAny; + virDrvDomainManagedSave domainManagedSave; + virDrvDomainHasManagedSaveImage domainHasManagedSaveImage; + virDrvDomainManagedSaveRemove domainManagedSaveRemove; }; typedef int diff --git a/src/esx/esx_driver.c b/src/esx/esx_driver.c index 663c560..7e57fff 100644 --- a/src/esx/esx_driver.c +++ b/src/esx/esx_driver.c @@ -3408,6 +3408,9 @@ static virDriver esxDriver = { NULL, /* domainMigrateSetMaxDowntime */ NULL, /* domainEventRegisterAny */ NULL, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; diff --git a/src/libvirt.c b/src/libvirt.c index cc5b4c5..293ea17 100644 --- a/src/libvirt.c +++ b/src/libvirt.c @@ -12135,3 +12135,143 @@ error: virDispatchError(conn); return -1; } + +/** + * virDomainManagedSave: + * @dom: pointer to the domain + * @flags: optional flags currently unused + * + * This method will suspend a domain and save its memory contents to + * a file on disk. After the call, if successful, the domain is not + * listed as running anymore. + * The difference from virDomainSave() is that libvirt is keeping track of + * the saved state itself, and will reuse it once the domain is being + * restarted (automatically or via an explicit libvirt call). + * As a result any running domain is sure to not have a managed saved image. + * + * Returns 0 in case of success or -1 in case of failure + */ +int virDomainManagedSave(virDomainPtr dom, unsigned int flags) +{ + virConnectPtr conn; + + VIR_DEBUG("dom=%p, flags=%u", dom, flags); + + virResetLastError(); + + if (!VIR_IS_CONNECTED_DOMAIN(dom)) { + virLibDomainError(NULL, VIR_ERR_INVALID_DOMAIN, __FUNCTION__); + virDispatchError(NULL); + return -1; + } + + conn = dom->conn; + if (conn->flags & VIR_CONNECT_RO) { + virLibDomainError(dom, VIR_ERR_OPERATION_DENIED, __FUNCTION__); + goto error; + } + + if (conn->driver->domainManagedSave) { + int ret; + + ret = conn->driver->domainManagedSave(dom, flags); + if (ret < 0) + goto error; + return ret; + } + + virLibConnError(conn, VIR_ERR_NO_SUPPORT, __FUNCTION__); + +error: + virDispatchError(conn); + return -1; +} + +/** + * virDomainHasManagedSaveImage: + * @dom: pointer to the domain + * @flags: optional flags currently unused + * + * Check if a domain has a managed save image as created by + * virDomainManagedSave(). Note that any running domain should not have + * such an image, as it should have been removed on restart. + * + * Returns 0 if no image is present, 1 if an image is present, and + * -1 in case of error + */ +int virDomainHasManagedSaveImage(virDomainPtr dom, unsigned int flags) +{ + virConnectPtr conn; + + VIR_DEBUG("dom=%p, flags=%u", dom, flags); + + virResetLastError(); + + if (!VIR_IS_CONNECTED_DOMAIN(dom)) { + virLibDomainError(NULL, VIR_ERR_INVALID_DOMAIN, __FUNCTION__); + virDispatchError(NULL); + return -1; + } + + conn = dom->conn; + + if (conn->driver->domainHasManagedSaveImage) { + int ret; + + ret = conn->driver->domainHasManagedSaveImage(dom, flags); + if (ret < 0) + goto error; + return ret; + } + + virLibConnError(conn, VIR_ERR_NO_SUPPORT, __FUNCTION__); + +error: + virDispatchError(conn); + return -1; +} + +/** + * virDomainManagedSaveRemove: + * @dom: pointer to the domain + * @flags: optional flags currently unused + * + * Remove any managed save image as for this domain. + * + * Returns 0 in case of success, and -1 in case of error + */ +int virDomainManagedSaveRemove(virDomainPtr dom, unsigned int flags) +{ + virConnectPtr conn; + + VIR_DEBUG("dom=%p, flags=%u", dom, flags); + + virResetLastError(); + + if (!VIR_IS_CONNECTED_DOMAIN(dom)) { + virLibDomainError(NULL, VIR_ERR_INVALID_DOMAIN, __FUNCTION__); + virDispatchError(NULL); + return -1; + } + + conn = dom->conn; + if (conn->flags & VIR_CONNECT_RO) { + virLibDomainError(dom, VIR_ERR_OPERATION_DENIED, __FUNCTION__); + goto error; + } + + if (conn->driver->domainManagedSaveRemove) { + int ret; + + ret = conn->driver->domainManagedSaveRemove(dom, flags); + if (ret < 0) + goto error; + return ret; + } + + virLibConnError(conn, VIR_ERR_NO_SUPPORT, __FUNCTION__); + +error: + virDispatchError(conn); + return -1; +} diff --git a/src/libvirt_public.syms b/src/libvirt_public.syms index 2f812a1..24a422f 100644 --- a/src/libvirt_public.syms +++ b/src/libvirt_public.syms @@ -378,6 +378,9 @@ LIBVIRT_0.7.8 { virNWFilterRef; virNWFilterDefineXML; virNWFilterUndefine; + virDomainManagedSave; + virDomainHasManagedSaveImage; + virDomainManagedSaveRemove; } LIBVIRT_0.7.7; diff --git a/src/lxc/lxc_driver.c b/src/lxc/lxc_driver.c index 7ebc7ae..1049f1b 100644 --- a/src/lxc/lxc_driver.c +++ b/src/lxc/lxc_driver.c @@ -2544,6 +2544,9 @@ static virDriver lxcDriver = { NULL, /* domainMigrateSetMaxDowntime */ lxcDomainEventRegisterAny, /* domainEventRegisterAny */ lxcDomainEventDeregisterAny, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; static virStateDriver lxcStateDriver = { diff --git a/src/opennebula/one_driver.c b/src/opennebula/one_driver.c index e901a03..91d7459 100644 --- a/src/opennebula/one_driver.c +++ b/src/opennebula/one_driver.c @@ -792,6 +792,9 @@ static virDriver oneDriver = { NULL, /* domainMigrateSetMaxDowntime */ NULL, /* domainEventRegisterAny */ NULL, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; static virStateDriver oneStateDriver = { diff --git a/src/openvz/openvz_driver.c b/src/openvz/openvz_driver.c index 6176577..32bc3c2 100644 --- a/src/openvz/openvz_driver.c +++ b/src/openvz/openvz_driver.c @@ -1544,6 +1544,9 @@ static virDriver openvzDriver = { NULL, /* domainMigrateSetMaxDowntime */ NULL, /* domainEventRegisterAny */ NULL, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; int openvzRegister(void) { diff --git a/src/phyp/phyp_driver.c b/src/phyp/phyp_driver.c index 4f7efdb..0e1d35f 100644 --- a/src/phyp/phyp_driver.c +++ b/src/phyp/phyp_driver.c @@ -1651,6 +1651,9 @@ virDriver phypDriver = { NULL, /* domainMigrateSetMaxDowntime */ NULL, /* domainEventRegisterAny */ NULL, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; int diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 9ee5da7..17e6118 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -10240,6 +10240,9 @@ static virDriver qemuDriver = { qemuDomainMigrateSetMaxDowntime, /* domainMigrateSetMaxDowntime */ qemuDomainEventRegisterAny, /* domainEventRegisterAny */ qemuDomainEventDeregisterAny, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index 3b8be21..9b500d0 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -9818,6 +9818,9 @@ static virDriver remote_driver = { remoteDomainMigrateSetMaxDowntime, /* domainMigrateSetMaxDowntime */ remoteDomainEventRegisterAny, /* domainEventRegisterAny */ remoteDomainEventDeregisterAny, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; static virNetworkDriver network_driver = { diff --git a/src/test/test_driver.c b/src/test/test_driver.c index 646c7db..105f825 100644 --- a/src/test/test_driver.c +++ b/src/test/test_driver.c @@ -5306,6 +5306,9 @@ static virDriver testDriver = { NULL, /* domainMigrateSetMaxDowntime */ testDomainEventRegisterAny, /* domainEventRegisterAny */ testDomainEventDeregisterAny, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; static virNetworkDriver testNetworkDriver = { diff --git a/src/uml/uml_driver.c b/src/uml/uml_driver.c index 835e5d4..5fa1beb 100644 --- a/src/uml/uml_driver.c +++ b/src/uml/uml_driver.c @@ -1936,6 +1936,9 @@ static virDriver umlDriver = { NULL, /* domainMigrateSetMaxDowntime */ NULL, /* domainEventRegisterAny */ NULL, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; diff --git a/src/vbox/vbox_tmpl.c b/src/vbox/vbox_tmpl.c index f7a9b9f..86f6a5a 100644 --- a/src/vbox/vbox_tmpl.c +++ b/src/vbox/vbox_tmpl.c @@ -7188,6 +7188,9 @@ virDriver NAME(Driver) = { vboxDomainEventRegisterAny, /* domainEventRegisterAny */ vboxDomainEventDeregisterAny, /* domainEventDeregisterAny */ #endif + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; virNetworkDriver NAME(NetworkDriver) = { diff --git a/src/xen/xen_driver.c b/src/xen/xen_driver.c index ebdc600..a5d58d0 100644 --- a/src/xen/xen_driver.c +++ b/src/xen/xen_driver.c @@ -1980,6 +1980,9 @@ static virDriver xenUnifiedDriver = { NULL, /* domainMigrateSetMaxDowntime */ xenUnifiedDomainEventRegisterAny, /* domainEventRegisterAny */ xenUnifiedDomainEventDeregisterAny, /* domainEventDeregisterAny */ + NULL, /* domainManagedSave */ + NULL, /* domainHasManagedSaveImage */ + NULL, /* domainManagedSaveRemove */ }; /** -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/
* src/remote/remote_protocol.x src/remote/remote_protocol.h src/remote/remote_protocol.c src/remote/remote_driver.c: add the entry points in the remote driver * daemon/remote.c daemon/remote_dispatch_args.h daemon/remote_dispatch_prototypes.h daemon/remote_dispatch_table.h: and implement the daemon counterpart diff --git a/daemon/remote.c b/daemon/remote.c index 67162d5..b708027 100644 --- a/daemon/remote.c +++ b/daemon/remote.c @@ -2416,6 +2416,84 @@ remoteDispatchListDomains (struct qemud_server *server ATTRIBUTE_UNUSED, } static int +remoteDispatchDomainManagedSave (struct qemud_server *server ATTRIBUTE_UNUSED, + struct qemud_client *client ATTRIBUTE_UNUSED, + virConnectPtr conn, + remote_message_header *hdr ATTRIBUTE_UNUSED, + remote_error *rerr, + remote_domain_managed_save_args *args, + void *ret ATTRIBUTE_UNUSED) +{ + virDomainPtr dom; + + dom = get_nonnull_domain (conn, args->dom); + if (dom == NULL) { + remoteDispatchConnError(rerr, conn); + return -1; + } + + if (virDomainManagedSave (dom, args->flags) == -1) { + virDomainFree(dom); + remoteDispatchConnError(rerr, conn); + return -1; + } + virDomainFree(dom); + return 0; +} + +static int +remoteDispatchDomainHasManagedSaveImage (struct qemud_server *server ATTRIBUTE_UNUSED, + struct qemud_client *client ATTRIBUTE_UNUSED, + virConnectPtr conn, + remote_message_header *hdr ATTRIBUTE_UNUSED, + remote_error *rerr, + remote_domain_has_managed_save_image_args *args, + void *ret ATTRIBUTE_UNUSED) +{ + virDomainPtr dom; + + dom = get_nonnull_domain (conn, args->dom); + if (dom == NULL) { + remoteDispatchConnError(rerr, conn); + return -1; + } + + if (virDomainHasManagedSaveImage (dom, args->flags) == -1) { + virDomainFree(dom); + remoteDispatchConnError(rerr, conn); + return -1; + } + virDomainFree(dom); + return 0; +} + +static int +remoteDispatchDomainManagedSaveRemove (struct qemud_server *server ATTRIBUTE_UNUSED, + struct qemud_client *client ATTRIBUTE_UNUSED, + virConnectPtr conn, + remote_message_header *hdr ATTRIBUTE_UNUSED, + remote_error *rerr, + remote_domain_managed_save_remove_args *args, + void *ret ATTRIBUTE_UNUSED) +{ + virDomainPtr dom; + + dom = get_nonnull_domain (conn, args->dom); + if (dom == NULL) { + remoteDispatchConnError(rerr, conn); + return -1; + } + + if (virDomainManagedSaveRemove (dom, args->flags) == -1) { + virDomainFree(dom); + remoteDispatchConnError(rerr, conn); + return -1; + } + virDomainFree(dom); + return 0; +} + +static int remoteDispatchListNetworks (struct qemud_server *server ATTRIBUTE_UNUSED, struct qemud_client *client ATTRIBUTE_UNUSED, virConnectPtr conn, diff --git a/daemon/remote_dispatch_args.h b/daemon/remote_dispatch_args.h index b188e6e..51c1675 100644 --- a/daemon/remote_dispatch_args.h +++ b/daemon/remote_dispatch_args.h @@ -151,3 +151,6 @@ remote_list_nwfilters_args val_remote_list_nwfilters_args; remote_nwfilter_define_xml_args val_remote_nwfilter_define_xml_args; remote_nwfilter_undefine_args val_remote_nwfilter_undefine_args; + remote_domain_managed_save_args val_remote_domain_managed_save_args; + remote_domain_has_managed_save_image_args val_remote_domain_has_managed_save_image_args; + remote_domain_managed_save_remove_args val_remote_domain_managed_save_remove_args; diff --git a/daemon/remote_dispatch_prototypes.h b/daemon/remote_dispatch_prototypes.h index e155c69..3671a26 100644 --- a/daemon/remote_dispatch_prototypes.h +++ b/daemon/remote_dispatch_prototypes.h @@ -282,6 +282,14 @@ static int remoteDispatchDomainGetVcpus( remote_error *err, remote_domain_get_vcpus_args *args, remote_domain_get_vcpus_ret *ret); +static int remoteDispatchDomainHasManagedSaveImage( + struct qemud_server *server, + struct qemud_client *client, + virConnectPtr conn, + remote_message_header *hdr, + remote_error *err, + remote_domain_has_managed_save_image_args *args, + void *ret); static int remoteDispatchDomainInterfaceStats( struct qemud_server *server, struct qemud_client *client, @@ -330,6 +338,22 @@ static int remoteDispatchDomainLookupByUuid( remote_error *err, remote_domain_lookup_by_uuid_args *args, remote_domain_lookup_by_uuid_ret *ret); +static int remoteDispatchDomainManagedSave( + struct qemud_server *server, + struct qemud_client *client, + virConnectPtr conn, + remote_message_header *hdr, + remote_error *err, + remote_domain_managed_save_args *args, + void *ret); +static int remoteDispatchDomainManagedSaveRemove( + struct qemud_server *server, + struct qemud_client *client, + virConnectPtr conn, + remote_message_header *hdr, + remote_error *err, + remote_domain_managed_save_remove_args *args, + void *ret); static int remoteDispatchDomainMemoryPeek( struct qemud_server *server, struct qemud_client *client, diff --git a/daemon/remote_dispatch_table.h b/daemon/remote_dispatch_table.h index 24756d7..ac54f00 100644 --- a/daemon/remote_dispatch_table.h +++ b/daemon/remote_dispatch_table.h @@ -912,3 +912,18 @@ .args_filter = (xdrproc_t) xdr_remote_nwfilter_undefine_args, .ret_filter = (xdrproc_t) xdr_void, }, +{ /* DomainManagedSave => 182 */ + .fn = (dispatch_fn) remoteDispatchDomainManagedSave, + .args_filter = (xdrproc_t) xdr_remote_domain_managed_save_args, + .ret_filter = (xdrproc_t) xdr_void, +}, +{ /* DomainHasManagedSaveImage => 183 */ + .fn = (dispatch_fn) remoteDispatchDomainHasManagedSaveImage, + .args_filter = (xdrproc_t) xdr_remote_domain_has_managed_save_image_args, + .ret_filter = (xdrproc_t) xdr_void, +}, +{ /* DomainManagedSaveRemove => 184 */ + .fn = (dispatch_fn) remoteDispatchDomainManagedSaveRemove, + .args_filter = (xdrproc_t) xdr_remote_domain_managed_save_remove_args, + .ret_filter = (xdrproc_t) xdr_void, +}, diff --git a/src/remote/remote_driver.c b/src/remote/remote_driver.c index 9b500d0..47e9c7d 100644 --- a/src/remote/remote_driver.c +++ b/src/remote/remote_driver.c @@ -3616,6 +3616,78 @@ done: return rv; } +static int +remoteDomainManagedSave (virDomainPtr domain, unsigned int flags) +{ + int rv = -1; + remote_domain_managed_save_args args; + struct private_data *priv = domain->conn->privateData; + + remoteDriverLock(priv); + + make_nonnull_domain (&args.dom, domain); + args.flags = flags; + + if (call (domain->conn, priv, 0, REMOTE_PROC_DOMAIN_MANAGED_SAVE, + (xdrproc_t) xdr_remote_domain_managed_save_args, (char *) &args, + (xdrproc_t) xdr_void, (char *) NULL) == -1) + goto done; + + rv = 0; + +done: + remoteDriverUnlock(priv); + return rv; +} + +static int +remoteDomainHasManagedSaveImage (virDomainPtr domain, unsigned int flags) +{ + int rv = -1; + remote_domain_has_managed_save_image_args args; + struct private_data *priv = domain->conn->privateData; + + remoteDriverLock(priv); + + make_nonnull_domain (&args.dom, domain); + args.flags = flags; + + if (call (domain->conn, priv, 0, REMOTE_PROC_DOMAIN_HAS_MANAGED_SAVE_IMAGE, + (xdrproc_t) xdr_remote_domain_has_managed_save_image_args, (char *) &args, + (xdrproc_t) xdr_void, (char *) NULL) == -1) + goto done; + + rv = 0; + +done: + remoteDriverUnlock(priv); + return rv; +} + +static int +remoteDomainManagedSaveRemove (virDomainPtr domain, unsigned int flags) +{ + int rv = -1; + remote_domain_managed_save_remove_args args; + struct private_data *priv = domain->conn->privateData; + + remoteDriverLock(priv); + + make_nonnull_domain (&args.dom, domain); + args.flags = flags; + + if (call (domain->conn, priv, 0, REMOTE_PROC_DOMAIN_MANAGED_SAVE_REMOVE, + (xdrproc_t) xdr_remote_domain_managed_save_remove_args, (char *) &args, + (xdrproc_t) xdr_void, (char *) NULL) == -1) + goto done; + + rv = 0; + +done: + remoteDriverUnlock(priv); + return rv; +} + /*----------------------------------------------------------------------*/ static virDrvOpenStatus ATTRIBUTE_NONNULL (1) @@ -9818,9 +9890,9 @@ static virDriver remote_driver = { remoteDomainMigrateSetMaxDowntime, /* domainMigrateSetMaxDowntime */ remoteDomainEventRegisterAny, /* domainEventRegisterAny */ remoteDomainEventDeregisterAny, /* domainEventDeregisterAny */ - NULL, /* domainManagedSave */ - NULL, /* domainHasManagedSaveImage */ - NULL, /* domainManagedSaveRemove */ + remoteDomainManagedSave, /* domainManagedSave */ + remoteDomainHasManagedSaveImage, /* domainHasManagedSaveImage */ + remoteDomainManagedSaveRemove, /* domainManagedSaveRemove */ }; static virNetworkDriver network_driver = { diff --git a/src/remote/remote_protocol.c b/src/remote/remote_protocol.c index f252d85..47a98e0 100644 --- a/src/remote/remote_protocol.c +++ b/src/remote/remote_protocol.c @@ -3289,6 +3289,39 @@ xdr_remote_domain_event_graphics_msg (XDR *xdrs, remote_domain_event_graphics_ms } bool_t +xdr_remote_domain_managed_save_args (XDR *xdrs, remote_domain_managed_save_args *objp) +{ + + if (!xdr_remote_nonnull_domain (xdrs, &objp->dom)) + return FALSE; + if (!xdr_u_int (xdrs, &objp->flags)) + return FALSE; + return TRUE; +} + +bool_t +xdr_remote_domain_has_managed_save_image_args (XDR *xdrs, remote_domain_has_managed_save_image_args *objp) +{ + + if (!xdr_remote_nonnull_domain (xdrs, &objp->dom)) + return FALSE; + if (!xdr_u_int (xdrs, &objp->flags)) + return FALSE; + return TRUE; +} + +bool_t +xdr_remote_domain_managed_save_remove_args (XDR *xdrs, remote_domain_managed_save_remove_args *objp) +{ + + if (!xdr_remote_nonnull_domain (xdrs, &objp->dom)) + return FALSE; + if (!xdr_u_int (xdrs, &objp->flags)) + return FALSE; + return TRUE; +} + +bool_t xdr_remote_procedure (XDR *xdrs, remote_procedure *objp) { diff --git a/src/remote/remote_protocol.h b/src/remote/remote_protocol.h index d898502..45dc652 100644 --- a/src/remote/remote_protocol.h +++ b/src/remote/remote_protocol.h @@ -1860,6 +1860,24 @@ struct remote_domain_event_graphics_msg { } subject; }; typedef struct remote_domain_event_graphics_msg remote_domain_event_graphics_msg; + +struct remote_domain_managed_save_args { + remote_nonnull_domain dom; + u_int flags; +}; +typedef struct remote_domain_managed_save_args remote_domain_managed_save_args; + +struct remote_domain_has_managed_save_image_args { + remote_nonnull_domain dom; + u_int flags; +}; +typedef struct remote_domain_has_managed_save_image_args remote_domain_has_managed_save_image_args; + +struct remote_domain_managed_save_remove_args { + remote_nonnull_domain dom; + u_int flags; +}; +typedef struct remote_domain_managed_save_remove_args remote_domain_managed_save_remove_args; #define REMOTE_PROGRAM 0x20008086 #define REMOTE_PROTOCOL_VERSION 1 @@ -2045,6 +2063,9 @@ enum remote_procedure { REMOTE_PROC_LIST_NWFILTERS = 179, REMOTE_PROC_NWFILTER_DEFINE_XML = 180, REMOTE_PROC_NWFILTER_UNDEFINE = 181, + REMOTE_PROC_DOMAIN_MANAGED_SAVE = 182, + REMOTE_PROC_DOMAIN_HAS_MANAGED_SAVE_IMAGE = 183, + REMOTE_PROC_DOMAIN_MANAGED_SAVE_REMOVE = 184, }; typedef enum remote_procedure remote_procedure; @@ -2380,6 +2401,9 @@ extern bool_t xdr_remote_domain_event_io_error_msg (XDR *, remote_domain_event_ extern bool_t xdr_remote_domain_event_graphics_address (XDR *, remote_domain_event_graphics_address*); extern bool_t xdr_remote_domain_event_graphics_identity (XDR *, remote_domain_event_graphics_identity*); extern bool_t xdr_remote_domain_event_graphics_msg (XDR *, remote_domain_event_graphics_msg*); +extern bool_t xdr_remote_domain_managed_save_args (XDR *, remote_domain_managed_save_args*); +extern bool_t xdr_remote_domain_has_managed_save_image_args (XDR *, remote_domain_has_managed_save_image_args*); +extern bool_t xdr_remote_domain_managed_save_remove_args (XDR *, remote_domain_managed_save_remove_args*); extern bool_t xdr_remote_procedure (XDR *, remote_procedure*); extern bool_t xdr_remote_message_type (XDR *, remote_message_type*); extern bool_t xdr_remote_message_status (XDR *, remote_message_status*); @@ -2689,6 +2713,9 @@ extern bool_t xdr_remote_domain_event_io_error_msg (); extern bool_t xdr_remote_domain_event_graphics_address (); extern bool_t xdr_remote_domain_event_graphics_identity (); extern bool_t xdr_remote_domain_event_graphics_msg (); +extern bool_t xdr_remote_domain_managed_save_args (); +extern bool_t xdr_remote_domain_has_managed_save_image_args (); +extern bool_t xdr_remote_domain_managed_save_remove_args (); extern bool_t xdr_remote_procedure (); extern bool_t xdr_remote_message_type (); extern bool_t xdr_remote_message_status (); diff --git a/src/remote/remote_protocol.x b/src/remote/remote_protocol.x index bf87c33..f86c7f1 100644 --- a/src/remote/remote_protocol.x +++ b/src/remote/remote_protocol.x @@ -1647,6 +1647,21 @@ struct remote_domain_event_graphics_msg { remote_domain_event_graphics_identity subject<REMOTE_DOMAIN_EVENT_GRAPHICS_IDENTITY_MAX>; }; +struct remote_domain_managed_save_args { + remote_nonnull_domain dom; + unsigned flags; +}; + +struct remote_domain_has_managed_save_image_args { + remote_nonnull_domain dom; + unsigned flags; +}; + +struct remote_domain_managed_save_remove_args { + remote_nonnull_domain dom; + unsigned flags; +}; + /*----- Protocol. -----*/ /* Define the program number, protocol version and procedure numbers here. */ @@ -1852,7 +1867,10 @@ enum remote_procedure { REMOTE_PROC_LIST_NWFILTERS = 179, REMOTE_PROC_NWFILTER_DEFINE_XML = 180, - REMOTE_PROC_NWFILTER_UNDEFINE = 181 + REMOTE_PROC_NWFILTER_UNDEFINE = 181, + REMOTE_PROC_DOMAIN_MANAGED_SAVE = 182, + REMOTE_PROC_DOMAIN_HAS_MANAGED_SAVE_IMAGE = 183, + REMOTE_PROC_DOMAIN_MANAGED_SAVE_REMOVE = 184 /* * Notice how the entries are grouped in sets of 10 ? -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/
The images are saved in /var/run/libvirt/qemu and named $domainname.save . The directory is created appropriately at daemon startup. When a domain is started while a saved image is available, libvirt will try to load this saved image, and start the domain as usual in case of failure. In any case the saved image is discarded once the domain is created. * src/qemu/qemu_conf.h: adds an extra save path to the driver config * src/qemu/qemu_driver.c: implement the 3 new operations and handling of the image directory diff --git a/src/qemu/qemu_conf.h b/src/qemu/qemu_conf.h index 39518ca..0b247d6 100644 --- a/src/qemu/qemu_conf.h +++ b/src/qemu/qemu_conf.h @@ -120,6 +120,7 @@ struct qemud_driver { * the QEMU user/group */ char *libDir; char *cacheDir; + char *saveDir; unsigned int vncTLS : 1; unsigned int vncTLSx509verify : 1; unsigned int vncSASL : 1; diff --git a/src/qemu/qemu_driver.c b/src/qemu/qemu_driver.c index 17e6118..359de2e 100644 --- a/src/qemu/qemu_driver.c +++ b/src/qemu/qemu_driver.c @@ -1332,6 +1332,9 @@ qemudStartup(int privileged) { if (virAsprintf(&qemu_driver->cacheDir, "%s/cache/libvirt/qemu", LOCAL_STATE_DIR) == -1) goto out_of_memory; + if (virAsprintf(&qemu_driver->saveDir, + "%s/run/libvirt/qemu/save", LOCAL_STATE_DIR) == -1) + goto out_of_memory; } else { uid_t uid = geteuid(); char *userdir = virGetUserDirectory(uid); @@ -1356,6 +1359,8 @@ qemudStartup(int privileged) { goto out_of_memory; if (virAsprintf(&qemu_driver->cacheDir, "%s/qemu/cache", base) == -1) goto out_of_memory; + if (virAsprintf(&qemu_driver->saveDir, "%s/qemu/save", base) == -1) + goto out_of_memory; } if (virFileMakePath(qemu_driver->stateDir) != 0) { @@ -1376,6 +1381,12 @@ qemudStartup(int privileged) { qemu_driver->cacheDir, virStrerror(errno, ebuf, sizeof ebuf)); goto error; } + if (virFileMakePath(qemu_driver->saveDir) != 0) { + char ebuf[1024]; + VIR_ERROR(_("Failed to create save dir '%s': %s"), + qemu_driver->saveDir, virStrerror(errno, ebuf, sizeof ebuf)); + goto error; + } /* Configuration paths are either ~/.libvirt/qemu/... (session) or * /etc/libvirt/qemu/... (system). @@ -1426,6 +1437,12 @@ qemudStartup(int privileged) { qemu_driver->cacheDir, qemu_driver->user, qemu_driver->group); goto error; } + if (chown(qemu_driver->saveDir, qemu_driver->user, qemu_driver->group) < 0) { + virReportSystemError(errno, + _("unable to set ownership of '%s' to %d:%d"), + qemu_driver->saveDir, qemu_driver->user, qemu_driver->group); + goto error; + } } /* If hugetlbfs is present, then we need to create a sub-directory within @@ -1578,6 +1595,7 @@ qemudShutdown(void) { VIR_FREE(qemu_driver->stateDir); VIR_FREE(qemu_driver->libDir); VIR_FREE(qemu_driver->cacheDir); + VIR_FREE(qemu_driver->saveDir); VIR_FREE(qemu_driver->vncTLSx509certdir); VIR_FREE(qemu_driver->vncListen); VIR_FREE(qemu_driver->vncPassword); @@ -4503,8 +4521,8 @@ endjob: } -static int qemudDomainSave(virDomainPtr dom, - const char *path) +static int qemudDomainSaveFlag(virDomainPtr dom, const char *path, + int compressed) { struct qemud_driver *driver = dom->conn->privateData; virDomainObjPtr vm = NULL; @@ -4522,18 +4540,8 @@ static int qemudDomainSave(virDomainPtr dom, header.version = QEMUD_SAVE_VERSION; qemuDriverLock(driver); - if (driver->saveImageFormat == NULL) - header.compressed = QEMUD_SAVE_FORMAT_RAW; - else { - header.compressed = - qemudSaveCompressionTypeFromString(driver->saveImageFormat); - if (header.compressed < 0) { - qemuReportError(VIR_ERR_OPERATION_FAILED, - "%s", _("Invalid save image format specified " - "in configuration file")); - goto cleanup; - } - } + + header.compressed = compressed; vm = virDomainFindByUUID(&driver->domains, dom->uuid); @@ -4762,6 +4770,165 @@ cleanup: return ret; } +static int qemudDomainSave(virDomainPtr dom, const char *path) +{ + struct qemud_driver *driver = dom->conn->privateData; + int compressed; + + /* Hm, is this safe against qemudReload? */ + if (driver->saveImageFormat == NULL) + compressed = QEMUD_SAVE_FORMAT_RAW; + else { + compressed = qemudSaveCompressionTypeFromString(driver->saveImageFormat); + if (compressed < 0) { + qemuReportError(VIR_ERR_OPERATION_FAILED, + "%s", _("Invalid save image format specified " + "in configuration file")); + return -1; + } + } + + return qemudDomainSaveFlag(dom, path, compressed); +} + +static char * +qemuDomainManagedSavePath(struct qemud_driver *driver, virDomainObjPtr vm) { + char *ret; + + if (virAsprintf(&ret, "%s/%s.save", driver->saveDir, vm->def->name) < 0) { + virReportOOMError(); + return(NULL); + } + + return(ret); +} + +static int +qemuDomainManagedSave(virDomainPtr dom, + unsigned int flags ATTRIBUTE_UNUSED) +{ + struct qemud_driver *driver = dom->conn->privateData; + virDomainObjPtr vm = NULL; + char *name = NULL; + int ret = -1; + int compressed; + + qemuDriverLock(driver); + vm = virDomainFindByUUID(&driver->domains, dom->uuid); + if (!vm) { + char uuidstr[VIR_UUID_STRING_BUFLEN]; + virUUIDFormat(dom->uuid, uuidstr); + qemuReportError(VIR_ERR_NO_DOMAIN, + _("no domain with matching uuid '%s'"), uuidstr); + goto error; + } + + if (!virDomainObjIsActive(vm)) { + qemuReportError(VIR_ERR_OPERATION_INVALID, + "%s", _("domain is not running")); + goto error; + } + + name = qemuDomainManagedSavePath(driver, vm); + if (name == NULL) + goto error; + + VIR_DEBUG("Saving state to %s", name); + + /* FIXME: we should take the flags parameter, and use bits out + * of there to control whether we are compressing or not + */ + compressed = QEMUD_SAVE_FORMAT_RAW; + + /* we have to drop our locks here because qemudDomainSaveFlag is + * going to pick them back up. Unfortunately it opens a race window + * between us dropping and qemudDomainSaveFlag picking it back up, but + * if we want to allow other operations to be able to happen while + * qemuDomainSaveFlag is running (possibly for a long time), I'm not + * sure if there is a better solution + */ + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + + ret = qemudDomainSaveFlag(dom, name, compressed); + +cleanup: + VIR_FREE(name); + + return ret; + +error: + if (vm) + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + goto cleanup; +} + +static int +qemuDomainHasManagedSaveImage(virDomainPtr dom, + unsigned int flags ATTRIBUTE_UNUSED) +{ + struct qemud_driver *driver = dom->conn->privateData; + virDomainObjPtr vm = NULL; + int ret = -1; + char *name = NULL; + + qemuDriverLock(driver); + vm = virDomainFindByUUID(&driver->domains, dom->uuid); + if (!vm) { + char uuidstr[VIR_UUID_STRING_BUFLEN]; + virUUIDFormat(dom->uuid, uuidstr); + qemuReportError(VIR_ERR_NO_DOMAIN, + _("no domain with matching uuid '%s'"), uuidstr); + goto cleanup; + } + + name = qemuDomainManagedSavePath(driver, vm); + if (name == NULL) + goto cleanup; + + ret = virFileExists(name); + +cleanup: + VIR_FREE(name); + if (vm) + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + return ret; +} + +static int +qemuDomainManagedSaveRemove(virDomainPtr dom, + unsigned int flags ATTRIBUTE_UNUSED) +{ + struct qemud_driver *driver = dom->conn->privateData; + virDomainObjPtr vm = NULL; + int ret = -1; + char *name = NULL; + + qemuDriverLock(driver); + vm = virDomainFindByUUID(&driver->domains, dom->uuid); + if (!vm) { + char uuidstr[VIR_UUID_STRING_BUFLEN]; + virUUIDFormat(dom->uuid, uuidstr); + qemuReportError(VIR_ERR_NO_DOMAIN, + _("no domain with matching uuid '%s'"), uuidstr); + goto cleanup; + } + + name = qemuDomainManagedSavePath(driver, vm); + if (name == NULL) + goto cleanup; + + ret = unlink(name); + +cleanup: + VIR_FREE(name); + if (vm) + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + return ret; +} static int qemudDomainCoreDump(virDomainPtr dom, const char *path, @@ -5907,6 +6074,7 @@ static int qemudDomainStart(virDomainPtr dom) { virDomainObjPtr vm; int ret = -1; virDomainEventPtr event = NULL; + char *managed_save = NULL; qemuDriverLock(driver); vm = virDomainFindByUUID(&driver->domains, dom->uuid); @@ -5928,6 +6096,29 @@ static int qemudDomainStart(virDomainPtr dom) { goto endjob; } + /* + * If there is a managed saved state restore it instead of starting + * from scratch. In any case the old state is removed. + */ + managed_save = qemuDomainManagedSavePath(driver, vm); + if ((managed_save) && (virFileExists(managed_save))) { + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + ret = qemudDomainRestore(dom->conn, managed_save); + + if (unlink(managed_save) < 0) { + VIR_WARN("Failed to remove the mnaged state %s", managed_save); + } + + if (ret == 0) { + /* qemudDomainRestore should have sent the Started/Restore event */ + VIR_FREE(managed_save); + return(ret); + } + qemuDriverLock(driver); + virDomainObjLock(vm); + } + ret = qemudStartVMDaemon(dom->conn, driver, vm, NULL, -1); if (ret != -1) event = virDomainEventNewFromObj(vm, @@ -5939,6 +6130,7 @@ endjob: vm = NULL; cleanup: + VIR_FREE(managed_save); if (vm) virDomainObjUnlock(vm); if (event) @@ -10240,9 +10432,9 @@ static virDriver qemuDriver = { qemuDomainMigrateSetMaxDowntime, /* domainMigrateSetMaxDowntime */ qemuDomainEventRegisterAny, /* domainEventRegisterAny */ qemuDomainEventDeregisterAny, /* domainEventDeregisterAny */ - NULL, /* domainManagedSave */ - NULL, /* domainHasManagedSaveImage */ - NULL, /* domainManagedSaveRemove */ + qemuDomainManagedSave, /* domainManagedSave */ + qemuDomainHasManagedSaveImage, /* domainHasManagedSaveImage */ + qemuDomainManagedSaveRemove, /* domainManagedSaveRemove */ }; -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/
On 04/01/2010 10:22 AM, Daniel Veillard wrote:
@@ -5928,6 +6096,29 @@ static int qemudDomainStart(virDomainPtr dom) { goto endjob; }
+ /* + * If there is a managed saved state restore it instead of starting + * from scratch. In any case the old state is removed. + */ + managed_save = qemuDomainManagedSavePath(driver, vm); + if ((managed_save) && (virFileExists(managed_save))) { + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + ret = qemudDomainRestore(dom->conn, managed_save); + + if (unlink(managed_save) < 0) { + VIR_WARN("Failed to remove the mnaged state %s", managed_save);
s/mnaged/managed/ -- Eric Blake eblake@redhat.com +1-801-349-2682 Libvirt virtualization library http://libvirt.org
On 04/01/2010 12:22 PM, Daniel Veillard wrote:
@@ -5928,6 +6096,29 @@ static int qemudDomainStart(virDomainPtr dom) { goto endjob; }
+ /* + * If there is a managed saved state restore it instead of starting + * from scratch. In any case the old state is removed. + */ + managed_save = qemuDomainManagedSavePath(driver, vm); + if ((managed_save)&& (virFileExists(managed_save))) { + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + ret = qemudDomainRestore(dom->conn, managed_save);
There is problem here. qemudDomainRestore will call qemuDomainObjBeginJobWithDriver, which was already called just above this bit of code in qemudDomainStart. The result will be a temporary deadlock, while the semaphore wait times out, followed by failure. Either qemudDomainStart needs to call that function later, or else it needs to call qemuDomainObjEndJob before calling qemudDomainRestore. (Unfortunately, I haven't yet had the time to do any more of a review other than build and try running the code.)
On Fri, Apr 02, 2010 at 11:54:59AM -0400, Laine Stump wrote:
On 04/01/2010 12:22 PM, Daniel Veillard wrote:
@@ -5928,6 +6096,29 @@ static int qemudDomainStart(virDomainPtr dom) { goto endjob; }
+ /* + * If there is a managed saved state restore it instead of starting + * from scratch. In any case the old state is removed. + */ + managed_save = qemuDomainManagedSavePath(driver, vm); + if ((managed_save)&& (virFileExists(managed_save))) { + virDomainObjUnlock(vm); + qemuDriverUnlock(driver); + ret = qemudDomainRestore(dom->conn, managed_save);
There is problem here.
qemudDomainRestore will call qemuDomainObjBeginJobWithDriver, which was already called just above this bit of code in qemudDomainStart. The result will be a temporary deadlock, while the semaphore wait times out, followed by failure.
Either qemudDomainStart needs to call that function later, or else it needs to call qemuDomainObjEndJob before calling qemudDomainRestore.
(Unfortunately, I haven't yet had the time to do any more of a review other than build and try running the code.)
Right, spot on ! With the fix and the restore patch from Chris the cycle seems to work just fine for me now :-) I will integrate the feedback and update the patches, thanks ! Daniel -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/
This command implements the managed save operation * tools/virsh.c: new command * tools/virsh.pod: documentation diff --git a/tools/virsh.c b/tools/virsh.c index 5c56fa6..6d01fa4 100644 --- a/tools/virsh.c +++ b/tools/virsh.c @@ -1333,6 +1333,44 @@ cmdSave(vshControl *ctl, const vshCmd *cmd) } /* + * "managedsave" command + */ +static const vshCmdInfo info_managedsave[] = { + {"help", N_("managed save of a domain state")}, + {"desc", N_("Save a running domain the data being managed by libvirt.")}, + {NULL, NULL} +}; + +static const vshCmdOptDef opts_managedsave[] = { + {"domain", VSH_OT_DATA, VSH_OFLAG_REQ, N_("domain name, id or uuid")}, + {NULL, 0, 0, NULL} +}; + +static int +cmdManagedSave(vshControl *ctl, const vshCmd *cmd) +{ + virDomainPtr dom; + char *name; + int ret = TRUE; + + if (!vshConnectionUsability(ctl, ctl->conn, TRUE)) + return FALSE; + + if (!(dom = vshCommandOptDomain(ctl, cmd, &name))) + return FALSE; + + if (virDomainManagedSave(dom, 0) == 0) { + vshPrint(ctl, _("Domain %s state saved by libvirt\n"), name); + } else { + vshError(ctl, _("Failed to save domain %s state"), name); + ret = FALSE; + } + + virDomainFree(dom); + return ret; +} + +/* * "schedinfo" command */ static const vshCmdInfo info_schedinfo[] = { @@ -8208,6 +8246,8 @@ static const vshCmdDef commands[] = { {"iface-start", cmdInterfaceStart, opts_interface_start, info_interface_start}, {"iface-destroy", cmdInterfaceDestroy, opts_interface_destroy, info_interface_destroy}, + {"managedsave", cmdManagedSave, opts_managedsave, info_managedsave}, + {"nodeinfo", cmdNodeinfo, NULL, info_nodeinfo}, {"nodedev-list", cmdNodeListDevices, opts_node_list_devices, info_node_list_devices}, diff --git a/tools/virsh.pod b/tools/virsh.pod index fc4a70c..603fc16 100644 --- a/tools/virsh.pod +++ b/tools/virsh.pod @@ -335,6 +335,12 @@ except that it does some error checking. The editor used can be supplied by the C<$VISUAL> or C<$EDITOR> environment variables, and defaults to C<vi>. +=item B<namagedsave> I<domain-id> + +Ask libvirt to save a running domain state in a place managed by libvirt. +If libvirt is asked to restart the domain later on it will resume it from +the saved domain state (and the state is discarded). + =item B<migrate> optional I<--live> I<--suspend> I<domain-id> I<desturi> I<migrateuri> Migrate domain to another host. Add --live for live migration; --suspend -- Daniel Veillard | libxml Gnome XML XSLT toolkit http://xmlsoft.org/ daniel@veillard.com | Rpmfind RPM search engine http://rpmfind.net/ http://veillard.com/ | virtualization library http://libvirt.org/
On 04/01/2010 10:23 AM, Daniel Veillard wrote:
+static const vshCmdInfo info_managedsave[] = { + {"help", N_("managed save of a domain state")}, + {"desc", N_("Save a running domain the data being managed by libvirt.")},
That reads awkwardly. This is longer, but may be a better description (I'm not sure how to shorten it): Save and stop a running domain, so libvirt can later restart it from the same point.
+++ b/tools/virsh.pod @@ -335,6 +335,12 @@ except that it does some error checking. The editor used can be supplied by the C<$VISUAL> or C<$EDITOR> environment variables, and defaults to C<vi>.
+=item B<namagedsave> I<domain-id>
s/namaged/managed/ -- Eric Blake eblake@redhat.com +1-801-349-2682 Libvirt virtualization library http://libvirt.org
participants (3)
-
Daniel Veillard -
Eric Blake -
Laine Stump