虚拟机的镜像文件主要是指磁盘镜像文件,当然有的还包括ramdisk和kernel镜像来配合磁盘镜像文件使用,这篇文章我们来重点分析一下创建虚拟机磁盘镜像的整个过程。
虚拟机磁盘镜像是虚拟机正常运行不可缺少的镜像文件,它是虚拟机的主磁盘。先来概述一下创建虚拟机磁盘镜像的整个流程:
1. LibvirtDriver类中的_create_img方法,完成虚拟机镜像文件的创建工作的准备工作后,调用Qcow2类的cache方法。
2. Qcow2类的cache方法调用create_image方法,首先检查本地计算节点上是否缓存了虚拟机所需的磁盘镜像文件。如果不存在缓存文件,则调用libvirt_utils包的fetch_image方法从Glance服务器下载。然后,Qcow2类的create_image方法调用copy_qcow2_img方法创建qcow2格式的虚拟机磁盘镜像文件。
3. libvirt_utils包的fetch_image方法调用fetch_to_raw方法。fetch_to_raw方法首先调用fetch方法从Glance服务器上下载镜像文件,然后检查下载的镜像文件格式是否正确,如果下载的不是raw格式,则将其转化为raw格式。
4. Qcow2类的copy_qcow2_img方法通过执行qemu-img命令完成虚拟机磁盘镜像文件的创建,创建的虚拟机磁盘镜像文件会把换成的镜像文件作为baking file。
接着上一篇文章,来分析创建虚拟机镜像文件的代码
class LibvirtDriver(driver.ComputeDriver):
def _create_image(self, context, instance,
disk_mapping, suffix='',
disk_images=None, network_info=None,
block_device_info=None, files=None, admin_pass=None):
#默认的磁盘镜像文件后缀为空
if not suffix:
suffix = ''
...
#创建保存虚拟机磁盘镜像的目录
fileutils.ensure_tree(basepath(suffix='')
...
if not booted_from_volume:
#获取磁盘镜像在本地的文件名
root_fname = imagecache.get_cache_fname(disk_images, 'image_id')
#获取磁盘镜像的大小
size = instance['root_gb'] * 1024 * 1024 * 1024
if size == 0 or suffix == '.rescue':
size = None
#将磁盘镜像保存在本地
image('disk').cache(fetch_func=libvirt_utils.fetch_image,
context=context,
filename=root_fname,
size=size,
image_id=disk_images['image_id'],
user_id=instance['user_id'],
project_id=instance['project_id'])
...
为了便于理解,我们先看一下传入的“instance”变量中保存的是什么?
{'vm_state': 'building', 'availability_zone': None, 'terminated_at': None, 'ephemeral_gb': 0,
'instance_type_id': 4, 'user_data': 'IyEvYmluL2Jhc2gKdXNlcmFkZCAtbSByb290CnBhc3N3ZCByb290IDw8IEVPRgpyb29vdApyb29vdApFT0YKcGFzc3dkIHJvb3QgPDwgRU9GCnJvb290CnJvb290CkVPRgo=',
'vm_mode': None, 'deleted_at': None, 'reservation_id': 'r-ze5a808m', 'id': 70,
'security_groups': [{'project_id': 'e2c96ea3efe0418cb86fef29aabc2725', 'user_id': 'cc4f87ee9b0d43a4bfe6d1113473f796',
'name': 'default', 'deleted': False, 'created_at': '2014-11-10T02:43:37.000000', 'updated_at': None,
'rules': [...], 'disable_terminate': False, 'root_device_name': None, 'user_id': 'cc4f87ee9b0d43a4bfe6d1113473f796',
'uuid': '19df2538-47c2-49f1-8735-afbc24e68743', 'server_name': None, 'default_swap_device': None,
'info_cache': {'instance_uuid': '19df2538-47c2-49f1-8735-afbc24e68743', 'deleted': False, 'created_at': '2015-02-12T06:05:27.000000', 'updated_at': None, 'network_info': '[]', 'deleted_at': None, 'id': 70},
'hostname': 'test', 'launched_on': None, 'display_description': 'test', 'key_data': None, 'deleted': False,
'scheduled_at': '2015-02-12T06:05:28.000000', 'power_state': 0, 'default_ephemeral_device': None,
'progress': 0, 'project_id': 'e2c96ea3efe0418cb86fef29aabc2725', 'launched_at': None, 'config_drive': '',
'node': 'sts-zestack-02', 'ramdisk_id': '', 'access_ip_v6': None, 'access_ip_v4': None, 'kernel_id': '',
'key_name': None, 'updated_at': '2015-02-12T06:05:28.446678', 'host': 'sts-zestack-02', 'display_name': 'test',
'system_metadata': [...], 'task_state': 'scheduling', 'shutdown_terminate': False, 'cdrom_active': False,
'root_gb': 0, 'locked': False, 'name': 'instance-00000046', 'hypervisor': 'libvirt', 'created_at': '2015-02-12T06:05:27.000000',
'launch_index': 0, 'memory_mb': 512, 'instance_type': {'memory_mb': 512, 'root_gb': 0, 'name': 'm1.tiny', 'deleted': False, 'created_at': None,
'ephemeral_gb': 0, 'updated_at': None, 'disabled': False, 'vcpus': 1, 'flavorid': '1', 'swap': 0, 'rxtx_factor': 1.0, 'is_public': True,
'deleted_at': None, 'vcpu_weight': None, 'id': 4}, 'vcpus': 1, 'image_ref': '11d25bff-1091-4f7a-9a3c-0e48be0cc21b', 'architecture': None, 'auto_disk_config': None, 'os_type': None, 'metadata': []}
逐一分析一下_create_image方法:
1. ensure_tree方法
fileutils对象的ensure方法会检查保存虚拟机镜像文件的目录是否存在,其中的basepath方法是内部方法
#获取保存虚拟机镜像文件的目录
def basepath(fname='', suffix=suffix):
return os.path.join(libvirt_utils.get_instance_path(instance),
fname + suffix)
这个方法最终返回的是保存虚拟机镜像文件的目录。
2. get_cache_fname方法
imagecache对象的get_cache_fname方法的作用是获取缓存在本地的磁盘镜像文件名。在Nova的设计过程中,为了减轻传输磁盘镜像文件对网络造成的负担,可以将计算节点中用到的磁盘镜像文件保存在本地。用户可以在nova.conf配置文件中通过cache_images配置项进行配置,cache_images=all表示缓存所有的镜像文件,cache_images=some表示缓存cache_in_nova=True的镜像文件,cache_images=none表示不缓存镜像文件。
3. cache方法
分析这个方法前,得先看一下image方法
def image(fname, image_type=CONF.libvirt_images_type):
return self.image_backend.image(instance,
fname + suffix, image_type)
其中image_backend是Backend类的对象,所以该方法的返回值是Backend类下image方法的返回值
class Backend(object):
def image(self, instance, disk_name, image_type=None):
backend = self.backend(image_type)
return backend(instance=instance, disk_name=disk_name)
可见该方法会根据实际的需要,创建并返回一个相应类型的backend对象,这些类型包括raw、qcow2和lvm格式的镜像。这里我配置的是qcow2格式的镜像,所以backend对象的image方法会创建并返回一个Qcow2类。
现在可以来看这个cache方法了,通过上面的分析,我们知道,这个cache方法实际上是Qcow2类的cache方法,Qcow2类继承自Image类,cache方法定义下Image类中,这个方法定义如下
class Image(object):
def cache(self, fetch_func, filename, size=None, *args, **kwargs):
def call_if_not_exists(target, *args, **kwargs):
if not os.path.exists(target):
fetch_func(target=target, *args, **kwargs)
elif CONF.libvirt_images_type == "lvm" and \
'ephemeral_size' in kwargs:
fetch_func(target=target, *args, **kwargs)
#获取缓存镜像文件的_base目录
base_dir = os.path.join(CONF.instances_path, CONF.base_dir_name)
#创建base目录
if not os.path.exists(base_dir):
fileutils.ensure_tree(base_dir)
#获取缓存的镜像文件名
base = os.path.join(base_dir, filename)
#如果虚拟机镜像文件或缓存的镜像文件不存在,则创建
if not os.path.exists(self.path) or not os.path.exists(base):
self.create_image(call_if_not_exists, base, size,
*args, **kwargs)
...
这个方法首先获取缓存镜像文件的_base目录,这里的目录是通过nova.conf配置文件进行配置的。通常的配置为instances_path=/opt/stack/data/instance,base_dir_name=_base,所以拼接后的镜像文件的缓存目录为/opt/stack/data/_base。接下来,如果上述目录不存在,则会创建这个目录。完成针对目录的操作后,会检查是否存在虚拟机的镜像文件或缓存的镜像文件,如果不存在,则会调用create_image方法来创建一个镜像文件。
我们来分析create_image这个方法。Qcow2类重载了其父类Image类的create_image方法,其定义如下
class Qcow2(Image):
def create_image(self, prepare_template, base, size, *args, **kwargs):
#如果base目录下不存在缓存的镜像文件,则从Glance服务器下载
if not os.path.exists(base):
prepare_template(target=base, *args, **kwargs)
...
#如果镜像文件没有创建,则创建镜像文件
if not os.path.exists(self.path):
with utils.remove_path_on_error(self.path):
copy_qcow2_image(base, self.path, size)
这个方法分别调用了prepare_template方法和copy_qcow2_image方法。如果在base目录下没有发现缓存的镜像文件,则调用prepare_template方法,从Glance服务器下载镜像文件并缓存到base目录下;如果发现镜像文件没有创建,则调用copy_qcow2_image方法创建镜像文件。
下面分别来分析这两个方法:
1. prepare_template方法
这个方法是Image类的cache方法以参数的形式传递过来的,其传入的参数为call_if_not_exists方法。从上面的代码中,可以看到call_if_not_exists方法的定义
def cache(self, fetch_func, filename, size=None, *args, **kwargs):
def call_if_not_exists(target, *args, **kwargs):
if not os.path.exists(target):
fetch_func(target=target, *args, **kwargs)
elif CONF.libvirt_images_type == "lvm" and \
'ephemeral_size' in kwargs:
fetch_func(target=target, *args, **kwargs)
...
该方法是cache方法的内部方法,还是会再次检查缓存的镜像文件是否存在,如果不存在,则会调用fetch_func方法从Glance服务器上下载。fetch_func方法同样是通过参数传递进来的,查看上面LibertDriver类的_create_image方法,我们看到,fetch_func的值为libvirt_utils包下fetch_image方法。其定义如下
def fetch_image(context, target, image_id, user_id, project_id):
images.fetch_to_raw(context, image_id, target, user_id, project_id)
fetch_image方法调用了images包下的fetch_to_raw方法,其定义如下
def fetch_to_raw(context, image_href, path, user_id, project_id):
#构造缓存文件的临时文件名
path_tmp = "%s.part" % path
#将镜像文件从Glance服务器下载并缓存到本地
fetch(context, image_href, path_tmp, user_id, project_id)
with utils.remove_path_on_error(path_tmp):
#查询缓存的镜像文件的信息
data = qemu_img_info(path_tmp)
#获取缓存镜像文件的格式
fmt = data.file_format
if fmt is None:
raise exception.ImageUnacceptable(
reason=_("'qemu-img info' parsing failed."),
image_id=image_href)
backing_file = data.backing_file
#Glance服务器上的镜像文件不允许有backing_file
if backing_file is not None:
raise exception.ImageUnacceptable(image_id=image_href,
reason=_("fmt=%(fmt)s backed by: %(backing_file)s") % locals())
#将镜像文件转化为raw格式
if fmt != "raw" and CONF.force_raw_images:
staged = "%s.converted" % path
LOG.debug("%s was %s, converting to raw" % (image_href, fmt))
with utils.remove_path_on_error(staged):
#将缓存文件转化为raw格式
convert_image(path_tmp, staged, 'raw')
#删除转化前的格式
os.unlink(path_tmp)
#验证转化是否成功
data = qemu_img_info(staged)
if data.file_format != "raw":
raise exception.ImageUnacceptable(image_id=image_href,
reason=_("Converted to raw, but format is now %s") %
data.file_format)
os.rename(staged, path)
else:
os.rename(path_tmp, path)
fetch_to_raw方法主要做了3件事:
(1) 从Glance服务器下载镜像到本地
主要是调用了fetch方法从Glance服务器下载镜像文件,fetch方法定义如下
def fetch(context, image_href, path, _user_id, _project_id):
(image_service, image_id) = glance.get_remote_image_service(context,
image_href)
with utils.remove_path_on_error(path):
with open(path, "wb") as image_file:
image_service.download(context, image_id, image_file)
glance对象的get_remote_image_service方法首先会构造一个用于连接Glance服务器的客户端,这个客户端会向封装并向Glance服务器发送HTTP请求,Glance服务器会通过WSGI服务接收并处理这些请求。然后把刚刚构造的客户端当作参数传给GlanceImageService类并最终返回这个类的对象,即变量image_service,image_service对象下的download方法下载镜像文件。
(2) 检查下载的镜像文件格式是否正确
首先,调用qemu_img_info方法查询镜像文件的详细信息,这个方法相当于执行了qemu-img info命令。然后,获取通过file_format方法获取镜像文件格式,并判断格式是否正确。最后,由于Glance服务器上的镜像文件必须是独立的,不允许有backing_file,所以还需要验证一下是否存在backing_file。
(3) 将非raw格式的镜像文件转化为raw格式
2. copy_qcow2_image方法
copy_qcow2_image方法是Qcow2类create_image方法的内部方法,其定义如下
def copy_qcow2_image(base, target, size):
#为虚拟机创建qcow2格式的磁盘镜像文件
libvirt_utils.create_cow_image(base, target)
if size:
#扩展磁盘镜像文件的容量
disk.extend(target, size)
copy_qcow2_image方法首先会调用libvirt_utils包的create_cow_image方法为虚拟机创建磁盘镜像文件。相当于执行了如下命令:
qemu-img create -f qcow2 -o backing_file= size=
然后,copy_qcow2_image方法调用了disk包的extend方法扩展虚拟机磁盘镜像文件的容量,相当于执行了如下命令:
qemu-img resize e2fsck -fp resize2fs