[Kimchi-devel] [PATCH 02/10] snapshot: Create domain snapshots
Aline Manera
alinefm at linux.vnet.ibm.com
Wed Nov 12 15:47:02 UTC 2014
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 at 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
> +
> +
> + at 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)
More information about the Kimchi-devel
mailing list