这篇文章记录nova创建快照时候的过程,根据文档,创建快照其实是向/servers/{server_id}/action发送了一个POST的请求,内容则是类似:

1
2
3
4
5
    "createImage" : {
"name" : "image-name",
"metadata": {}
}
}

根据openstack的套路,首先找到处理这个请求的代码,在第一篇文章中我们分析了在Stein版中虚拟机的创建过程,但由于某些不可描述的原因,下面的代码是N版的,一定要注意版本问题。N版中是没有nova/api/openstack/compute/routes.py这个文件的,所有处理请求的代码都在nova/api/openstack/compute/目录中,所以我们直接看servers.py即可,相关代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@wsgi.response(202)
@extensions.expected_errors((400, 403, 404, 409))
@wsgi.action('createImage')
@common.check_snapshots_enabled
@validation.schema(schema_servers.create_image, '2.0', '2.0')
@validation.schema(schema_servers.create_image, '2.1')
def _action_create_image(self, req, id, body):
"""Snapshot a server instance."""
....
try:
if compute_utils.is_volume_backed_instance(context, instance,
bdms):
context.can(server_policies.SERVERS %
'create_image:allow_volume_backed')
image = self.compute_api.snapshot_volume_backed(
context,
instance,
image_name,
extra_properties=
metadata)
else:
image = self.compute_api.snapshot(context,
instance,
image_name,
extra_properties=metadata)
...
# build location of newly-created image entity
image_id = str(image['id'])
image_ref = glance.generate_image_url(image_id)

resp = webob.Response(status_int=202)
resp.headers['Location'] = image_ref
return resp

由于我的配置文件中没启用cell功能,所以代码中的self.compute_api就是nova/compute/api.py中的API()类,snapshot方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@check_instance_cell
@check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED,
vm_states.PAUSED, vm_states.SUSPENDED])
def snapshot(self, context, instance, name, extra_properties=None):
"""Snapshot the given instance.

:param instance: nova.objects.instance.Instance object
:param name: name of the snapshot
:param extra_properties: dict of extra image properties to include
when creating the image.
:returns: A dict containing image metadata
"""
image_meta = self._create_image(context, instance, name,
'snapshot',
extra_properties=extra_properties)

# NOTE(comstud): Any changes to this method should also be made
# to the snapshot_instance() method in nova/cells/messaging.py
instance.task_state = task_states.IMAGE_SNAPSHOT_PENDING
instance.save(expected_task_state=[None])

self.compute_rpcapi.snapshot_instance(context, instance,
image_meta['id'])

return image_meta

def _create_image(self, context, instance, name, image_type,
extra_properties=None):
"""Create new image entry in the image service. This new image
will be reserved for the compute manager to upload a snapshot
or backup.

:param context: security context
:param instance: nova.objects.instance.Instance object
:param name: string for name of the snapshot
:param image_type: snapshot | backup
:param extra_properties: dict of extra image properties to include

"""
properties = {
'instance_uuid': instance.uuid,
'user_id': str(context.user_id),
'image_type': image_type,
}
properties.update(extra_properties or {})

image_meta = self._initialize_instance_snapshot_metadata(
instance, name, properties)
# if we're making a snapshot, omit the disk and container formats,
# since the image may have been converted to another format, and the
# original values won't be accurate. The driver will populate these
# with the correct values later, on image upload.
if image_type == 'snapshot':
image_meta.pop('disk_format', None)
image_meta.pop('container_format', None)
return self.image_api.create(context, image_meta)

代码中的self.image_api就是调用glanceclient创建镜像,之前已经写过镜像上传这里就不贴代码了,有兴趣的可以去看nova/image/glance.py,看到rpc就知道这里发送一个异步请求,看nova/compute/rpcapi.py代码不出所料:

1
2
3
4
5
6
7
def snapshot_instance(self, ctxt, instance, image_id):
version = '4.0'
cctxt = self.router.by_instance(ctxt, instance).prepare(
server=_compute_host(None, instance), version=version)
cctxt.cast(ctxt, 'snapshot_instance',
instance=instance,
image_id=image_id)

