
This is the difference between this and the previous patchset (v1): - Use "dict.get(key, default_value)" instead of handling "dict[key]" in a try/except block when the key doesn't exist. - Remove the mention of a parameter in the snapshot method DELETE because it wasn't implemented. - Fix documentation, including mispelled words. - Rebased to the latest revision (bde438e). Crístian Viana (10): Update clone test snapshot: Create domain snapshots snapshot: Lookup a domain snapshot snapshot: List domain snapshots snapshot: Delete a domain snapshot snapshot: Lookup current snapshot on a domain snapshot: Revert a domain to a snapshot snapshot: Add model tests snapshot: Delete snapshots when deleting a VM snapshot: Clone snapshots when cloning a VM docs/API.md | 27 ++++++ src/kimchi/control/vm/snapshots.py | 58 ++++++++++++ src/kimchi/i18n.py | 10 ++ src/kimchi/mockmodel.py | 90 ++++++++++++++++++ src/kimchi/model/vms.py | 47 +++++++++- src/kimchi/model/vmsnapshots.py | 185 +++++++++++++++++++++++++++++++++++++ tests/test_model.py | 74 ++++++++++++++- tests/test_rest.py | 100 ++++++++++++++++++++ 8 files changed, 589 insertions(+), 2 deletions(-) create mode 100644 src/kimchi/control/vm/snapshots.py create mode 100644 src/kimchi/model/vmsnapshots.py -- 1.9.3

The clone model test assumes that the default VM is running so it can expect an exception when trying to clone that VM. But that's not always the case. Also, the test doesn't wait for the clone operation to finish, which sometimes may cause an error. Make sure the VM used in the test is running before cloning it (expecting an exception) and wait for the clone task to finish before moving on to the next test. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- tests/test_model.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/test_model.py b/tests/test_model.py index f4d842f..4e0c837 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -1262,6 +1262,8 @@ class ModelTests(unittest.TestCase): name = all_vm_names[0] original_vm = inst.vm_lookup(name) + if original_vm['state'] == u'shutoff': + inst.vm_start(name) # the VM 'test' should be running by now, so we can't clone it yet self.assertRaises(InvalidParameter, inst.vm_clone, name) @@ -1274,6 +1276,8 @@ class ModelTests(unittest.TestCase): clone_name = task['target_uri'].split('/')[-1] rollback.prependDefer(inst.vm_delete, clone_name) inst.task_wait(task['id']) + task = inst.task_lookup(task['id']) + self.assertEquals('finished', task['status']) # update the original VM info because its state has changed original_vm = inst.vm_lookup(name) -- 1.9.3

