1. 程式人生 > >OpenStack之Nova分析——建立虛擬機器(七)——建立虛擬機器映象檔案

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=<base> size=<size> <target>
然後,copy_qcow2_image方法呼叫了disk包的extend方法擴充套件虛擬機器磁碟映象檔案的容量,相當於執行了如下命令:
qemu-img resize <target> <size> e2fsck -fp <target> resize2fs <target>

至此虛擬機器磁碟映象檔案的建立過程就分析完了~~