[PATCH 00/10] Add snapshot support

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 | 30 ++++++ src/kimchi/control/vm/snapshots.py | 58 +++++++++++ src/kimchi/i18n.py | 10 ++ src/kimchi/mockmodel.py | 94 ++++++++++++++++++ src/kimchi/model/vms.py | 47 ++++++++- src/kimchi/model/vmsnapshots.py | 192 +++++++++++++++++++++++++++++++++++++ tests/test_model.py | 74 +++++++++++++- tests/test_rest.py | 100 +++++++++++++++++++ 8 files changed, 603 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 | 49 ++++++++++++++++++++ src/kimchi/model/vmsnapshots.py | 95 ++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 14 ++++++ 6 files changed, 210 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..e06b01f 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -965,6 +965,42 @@ class MockModel(object): info['model'] = params['model'] return mac + def vmsnapshots_create(self, vm_name, params): + try: + name = params['name'] + except KeyError: + 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.info['current']: + s.info['current'] = False + parent = sn + break + + snap_info = {'name': name, + '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 +1266,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 +1618,18 @@ 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.info = {'created': params.get('created', + unicode(int(time.time()))), + 'current': params.get('current', True), + '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..8701c03 --- /dev/null +++ b/src/kimchi/model/vmsnapshots.py @@ -0,0 +1,95 @@ +# +# 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 ommited, 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}) + + try: + name = params['name'] + except KeyError: + 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

On 11/12/2014 11:08 AM, Crístian Viana wrote:
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 | 49 ++++++++++++++++++++ src/kimchi/model/vmsnapshots.py | 95 ++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 14 ++++++ 6 files changed, 210 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..e06b01f 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -965,6 +965,42 @@ class MockModel(object): info['model'] = params['model'] return mac
+ def vmsnapshots_create(self, vm_name, params): + try: + name = params['name'] + except KeyError: + name = unicode(int(time.time())) +
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.info['current']: + s.info['current'] = False + parent = sn + break + + snap_info = {'name': name, + '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 +1266,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 +1618,18 @@ 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.info = {'created': params.get('created', + unicode(int(time.time()))), + 'current': params.get('current', True), + '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..8701c03 --- /dev/null +++ b/src/kimchi/model/vmsnapshots.py @@ -0,0 +1,95 @@ +# +# 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 ommited, 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}) +
+ try: + name = params['name'] + except KeyError: + name = unicode(int(time.time())) +
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)

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; * state: The corresponding VM state when the snapshot was created (currently, it can only be 'shutoff'); * parent: The name of the parent snapshot; Signed-off-by: Crístian Viana <vianac@linux.vnet.ibm.com> --- docs/API.md | 10 ++++++++++ src/kimchi/i18n.py | 2 ++ src/kimchi/mockmodel.py | 18 ++++++++++++----- src/kimchi/model/vmsnapshots.py | 43 +++++++++++++++++++++++++++++++++++++++++ tests/test_rest.py | 12 ++++++++++++ 5 files changed, 80 insertions(+), 5 deletions(-) diff --git a/docs/API.md b/docs/API.md index fe1c3cf..616958a 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. + * name: The snapshot name. + * state: The corresponding domain's state when the snapshot was created. + * created: The time when the snapshot was created + (in seconds, since the epoch). + * parent: The name of the parent snapshot, or an empty string if there is + no parent. + ### 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 e06b01f..cb52750 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -989,18 +989,25 @@ class MockModel(object): parent = u'' for sn, s in vm.snapshots.iteritems(): - if s.info['current']: - s.info['current'] = False + if s.current: + s.current = False parent = sn break - snap_info = {'name': name, - 'parent': parent, + snap_info = {'parent': parent, 'state': vm.info['state']} vm.snapshots[name] = MockVMSnapshot(vm_name, name, snap_info) 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') @@ -1622,10 +1629,11 @@ 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()))), - 'current': params.get('current', True), + 'name': name, 'parent': params.get('parent', u''), 'state': params.get('state', u'shutoff')} diff --git a/src/kimchi/model/vmsnapshots.py b/src/kimchi/model/vmsnapshots.py index 8701c03..27fd052 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 @@ -93,3 +94,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 {'name': unicode(snap_xml.name), + 'state': unicode(snap_xml.state), + 'created': unicode(snap_xml.creationTime), + 'parent': parent} + + 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 616958a..38c8a59 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 cb52750..d4d53fc 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -1000,6 +1000,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 27fd052..adc785f 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -95,6 +95,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 | 5 +++++ src/kimchi/i18n.py | 1 + src/kimchi/mockmodel.py | 8 ++++++++ src/kimchi/model/vmsnapshots.py | 9 +++++++++ tests/test_rest.py | 10 ++++++++++ 5 files changed, 33 insertions(+) diff --git a/docs/API.md b/docs/API.md index 38c8a59..a6ca0c2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -204,6 +204,11 @@ Represents a snapshot of the Virtual Machine's primary monitor. (in seconds, since the epoch). * parent: The name of the parent snapshot, or an empty string if there is no parent. +* **DELETE**: Delete snapshot. + * children: A boolean flag indicating whether the snapshot's children + should also be deleted: if true, the snapshot and its children + will be deleted; if false, only the snapshot will be deleted + (optional, defaults to false). ### 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 d4d53fc..50c6e0c 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -1012,6 +1012,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 adc785f..e02108d 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -133,6 +133,15 @@ class VMSnapshotModel(object): 'created': unicode(snap_xml.creationTime), 'parent': parent} + 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