A new command is added to create a new snapshot: POST /vms/<vm-name>/snapshots {'name': '<snapshot-name>'} It creates a new snapshot with the current state of the VM. Currently, due to Kimchi not supporting paused states, snapshots can only be created when the VM is shut off. The parameter 'name' is optional; Kimchi will create a default value based on the current time if it's not provided. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 5 +++ src/kimchi/control/vm/snapshots.py | 44 ++++++++++++++++++ src/kimchi/i18n.py | 3 ++ src/kimchi/mockmodel.py | 45 +++++++++++++++++++ src/kimchi/model/vmsnapshots.py | 91 ++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 14 ++++++ 6 files changed, 202 insertions(+) create mode 100644 src/kimchi/control/vm/snapshots.py create mode 100644 src/kimchi/model/vmsnapshots.py diff --git a/docs/API.md b/docs/API.md index 9b866f3..fe1c3cf 100644 --- a/docs/API.md +++ b/docs/API.md @@ -188,6 +188,11 @@ Represents a snapshot of the Virtual Machine's primary monitor. * type: The type of the assigned device. * **DELETE**: Detach the host device from VM. +### Sub-collection: Virtual Machine Snapshots +**URI:** /vms/*:name*/snapshots +* **POST**: Create a new snapshot on a VM. + * name: The snapshot name (optional, defaults to a value based on the + current time). ### Collection: Templates diff --git a/src/kimchi/control/vm/snapshots.py b/src/kimchi/control/vm/snapshots.py new file mode 100644 index 0000000..5650435 --- /dev/null +++ b/src/kimchi/control/vm/snapshots.py @@ -0,0 +1,44 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +from kimchi.control.base import AsyncCollection, Resource +from kimchi.control.utils import UrlSubNode + + +@UrlSubNode('snapshots') +class VMSnapshots(AsyncCollection): + def __init__(self, model, vm): + super(VMSnapshots, self).__init__(model) + self.resource = VMSnapshot + self.vm = vm + self.resource_args = [self.vm, ] + self.model_args = [self.vm, ] + + +class VMSnapshot(Resource): + def __init__(self, model, vm, ident): + super(VMSnapshot, self).__init__(model, ident) + self.vm = vm + self.ident = ident + self.model_args = [self.vm, self.ident] + self.uri_fmt = '/vms/%s/snapshots/%s' + + @property + def data(self): + return self.info diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index e823f2b..9c37931 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -310,4 +310,7 @@ messages = { "KCHREPOS0026E": _("Unable to add repository. Details: '%(err)s'"), "KCHREPOS0027E": _("Unable to remove repository. Details: '%(err)s'"), "KCHREPOS0028E": _("Configuration items: '%(items)s' are not supported by repository manager"), + + "KCHSNAP0001E": _("Virtual machine '%(vm)s' must be stopped before creating a snapshot on it."), + "KCHSNAP0002E": _("Unable to create snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), } diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 626ef35..211ae66 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -965,6 +965,37 @@ class MockModel(object): info['model'] = params['model'] return mac + def vmsnapshots_create(self, vm_name, params): + name = params.get('name', unicode(int(time.time()))) + + vm = self._get_vm(vm_name) + if vm.info['state'] != 'shutoff': + raise InvalidOperation('KCHSNAP0001E', {'vm': vm_name}) + + params = {'vm_name': vm_name, 'name': name} + taskid = self.add_task(u'/vms/%s/snapshots/%s' % (vm_name, name), + self._vmsnapshots_create_task, params) + return self.task_lookup(taskid) + + def _vmsnapshots_create_task(self, cb, params): + vm_name = params['vm_name'] + name = params['name'] + + vm = self._get_vm(vm_name) + + parent = u'' + for sn, s in vm.snapshots.iteritems(): + if s.current: + s.current = False + parent = sn + break + + snap_info = {'parent': parent, + 'state': vm.info['state']} + vm.snapshots[name] = MockVMSnapshot(vm_name, name, snap_info) + + cb('OK', True) + def tasks_get_list(self): with self.objstore as session: return session.get_list('task') @@ -1230,6 +1261,7 @@ class MockVM(object): ifaces = [MockVMIface(net) for net in self.networks] self.storagedevices = {} self.ifaces = dict([(iface.info['mac'], iface) for iface in ifaces]) + self.snapshots = {} stats = {'cpu_utilization': 20, 'net_throughput': 35, @@ -1581,6 +1613,19 @@ class MockDevices(object): 'path': '/sys/devices/pci0000:00/0000:40:00.0/2'}} +class MockVMSnapshot(object): + def __init__(self, vm_name, name, params={}): + self.vm = vm_name + self.name = name + self.current = True + + self.info = {'created': params.get('created', + unicode(int(time.time()))), + 'name': name, + 'parent': params.get('parent', u''), + 'state': params.get('state', u'shutoff')} + + def get_mock_environment(): model = MockModel() for i in xrange(5): diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py new file mode 100644 index 0000000..1512d9e --- /dev/null +++ b/src/kimchi/model/vmsnapshots.py @@ -0,0 +1,91 @@ +# +# Project Kimchi +# +# Copyright IBM, Corp. 2014 +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + +import time + +import libvirt +import lxml.etree as ET +from lxml.builder import E + +from kimchi.exception import InvalidOperation, NotFoundError, OperationFailed +from kimchi.model.tasks import TaskModel +from kimchi.model.vms import DOM_STATE_MAP, VMModel +from kimchi.utils import add_task + + +class VMSnapshotsModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.objstore = kargs['objstore'] + self.task = TaskModel(**kargs) + + def create(self, vm_name, params={}): + """Create a snapshot with the current domain state. + + The VM must be stopped before creating a snapshot on it; otherwise, an + exception will be raised. + + Parameters: + vm_name -- the name of the VM where the snapshot will be created. + params -- a dict with the following values: + "name": The snapshot name (optional). If omitted, a default value + based on the current time will be used. + + Return: + A Task running the operation. + """ + vir_dom = VMModel.get_vm(vm_name, self.conn) + if DOM_STATE_MAP[vir_dom.info()[0]] != u'shutoff': + raise InvalidOperation('KCHSNAP0001E', {'vm': vm_name}) + + name = params.get('name', unicode(int(time.time()))) + + task_params = {'vm_name': vm_name, 'name': name} + taskid = add_task(u'/vms/%s/snapshots/%s' % (vm_name, name), + self._create_task, self.objstore, task_params) + return self.task.lookup(taskid) + + def _create_task(self, cb, params): + """Asynchronous function which actually creates the snapshot. + + Parameters: + cb -- a callback function to signal the Task's progress. + params -- a dict with the following values: + "vm_name": the name of the VM where the snapshot will be created. + "name": the snapshot name. + """ + vm_name = params['vm_name'] + name = params['name'] + + cb('building snapshot XML') + root_elem = E.domainsnapshot() + root_elem.append(E.name(name)) + xml = ET.tostring(root_elem, encoding='utf-8') + + try: + cb('fetching snapshot domain') + vir_dom = VMModel.get_vm(vm_name, self.conn) + cb('creating snapshot') + vir_dom.snapshotCreateXML(xml, 0) + except (NotFoundError, OperationFailed, libvirt.libvirtError), e: + raise OperationFailed('KCHSNAP0002E', + {'name': name, 'vm': vm_name, + 'err': e.message}) + + cb('OK', True) diff --git a/tests/test_rest.py b/tests/test_rest.py index 6770647..7e6d684 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -346,6 +346,10 @@ class RestTests(unittest.TestCase): resp = self.request('/vms/test-vm/clone', '{}', 'POST') self.assertEquals(400, resp.status) + # Create a snapshot on a running VM + resp = self.request('/vms/test-vm/snapshots', '{}', 'POST') + self.assertEquals(400, resp.status) + # Force poweroff the VM resp = self.request('/vms/test-vm/poweroff', '{}', 'POST') vm = json.loads(self.request('/vms/test-vm').read()) @@ -382,6 +386,16 @@ class RestTests(unittest.TestCase): self.assertEquals(original_vm_info, clone_vm_info) + # Create a snapshot on a stopped VM + params = {'name': 'test-snap'} + resp = self.request('/vms/test-vm/snapshots', json.dumps(params), + 'POST') + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + task = json.loads(self.request('/tasks/%s' % task['id']).read()) + self.assertEquals('finished', task['status']) + # Delete the VM resp = self.request('/vms/test-vm', '{}', 'DELETE') self.assertEquals(204, resp.status) -- 1.9.3

