OpenStack之Nova分析——创建虚拟机(七)——创建虚拟机镜像文件

虚拟机的镜像文件主要是指磁盘镜像文件,当然有的还包括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 

至此虚拟机磁盘镜像文件的创建过程就分析完了~~





你可能感兴趣的:(openstack)