On 11/12/2014 11:08 AM, Crístian Viana wrote:
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 | 5 +++++ src/kimchi/i18n.py | 1 + src/kimchi/mockmodel.py | 8 ++++++++ src/kimchi/model/vmsnapshots.py | 9 +++++++++ tests/test_rest.py | 10 ++++++++++ 5 files changed, 33 insertions(+)
diff --git a/docs/API.md b/docs/API.md index 38c8a59..a6ca0c2 100644 --- a/docs/API.md +++ b/docs/API.md @@ -204,6 +204,11 @@ Represents a snapshot of the Virtual Machine's primary monitor. (in seconds, since the epoch). * parent: The name of the parent snapshot, or an empty string if there is no parent.
+* **DELETE**: Delete snapshot. + * children: A boolean flag indicating whether the snapshot's children + should also be deleted: if true, the snapshot and its children + will be deleted; if false, only the snapshot will be deleted + (optional, defaults to false).
As it is not implemented yet, I'd say to you remove this description to avoid confusion.
### 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 d4d53fc..50c6e0c 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -1012,6 +1012,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 adc785f..e02108d 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -133,6 +133,15 @@ class VMSnapshotModel(object): 'created': unicode(snap_xml.creationTime), 'parent': parent}
+ 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)

On 12-11-2014 13:50, Aline Manera wrote:
As it is not implemented yet, I'd say to you remove this description to avoid confusion.
Sure. I wrote that documentation when I thought I was going to implement it, but then I forgot to remove it.

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 | 24 ++++++++++++++++++++++++ tests/test_rest.py | 17 +++++++++++++++++ 6 files changed, 75 insertions(+), 6 deletions(-) diff --git a/docs/API.md b/docs/API.md index a6ca0c2..f4100c0 100644 --- a/docs/API.md +++ b/docs/API.md @@ -210,6 +210,10 @@ Represents a snapshot of the Virtual Machine's primary monitor. will be deleted; if false, only the snapshot will be deleted (optional, defaults to false). +### 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 50c6e0c..33aaa33 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -987,12 +987,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']} @@ -1004,6 +1004,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 e02108d..8d6c296 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -156,3 +156,27 @@ 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 there is no current snapshot, "snapshotCurrent" raises an + # exception with the error code referenced below. In that case, + # return an empty dict. + 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 f4100c0..a918fe5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -209,6 +209,11 @@ Represents a snapshot of the Virtual Machine's primary monitor. should also be deleted: if true, the snapshot and its children will be deleted; if false, only the snapshot will be deleted (optional, defaults to false). +* **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 33aaa33..0af6056 100644 --- a/src/kimchi/mockmodel.py +++ b/src/kimchi/mockmodel.py @@ -1029,6 +1029,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 8d6c296..bbcbf59 100644 --- a/src/kimchi/model/vmsnapshots.py +++ b/src/kimchi/model/vmsnapshots.py @@ -142,6 +142,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 | 16 ++++++++++++++++ tests/test_model.py | 3 +-- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/kimchi/model/vms.py b/src/kimchi/model/vms.py index d194049..811739e 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -268,6 +268,8 @@ class VMModel(object): self.storagepool = model.storagepools.StoragePoolModel(**kargs) self.storagevolume = model.storagevolumes.StorageVolumeModel(**kargs) self.storagevolumes = model.storagevolumes.StorageVolumesModel(**kargs) + self.vmsnapshot = model.vmsnapshots.VMSnapshotModel(**kargs) + self.vmsnapshots = model.vmsnapshots.VMSnapshotsModel(**kargs) def update(self, name, params): dom = self.get_vm(name, self.conn) @@ -752,6 +754,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 | 35 ++++++++++++++++++++++++++++++++--- tests/test_model.py | 11 +++++++++++ tests/test_rest.py | 19 +++++++++++++++++-- 4 files changed, 62 insertions(+), 5 deletions(-) diff --git a/src/kimchi/mockmodel.py b/src/kimchi/mockmodel.py index 0af6056..c956255 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 811739e..5b92e8e 100644 --- a/src/kimchi/model/vms.py +++ b/src/kimchi/model/vms.py @@ -268,8 +268,10 @@ class VMModel(object): self.storagepool = model.storagepools.StoragePoolModel(**kargs) self.storagevolume = model.storagevolumes.StorageVolumeModel(**kargs) self.storagevolumes = model.storagevolumes.StorageVolumesModel(**kargs) - self.vmsnapshot = model.vmsnapshots.VMSnapshotModel(**kargs) - self.vmsnapshots = model.vmsnapshots.VMSnapshotsModel(**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) @@ -369,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() @@ -536,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