A new command is added to look up an existing snapshot: GET /vms/<vm-name>/snapshots/<snapshot-name> It returns the following values: * created: The time when the snapshot was created (in seconds, since the Unix Epoch); * name: The snapshot name; * parent: The name of the parent snapshot; * state: The corresponding VM state when the snapshot was created (currently, it can only be 'shutoff'); Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 10 ++++++++++ src/kimchi/i18n.py | 2 ++ src/kimchi/mockmodel.py | 8 ++++++++ src/kimchi/model/vmsnapshots.py | 43 +++++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 12 ++++++++++++ 5 files changed, 75 insertions(+) diff --git a/docs/API.md b/docs/API.md index fe1c3cf..44cc825 100644 --- a/docs/API.md +++ b/docs/API.md @@ -194,6 +194,16 @@ Represents a snapshot of the Virtual Machine's primary monitor. * name: The snapshot name (optional, defaults to a value based on the current time). +### Sub-resource: Snapshot +**URI:** /vms/*:name*/snapshots/*:snapshot* +* **GET**: Retrieve snapshot information. + * created: The time when the snapshot was created + (in seconds, since the epoch). + * name: The snapshot name. + * parent: The name of the parent snapshot, or an empty string if there is + no parent. + * state: The corresponding domain's state when the snapshot was created. + ### Collection: Templates **URI:** /templates diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 9c37931..159a2d2 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -313,4 +313,6 @@ messages = { "KCHSNAP0001E": _("Virtual machine '%(vm)s' must be stopped before creating a snapshot on it."), "KCHSNAP0002E": _("Unable to create snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), + "KCHSNAP0003E": _("Snapshot '%(name)s' does not exist on virtual machine '%(vm)s'."), + "KCHSNAP0004E": _("Unable to retrieve snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), } diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 211ae66..b6fb531 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -996,6 +996,14 @@ class MockModel(object): cb('OK', True) + def vmsnapshot_lookup(self, vm_name, name): + vm = self._get_vm(vm_name) + + try: + return vm.snapshots[name].info + except KeyError: + raise NotFoundError('KCHSNAP0003E', {'vm': vm_name, 'name': name}) + def tasks_get_list(self): with self.objstore as session: return session.get_list('task') diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index 1512d9e..1ce6e9f 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -21,6 +21,7 @@ import time import libvirt import lxml.etree as ET +from lxml import objectify from lxml.builder import E from kimchi.exception import InvalidOperation, NotFoundError, OperationFailed @@ -89,3 +90,45 @@ class VMSnapshotsModel(object): 'err': e.message}) cb('OK', True) + + +class VMSnapshotModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + + def lookup(self, vm_name, name): + vir_snap = self.get_vmsnapshot(vm_name, name) + + try: + snap_xml_str = vir_snap.getXMLDesc(0).decode('utf-8') + except libvirt.libvirtError, e: + raise OperationFailed('KCHSNAP0004E', {'name': name, + 'vm': vm_name, + 'err': e.message}) + + snap_xml = objectify.fromstring(snap_xml_str) + + try: + parent = unicode(snap_xml.parent.name) + except AttributeError: + parent = u'' + + return {'created': unicode(snap_xml.creationTime), + 'name': unicode(snap_xml.name), + 'parent': parent, + 'state': unicode(snap_xml.state)} + + def get_vmsnapshot(self, vm_name, name): + vir_dom = VMModel.get_vm(vm_name, self.conn) + + try: + return vir_dom.snapshotLookupByName(name) + except libvirt.libvirtError, e: + code = e.get_error_code() + if code == libvirt.VIR_ERR_NO_DOMAIN_SNAPSHOT: + raise NotFoundError('KCHSNAP0003E', {'name': name, + 'vm': vm_name}) + else: + raise OperationFailed('KCHSNAP0004E', {'name': name, + 'vm': vm_name, + 'err': e.message}) diff --git a/tests/test_rest.py b/tests/test_rest.py index 7e6d684..e6ce715 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -396,6 +396,18 @@ class RestTests(unittest.TestCase): task = json.loads(self.request('/tasks/%s' % task['id']).read()) self.assertEquals('finished', task['status']) + # Look up a snapshot + resp = self.request('/vms/test-vm/snapshots/foobar', '{}', 'GET') + self.assertEquals(404, resp.status) + resp = self.request('/vms/test-vm/snapshots/%s' % params['name'], '{}', + 'GET') + self.assertEquals(200, resp.status) + snap = json.loads(resp.read()) + self.assertTrue(int(time.time()) >= int(snap['created'])) + self.assertEquals(params['name'], snap['name']) + self.assertEquals(u'', snap['parent']) + self.assertEquals(u'shutoff', snap['state']) + # Delete the VM resp = self.request('/vms/test-vm', '{}', 'DELETE') self.assertEquals(204, resp.status) -- 1.9.3