根据套路,这里应该是到nova/compute/manager.py了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
@wrap_exception()
@reverts_task_state
@wrap_instance_fault
@delete_image_on_error
def snapshot_instance(self, context, image_id, instance):
...
try:
instance.task_state = task_states.IMAGE_SNAPSHOT
instance.save(
expected_task_state=task_states.IMAGE_SNAPSHOT_PENDING)
except exception.InstanceNotFound:
# possibility instance no longer exists, no point in continuing
LOG.debug("Instance not found, could not set state %s "
"for instance.",
task_states.IMAGE_SNAPSHOT, instance=instance)
return
except exception.UnexpectedDeletingTaskStateError:
LOG.debug("Instance being deleted, snapshot cannot continue",
instance=instance)
return
self._snapshot_instance(context, image_id, instance,
task_states.IMAGE_SNAPSHOT)

def _snapshot_instance(self, context, image_id, instance,
expected_task_state):
context = context.elevated()
instance.power_state = self._get_power_state(context, instance)
try:
...
self.driver.snapshot(context, instance, image_id,
update_task_state)
instance.task_state = None
instance.save(expected_task_state=task_states.IMAGE_UPLOADING)
self._notify_about_instance_usage(context, instance,
"snapshot.end")
...

然后进入driver中,这里我以使用libvirt为例,位于nova/virt/libvirt/driver.py

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
def snapshot(self, context, instance, image_id, update_task_state):
"""Create snapshot from a running VM instance.

This command only works with qemu 0.14+
"""
...
snapshot = self._image_api.get(context, image_id)
...
# NOTE(bfilippov): save lvm and rbd as raw
if image_format == 'lvm' or image_format == 'rbd':
image_format = 'raw'

metadata = self._create_snapshot_metadata(instance.image_meta,
instance,
image_format,
snapshot['name'])
....
snapshot_backend = self.image_backend.snapshot(instance,
disk_path,
image_type=source_type)
try:
update_task_state(task_state=task_states.IMAGE_UPLOADING,
expected_state=task_states.IMAGE_PENDING_UPLOAD)
metadata['location'] = snapshot_backend.direct_snapshot(
context, snapshot_name, image_format, image_id,
instance.image_ref)
...
self._image_api.update(context, image_id, metadata,
purge_props=False)
...

这里代码太长了我做了删减,所以缩进看起来有点奇怪。经过一系列的判断、准备后,首先获取对应的存储后端,这里我以rbd为例,其他类型的可以看nova/virt/libvirt/imagebackend.pyBackend类的相关定义。然后调用Rbd类下的direct_snapshot方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def direct_snapshot(self, context, snapshot_name, image_format,
image_id, base_image_id):
"""Creates an RBD snapshot directly.
"""
fsid = self.driver.get_fsid()
parent_pool = self._get_parent_pool(context, base_image_id, fsid)
self.driver.create_snap(self.rbd_name, snapshot_name, protect=True)
location = {'url': 'rbd://%(fsid)s/%(pool)s/%(image)s/%(snap)s' %
dict(fsid=fsid,
pool=self.pool,
image=self.rbd_name,
snap=snapshot_name)}
try:
self.driver.clone(location, image_id, dest_pool=parent_pool)
self.driver.flatten(image_id, pool=parent_pool)
finally:
self.cleanup_direct_snapshot(location)
self.driver.create_snap(image_id, 'snap', pool=parent_pool,
protect=True)
return ('rbd://%(fsid)s/%(pool)s/%(image)s/snap' %
dict(fsid=fsid, pool=parent_pool, image=image_id))

这个函数本质上还是一层封装,最终调用rbd驱动提供的clonecreate_snapflatten方法返回一个location给glance。

总结一下,nova创建快照功能是交给底层对应的驱动来处理的,然后调用glance接口创建一条数据最后更新location字段即可。这里多说一句,如果镜像文件特别大的时候使用glance进行同步特别慢,可以参考这里的思路进行优化,使用ceph提供的功能复制后新增glance中的数据即可。