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(a)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)