A new command is added to list all existing snapshots on a virtual machine: GET /vms/<vm-name>/snapshots It returns a list of snapshot structures (as returned by 'GET /vms/<vm-name>/snapshots/<snapshot-name>') which exist on a virtual machine. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 1 + src/kimchi/i18n.py | 1 + src/kimchi/mockmodel.py | 4 ++++ src/kimchi/model/vmsnapshots.py | 11 +++++++++++ tests/test_rest.py | 18 ++++++++++++++++++ 5 files changed, 35 insertions(+) diff --git a/docs/API.md b/docs/API.md index 44cc825..590cb6c 100644 --- a/docs/API.md +++ b/docs/API.md @@ -193,6 +193,7 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **POST**: Create a new snapshot on a VM. * name: The snapshot name (optional, defaults to a value based on the current time). +* **GET**: Retrieve a list of snapshots on a VM. ### Sub-resource: Snapshot **URI:** /vms/*:name*/snapshots/*:snapshot* diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 159a2d2..0a6fee3 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -315,4 +315,5 @@ messages = { "KCHSNAP0002E": _("Unable to create snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0003E": _("Snapshot '%(name)s' does not exist on virtual machine '%(vm)s'."), "KCHSNAP0004E": _("Unable to retrieve snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), + "KCHSNAP0005E": _("Unable to list snapshots on virtual machine '%(vm)s'. Details: %(err)s"), } diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index b6fb531..11b4394 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -996,6 +996,10 @@ class MockModel(object): cb('OK', True) + def vmsnapshots_get_list(self, vm_name): + vm = self._get_vm(vm_name) + return sorted(vm.snapshots.keys(), key=unicode.lower) + def vmsnapshot_lookup(self, vm_name, name): vm = self._get_vm(vm_name) diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index 1ce6e9f..b3fb6d5 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -91,6 +91,17 @@ class VMSnapshotsModel(object): cb('OK', True) + def get_list(self, vm_name): + vir_dom = VMModel.get_vm(vm_name, self.conn) + + try: + vir_snaps = vir_dom.listAllSnapshots(0) + return sorted([s.getName().decode('utf-8') for s in vir_snaps], + key=unicode.lower) + except libvirt.libvirtError, e: + raise OperationFailed('KCHSNAP0005E', + {'vm': vm_name, 'err': e.message}) + class VMSnapshotModel(object): def __init__(self, **kargs): diff --git a/tests/test_rest.py b/tests/test_rest.py index e6ce715..6af0d72 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -408,6 +408,24 @@ class RestTests(unittest.TestCase): self.assertEquals(u'', snap['parent']) self.assertEquals(u'shutoff', snap['state']) + resp = self.request('/vms/test-vm/snapshots', '{}', 'GET') + self.assertEquals(200, resp.status) + snaps = json.loads(resp.read()) + self.assertEquals(1, len(snaps)) + + resp = self.request('/vms/test-vm/snapshots', '{}', 'POST') + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + resp = self.request('/tasks/%s' % task['id'], '{}', 'GET') + task = json.loads(resp.read()) + self.assertEquals('finished', task['status']) + + resp = self.request('/vms/test-vm/snapshots', '{}', 'GET') + self.assertEquals(200, resp.status) + snaps = json.loads(resp.read()) + self.assertEquals(2, len(snaps)) + # Delete the VM resp = self.request('/vms/test-vm', '{}', 'DELETE') self.assertEquals(204, resp.status) -- 1.9.3

A new command is added to delete an existing snapshot: DELETE /vms/<vm-name>/snapshots/<snapshot-name> It deletes the specified snapshot from the virtual machine. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 2 ++ src/kimchi/i18n.py | 1 + src/kimchi/mockmodel.py | 8 ++++++++ src/kimchi/model/vmsnapshots.py | 9 +++++++++ tests/test_rest.py | 10 ++++++++++ 5 files changed, 30 insertions(+) diff --git a/docs/API.md b/docs/API.md index 590cb6c..9e7365a 100644 --- a/docs/API.md +++ b/docs/API.md @@ -204,6 +204,8 @@ Represents a snapshot of the Virtual Machine's primary monitor. * parent: The name of the parent snapshot, or an empty string if there is no parent. * state: The corresponding domain's state when the snapshot was created. +* **DELETE**: Delete snapshot. If the snapshot has any children, they will be + merged automatically with the snapshot's parent. ### Collection: Templates diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 0a6fee3..6a1e193 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -316,4 +316,5 @@ messages = { "KCHSNAP0003E": _("Snapshot '%(name)s' does not exist on virtual machine '%(vm)s'."), "KCHSNAP0004E": _("Unable to retrieve snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0005E": _("Unable to list snapshots on virtual machine '%(vm)s'. Details: %(err)s"), + "KCHSNAP0006E": _("Unable to delete snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), } diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 11b4394..480cb8e 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -1008,6 +1008,14 @@ class MockModel(object): except KeyError: raise NotFoundError('KCHSNAP0003E', {'vm': vm_name, 'name': name}) + def vmsnapshot_delete(self, vm_name, name): + vm = self._get_vm(vm_name) + + try: + del vm.snapshots[name] + except KeyError: + raise NotFoundError('KCHSNAP0003E', {'vm': vm_name, 'name': name}) + def tasks_get_list(self): with self.objstore as session: return session.get_list('task') diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index b3fb6d5..b66cd99 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -129,6 +129,15 @@ class VMSnapshotModel(object): 'parent': parent, 'state': unicode(snap_xml.state)} + def delete(self, vm_name, name): + try: + vir_snap = self.get_vmsnapshot(vm_name, name) + vir_snap.delete(0) + except libvirt.libvirtError, e: + raise OperationFailed('KCHSNAP0006E', {'name': name, + 'vm': vm_name, + 'err': e.message}) + def get_vmsnapshot(self, vm_name, name): vir_dom = VMModel.get_vm(vm_name, self.conn) diff --git a/tests/test_rest.py b/tests/test_rest.py index 6af0d72..ef0fbf3 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -426,6 +426,16 @@ class RestTests(unittest.TestCase): snaps = json.loads(resp.read()) self.assertEquals(2, len(snaps)) + # Delete a snapshot + resp = self.request('/vms/test-vm/snapshots/foobar', '{}', 'DELETE') + self.assertEquals(404, resp.status) + resp = self.request('/vms/test-vm/snapshots/%s' % params['name'], + '{}', 'DELETE') + self.assertEquals(204, resp.status) + resp = self.request('/vms/test-vm/snapshots/%s' % params['name'], + '{}', 'GET') + self.assertEquals(404, resp.status) + # Delete the VM resp = self.request('/vms/test-vm', '{}', 'DELETE') self.assertEquals(204, resp.status) -- 1.9.3

A new command is added to look up the current snapshot on a virtual machine: GET /vms/<vm-name>/snapshots/current It returns a snapshot structure (as returned by 'GET /vms/<vm-name>/snapshots/<snapshot-name>') of the current snapshot on the specified virtual machine. If the VM doesn't have a current snapshot, an exception will be returned. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 4 ++++ src/kimchi/control/vm/snapshots.py | 13 +++++++++++++ src/kimchi/i18n.py | 2 ++ src/kimchi/mockmodel.py | 21 +++++++++++++++------ src/kimchi/model/vmsnapshots.py | 21 +++++++++++++++++++++ tests/test_rest.py | 17 +++++++++++++++++ 6 files changed, 72 insertions(+), 6 deletions(-) diff --git a/docs/API.md b/docs/API.md index 9e7365a..23c787b 100644 --- a/docs/API.md +++ b/docs/API.md @@ -207,6 +207,10 @@ Represents a snapshot of the Virtual Machine's primary monitor. * **DELETE**: Delete snapshot. If the snapshot has any children, they will be merged automatically with the snapshot's parent. +### Sub-resource: Current snapshot +**URI:** /vms/*:name*/snapshots/current +* **GET**: Retrieve current snapshot information for the virtual machine. + ### Collection: Templates **URI:** /templates diff --git a/src/kimchi/control/vm/snapshots.py b/src/kimchi/control/vm/snapshots.py index 5650435..d491015 100644 --- a/src/kimchi/control/vm/snapshots.py +++ b/src/kimchi/control/vm/snapshots.py @@ -29,6 +29,7 @@ class VMSnapshots(AsyncCollection): self.vm = vm self.resource_args = [self.vm, ] self.model_args = [self.vm, ] + self.current = CurrentVMSnapshot(model, vm) class VMSnapshot(Resource): @@ -42,3 +43,15 @@ class VMSnapshot(Resource): @property def data(self): return self.info + + +class CurrentVMSnapshot(Resource): + def __init__(self, model, vm): + super(CurrentVMSnapshot, self).__init__(model) + self.vm = vm + self.model_args = [self.vm] + self.uri_fmt = '/vms/%s/snapshots/current' + + @property + def data(self): + return self.info diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index 6a1e193..b1b8060 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -317,4 +317,6 @@ messages = { "KCHSNAP0004E": _("Unable to retrieve snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0005E": _("Unable to list snapshots on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0006E": _("Unable to delete snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), + "KCHSNAP0007E": _("Virtual machine '%(vm)s' does not have a current snapshot."), + "KCHSNAP0008E": _("Unable to retrieve current snapshot on virtual machine '%(vm)s'. Details: %(err)s"), } diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 480cb8e..75855f6 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -983,12 +983,12 @@ class MockModel(object): vm = self._get_vm(vm_name) - parent = u'' - for sn, s in vm.snapshots.iteritems(): - if s.current: - s.current = False - parent = sn - break + try: + parent = self.currentvmsnapshot_lookup(vm_name)['name'] + except NotFoundError: + parent = u'' + else: + vm.snapshots[parent].current = False snap_info = {'parent': parent, 'state': vm.info['state']} @@ -1000,6 +1000,15 @@ class MockModel(object): vm = self._get_vm(vm_name) return sorted(vm.snapshots.keys(), key=unicode.lower) + def currentvmsnapshot_lookup(self, vm_name): + vm = self._get_vm(vm_name) + + for sn, s in vm.snapshots.iteritems(): + if s.current: + return s.info + + raise NotFoundError('KCHSNAP0007E', {'vm': vm_name}) + def vmsnapshot_lookup(self, vm_name, name): vm = self._get_vm(vm_name) diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index b66cd99..b6702b9 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -152,3 +152,24 @@ class VMSnapshotModel(object): raise OperationFailed('KCHSNAP0004E', {'name': name, 'vm': vm_name, 'err': e.message}) + + +class CurrentVMSnapshotModel(object): + def __init__(self, **kargs): + self.conn = kargs['conn'] + self.vmsnapshot = VMSnapshotModel(**kargs) + + def lookup(self, vm_name): + vir_dom = VMModel.get_vm(vm_name, self.conn) + + try: + vir_snap = vir_dom.snapshotCurrent(0) + snap_name = vir_snap.getName().decode('utf-8') + except libvirt.libvirtError, e: + if e.get_error_code() == libvirt.VIR_ERR_NO_DOMAIN_SNAPSHOT: + raise NotFoundError('KCHSNAP007E', {'vm': vm_name}) + + raise OperationFailed('KCHSNAP0008E', + {'vm': vm_name, 'err': e.message}) + + return self.vmsnapshot.lookup(vm_name, snap_name) diff --git a/tests/test_rest.py b/tests/test_rest.py index ef0fbf3..1129aec 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -386,6 +386,10 @@ class RestTests(unittest.TestCase): self.assertEquals(original_vm_info, clone_vm_info) + # Look up current snapshot when there is no snapshot + resp = self.request('/vms/test-vm/snapshots/current', '{}', 'GET') + self.assertEquals(404, resp.status) + # Create a snapshot on a stopped VM params = {'name': 'test-snap'} resp = self.request('/vms/test-vm/snapshots', json.dumps(params), @@ -413,9 +417,16 @@ class RestTests(unittest.TestCase): snaps = json.loads(resp.read()) self.assertEquals(1, len(snaps)) + # Look up current snapshot (the one created above) + resp = self.request('/vms/test-vm/snapshots/current', '{}', 'GET') + self.assertEquals(200, resp.status) + snap = json.loads(resp.read()) + self.assertEquals(params['name'], snap['name']) + resp = self.request('/vms/test-vm/snapshots', '{}', 'POST') self.assertEquals(202, resp.status) task = json.loads(resp.read()) + snap_name = task['target_uri'].split('/')[-1] wait_task(self._task_lookup, task['id']) resp = self.request('/tasks/%s' % task['id'], '{}', 'GET') task = json.loads(resp.read()) @@ -426,6 +437,12 @@ class RestTests(unittest.TestCase): snaps = json.loads(resp.read()) self.assertEquals(2, len(snaps)) + # Look up current snapshot (the one created above) + resp = self.request('/vms/test-vm/snapshots/current', '{}', 'GET') + self.assertEquals(200, resp.status) + snap = json.loads(resp.read()) + self.assertEquals(snap_name, snap['name']) + # Delete a snapshot resp = self.request('/vms/test-vm/snapshots/foobar', '{}', 'DELETE') self.assertEquals(404, resp.status) -- 1.9.3

A new command is added to revert a virtual machine to a specified snapshot: POST /vms/<vm-name>/snapshots/<snapshot-name>/revert It changes the specified VM state to the exact same one as it was when the snapshot was created. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 5 +++++ src/kimchi/control/vm/snapshots.py | 1 + src/kimchi/i18n.py | 1 + src/kimchi/mockmodel.py | 14 ++++++++++++++ src/kimchi/model/vmsnapshots.py | 10 ++++++++++ tests/test_rest.py | 14 ++++++++++++++ 6 files changed, 45 insertions(+) diff --git a/docs/API.md b/docs/API.md index 23c787b..d4ed015 100644 --- a/docs/API.md +++ b/docs/API.md @@ -206,6 +206,11 @@ Represents a snapshot of the Virtual Machine's primary monitor. * state: The corresponding domain's state when the snapshot was created. * **DELETE**: Delete snapshot. If the snapshot has any children, they will be merged automatically with the snapshot's parent. +* **POST**: See "Snapshot actions (POST)" + +**Snapshot Actions (POST):** + +* revert: Revert the domain to the given snapshot. ### Sub-resource: Current snapshot **URI:** /vms/*:name*/snapshots/current diff --git a/src/kimchi/control/vm/snapshots.py b/src/kimchi/control/vm/snapshots.py index d491015..bbebc9a 100644 --- a/src/kimchi/control/vm/snapshots.py +++ b/src/kimchi/control/vm/snapshots.py @@ -39,6 +39,7 @@ class VMSnapshot(Resource): self.ident = ident self.model_args = [self.vm, self.ident] self.uri_fmt = '/vms/%s/snapshots/%s' + self.revert = self.generate_action_handler('revert') @property def data(self): diff --git a/src/kimchi/i18n.py b/src/kimchi/i18n.py index b1b8060..b95c707 100644 --- a/src/kimchi/i18n.py +++ b/src/kimchi/i18n.py @@ -319,4 +319,5 @@ messages = { "KCHSNAP0006E": _("Unable to delete snapshot '%(name)s' on virtual machine '%(vm)s'. Details: %(err)s"), "KCHSNAP0007E": _("Virtual machine '%(vm)s' does not have a current snapshot."), "KCHSNAP0008E": _("Unable to retrieve current snapshot on virtual machine '%(vm)s'. Details: %(err)s"), + "KCHSNAP0009E": _("Unable to revert virtual machine '%(vm)s' to snapshot '%(name)s'. Details: %(err)s"), } diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 75855f6..ed57cb7 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -1025,6 +1025,20 @@ class MockModel(object): except KeyError: raise NotFoundError('KCHSNAP0003E', {'vm': vm_name, 'name': name}) + def vmsnapshot_revert(self, vm_name, name): + vm = self._get_vm(vm_name) + + try: + snap = vm.snapshots[name] + except KeyError: + raise NotFoundError('KCHSNAP0003E', {'vm': vm_name, 'name': name}) + + current_snapshot_name = self.currentvmsnapshot_lookup(vm_name)['name'] + vm.snapshots[current_snapshot_name].current = False + snap.current = True + + vm.info['state'] = snap.info['state'] + def tasks_get_list(self): with self.objstore as session: return session.get_list('task') diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index b6702b9..e8e2294 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -138,6 +138,16 @@ class VMSnapshotModel(object): 'vm': vm_name, 'err': e.message}) + def revert(self, vm_name, name): + try: + vir_dom = VMModel.get_vm(vm_name, self.conn) + vir_snap = self.get_vmsnapshot(vm_name, name) + vir_dom.revertToSnapshot(vir_snap, 0) + except libvirt.libvirtError, e: + raise OperationFailed('KCHSNAP0009E', {'name': name, + 'vm': vm_name, + 'err': e.message}) + def get_vmsnapshot(self, vm_name, name): vir_dom = VMModel.get_vm(vm_name, self.conn) diff --git a/tests/test_rest.py b/tests/test_rest.py index 1129aec..b1bfbcc 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -443,6 +443,20 @@ class RestTests(unittest.TestCase): snap = json.loads(resp.read()) self.assertEquals(snap_name, snap['name']) + # Revert to snapshot + resp = self.request('/vms/test-vm/snapshots/%s/revert' % + params['name'], '{}', 'POST') + self.assertEquals(200, resp.status) + snap = json.loads(resp.read()) + resp = self.request('/vms/test-vm', '{}', 'GET') + self.assertEquals(200, resp.status) + vm = json.loads(resp.read()) + self.assertEquals(vm['state'], snap['state']) + resp = self.request('/vms/test-vm/snapshots/current', '{}', 'GET') + self.assertEquals(200, resp.status) + current_snap = json.loads(resp.read()) + self.assertEquals(snap, current_snap) + # Delete a snapshot resp = self.request('/vms/test-vm/snapshots/foobar', '{}', 'DELETE') self.assertEquals(404, resp.status) -- 1.9.3

Add the model tests related to snapshot. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- tests/test_model.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/tests/test_model.py b/tests/test_model.py index 4e0c837..cbcaed7 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -109,11 +109,69 @@ class ModelTests(unittest.TestCase): self.assertTrue('kimchi-vm' in vms) inst.vm_start('kimchi-vm') - rollback.prependDefer(inst.vm_poweroff, 'kimchi-vm') info = inst.vm_lookup('kimchi-vm') self.assertEquals('running', info['state']) + self.assertRaises(InvalidOperation, inst.vmsnapshots_create, + u'kimchi-vm') + + inst.vm_poweroff(u'kimchi-vm') + vm = inst.vm_lookup(u'kimchi-vm') + + self.assertRaises(NotFoundError, inst.currentvmsnapshot_lookup, + u'kimchi-vm') + + params = {'name': u'mysnap'} + task = inst.vmsnapshots_create(u'kimchi-vm', params) + rollback.prependDefer(inst.vmsnapshot_delete, + u'kimchi-vm', params['name']) + inst.task_wait(task['id']) + task = inst.task_lookup(task['id']) + self.assertEquals('finished', task['status']) + + self.assertRaises(NotFoundError, inst.vmsnapshot_lookup, + u'kimchi-vm', u'foobar') + + snap = inst.vmsnapshot_lookup(u'kimchi-vm', params['name']) + self.assertTrue(int(time.time()) >= int(snap['created'])) + self.assertEquals(vm['state'], snap['state']) + self.assertEquals(params['name'], snap['name']) + self.assertEquals(u'', snap['parent']) + + snaps = inst.vmsnapshots_get_list(u'kimchi-vm') + self.assertEquals([params['name']], snaps) + + current_snap = inst.currentvmsnapshot_lookup(u'kimchi-vm') + self.assertEquals(snap, current_snap) + + task = inst.vmsnapshots_create(u'kimchi-vm') + snap_name = task['target_uri'].split('/')[-1] + rollback.prependDefer(inst.vmsnapshot_delete, + u'kimchi-vm', snap_name) + inst.task_wait(task['id']) + task = inst.task_lookup(task['id']) + self.assertEquals('finished', task['status']) + + snaps = inst.vmsnapshots_get_list(u'kimchi-vm') + self.assertEquals(sorted([params['name'], snap_name], + key=unicode.lower), snaps) + + snap = inst.vmsnapshot_lookup(u'kimchi-vm', snap_name) + current_snap = inst.currentvmsnapshot_lookup(u'kimchi-vm') + self.assertEquals(snap, current_snap) + + snap = inst.vmsnapshot_lookup(u'kimchi-vm', params['name']) + inst.vmsnapshot_revert(u'kimchi-vm', params['name']) + vm = inst.vm_lookup(u'kimchi-vm') + self.assertEquals(vm['state'], snap['state']) + + current_snap = inst.currentvmsnapshot_lookup(u'kimchi-vm') + self.assertEquals(params['name'], current_snap['name']) + + self.assertRaises(NotFoundError, inst.vmsnapshot_delete, + u'kimchi-vm', u'foobar') + vms = inst.vms_get_list() self.assertFalse('kimchi-vm' in vms) -- 1.9.3

If a virtual machine is deleted, its snapshots should also be deleted. Update the function "VMModel.delete" in order to delete snapshots before deleting the virtual machine. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- src/kimchi/model/vms.py | 18 ++++++++++++++++++ tests/test_model.py | 3 +-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index d194049..211e438 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -268,6 +268,10 @@ class VMModel(object): self.storagepool = model.storagepools.StoragePoolModel(**kargs) self.storagevolume = model.storagevolumes.StorageVolumeModel(**kargs) self.storagevolumes = model.storagevolumes.StorageVolumesModel(**kargs) + cls = import_class('kimchi.model.vmsnapshots.VMSnapshotModel') + self.vmsnapshot = cls(**kargs) + cls = import_class('kimchi.model.vmsnapshots.VMSnapshotsModel') + self.vmsnapshots = cls(**kargs) def update(self, name, params): dom = self.get_vm(name, self.conn) @@ -752,6 +756,20 @@ class VMModel(object): if info['state'] == 'running': self.poweroff(name) + # delete existing snapshots before deleting VM + + # libvirt's Test driver does not support the function + # "virDomainListAllSnapshots", so "VMSnapshots.get_list" will raise + # "OperationFailed" in that case. + try: + snapshot_names = self.vmsnapshots.get_list(name) + except OperationFailed, e: + kimchi_log.error('cannot list snapshots: %s; ' + 'skipping snapshot deleting...' % e.message) + else: + for s in snapshot_names: + self.vmsnapshot.delete(name, s) + try: dom.undefine() except libvirt.libvirtError as e: diff --git a/tests/test_model.py b/tests/test_model.py index cbcaed7..86ab24a 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -122,10 +122,9 @@ class ModelTests(unittest.TestCase): self.assertRaises(NotFoundError, inst.currentvmsnapshot_lookup, u'kimchi-vm') + # this snapshot should be deleted when its VM is deleted params = {'name': u'mysnap'} task = inst.vmsnapshots_create(u'kimchi-vm', params) - rollback.prependDefer(inst.vmsnapshot_delete, - u'kimchi-vm', params['name']) inst.task_wait(task['id']) task = inst.task_lookup(task['id']) self.assertEquals('finished', task['status']) -- 1.9.3

If a virtual machine is cloned, its snapshots should also be cloned so we can have a real clone of the original VM. Update the function "VMModel.clone" in order to clone the snapshots when cloning a VM. Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- src/kimchi/mockmodel.py | 2 ++ src/kimchi/model/vms.py | 29 ++++++++++++++++++++++++++++- tests/test_model.py | 11 +++++++++++ tests/test_rest.py | 19 +++++++++++++++++-- 4 files changed, 58 insertions(+), 3 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index ed57cb7..3a858ca 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -218,6 +218,8 @@ class MockModel(object): new_path = u'%s-%d%s' % (basename, i, ext) new_vm.storagedevices[storage_name].path = new_path + new_vm.snapshots = copy.deepcopy(vm.snapshots) + self._mock_vms[new_name] = new_vm cb('OK', True) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index 211e438..5b92e8e 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -371,10 +371,15 @@ class VMModel(object): # create new guest cb('defining new VM') try: - vir_conn.defineXML(xml) + vir_new_dom = vir_conn.defineXML(xml) except libvirt.libvirtError, e: raise OperationFailed('KCHVM0035E', {'name': name, 'err': e.message}) + rollback.prependDefer(vir_new_dom.undefine) + + # copy snapshots + cb('copying VM snapshots') + self._clone_copy_snapshots(name, new_name) rollback.commitAll() @@ -538,6 +543,28 @@ class VMModel(object): # remove the new object store entry should an error occur later rollback.prependDefer(_rollback_objstore) + def _clone_copy_snapshots(self, vm_name, new_vm_name): + dom = self.get_vm(new_vm_name, self.conn) + flags = libvirt.VIR_DOMAIN_XML_SECURE + + # libvirt's Test driver does not support the function + # "virDomainListAllSnapshots", so "VMSnapshots.get_list" will raise + # "OperationFailed" in that case. + try: + snapshot_names = self.vmsnapshots.get_list(vm_name) + except OperationFailed, e: + kimchi_log.error('cannot list snapshots: %s; ' + 'skipping snapshot cloning...' % e.message) + else: + for s in snapshot_names: + vir_snap = self.vmsnapshot.get_vmsnapshot(vm_name, s) + try: + snap_xml = vir_snap.getXMLDesc(flags).decode('utf-8') + dom.snapshotCreateXML(snap_xml) + except libvirt.libvirtError, e: + raise OperationFailed('KCHVM0035E', {'name': vm_name, + 'err': e.message}) + def _build_access_elem(self, users, groups): access = E.access() for user in users: diff --git a/tests/test_model.py b/tests/test_model.py index 86ab24a..d0c0aed 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -156,6 +156,17 @@ class ModelTests(unittest.TestCase): self.assertEquals(sorted([params['name'], snap_name], key=unicode.lower), snaps) + # Clone the VM and check whether the snapshots have been cloned + # as well + task = inst.vm_clone(u'kimchi-vm') + clone_name = task['target_uri'].split('/')[-1] + rollback.prependDefer(inst.vm_delete, clone_name) + inst.task_wait(task['id']) + task = inst.task_lookup(task['id']) + self.assertEquals('finished', task['status']) + clone_snaps = inst.vmsnapshots_get_list(clone_name) + self.assertEquals(snaps, clone_snaps) + snap = inst.vmsnapshot_lookup(u'kimchi-vm', snap_name) current_snap = inst.currentvmsnapshot_lookup(u'kimchi-vm') self.assertEquals(snap, current_snap) diff --git a/tests/test_rest.py b/tests/test_rest.py index b1bfbcc..a64c88d 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -434,8 +434,8 @@ class RestTests(unittest.TestCase): resp = self.request('/vms/test-vm/snapshots', '{}', 'GET') self.assertEquals(200, resp.status) - snaps = json.loads(resp.read()) - self.assertEquals(2, len(snaps)) + orig_snaps = json.loads(resp.read()) + self.assertEquals(2, len(orig_snaps)) # Look up current snapshot (the one created above) resp = self.request('/vms/test-vm/snapshots/current', '{}', 'GET') @@ -457,6 +457,21 @@ class RestTests(unittest.TestCase): current_snap = json.loads(resp.read()) self.assertEquals(snap, current_snap) + # Clone a VM with snapshot + resp = self.request('/vms/test-vm/clone', '{}', 'POST') + self.assertEquals(202, resp.status) + task = json.loads(resp.read()) + wait_task(self._task_lookup, task['id']) + resp = self.request('/tasks/%s' % task['id'], '{}', 'GET') + self.assertEquals(200, resp.status) + task = json.loads(resp.read()) + self.assertEquals('finished', task['status']) + vm_name = task['target_uri'].split('/')[-1] + resp = self.request('/vms/%s/snapshots' % vm_name, '{}', 'GET') + self.assertEquals(200, resp.status) + clone_snaps = json.loads(resp.read()) + self.assertEquals(orig_snaps, clone_snaps) + # Delete a snapshot resp = self.request('/vms/test-vm/snapshots/foobar', '{}', 'DELETE') self.assertEquals(404, resp.status) -- 1.9.3
participants (2)
-
Aline Manera
-
Crístian Viana