OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机启动源码实现(2)

时间:2020-12-17 08:42:53

感谢朋友支持本博客,欢迎共同探讨交流,由于能力和时间有限,错误之处在所难免,欢迎指正!
如果转载,请保留作者信息。
博客地址:http://blog.csdn.net/gaoxingnengjisuan
邮箱地址:dong.liu@siat.ac.cn


来继续解析方法_create_image,这是建立虚拟机过程中比较重要的一个方法,它实现了虚拟机镜像的建立。,需要注意的是,这个方法并没有实现新建立的虚拟机的启动。具体来看方法_create_image:

def _create_image(self, context, instance, libvirt_xml,
disk_mapping, suffix='',
disk_images=None, network_info=None,
block_device_info=None, files=None, admin_pass=None):
"""
调用之一传进来的参数:
# context:上下文信息;
# instance:实例信息;
# libvirt_xml:为新建立的实例参数获取配置数据conf,并把获取的数据conf转换为xml格式;
# disk_mapping=disk_info['mapping']:来宾系统磁盘的映射信息;
# network_info=network_info:转换为传统格式的网络资源信息;
# block_device_info=block_device_info:实例错误记录的块设备;
# files=injected_files:编码后的注入文件;
# admin_pass=admin_password:admin密码;
# suffix='';
# disk_images=None;
"""
if not suffix:
suffix = ''

# 从volume启动设置(这里不明白是什么意思);
booted_from_volume = (
(not bool(instance.get('image_ref')))
or 'disk' not in disk_mapping
)

def basepath(fname='', suffix=suffix):
return os.path.join(libvirt_utils.get_instance_path(instance), fname + suffix)

def image(fname, image_type=CONF.libvirt_images_type):
return self.image_backend.image(instance, fname + suffix, image_type)

def raw(fname):
"""传进来的值为'kernel';"""
return image(fname, image_type='raw')

# ensure_tree:按照path的路径信息,建立一个完整的路径,包括所有需要的上级路径;
fileutils.ensure_tree(basepath(suffix=''))

# 提示建立镜像实例;
LOG.info(_('Creating image'), instance=instance)

# basepath('libvirt.xml'):获取'libvirt.xml'的完整路径;
# 把传进来的包含实例参数获取配置数据的xml格式文件libvirt_xml的内容写入libvirt.xml之中;
libvirt_utils.write_to_file(basepath('libvirt.xml'), libvirt_xml)

# 如果文件console.log存在,则改变它的所有权者为os;
self._chown_console_log_for_instance(instance)

# self._get_console_log_path(instance):获取文件console.log的路径;
# umask=007:设置限制新文件权限的掩码;
# 写入一个空值进入文件console.log,所以这条语句的实际作用是设置umask=007(设置限制新文件权限的掩码);
libvirt_utils.write_to_file(self._get_console_log_path(instance), '', 007)

# 如果disk_images没有定义赋值,则获取image_id、kernel_id和ramdisk_id给disk_images;
if not disk_images:
disk_images = {'image_id': instance['image_ref'],
'kernel_id': instance['kernel_id'],
'ramdisk_id': instance['ramdisk_id']}

if disk_images['kernel_id']:
# get_cache_fname:基于给定的image镜像ID的SHA1哈希算法,返回文件名;
# 返回disk_images['kernel_id']给fname;
fname = imagecache.get_cache_fname(disk_images, 'kernel_id')

# cache:从模板创建符合要求的镜像image;
# fetch_func=libvirt_utils.fetch_image(见下面详解):通过直接下载或者glance客户端下载方式下载获取的image镜像数据文件的方法;
# context:上下文信息;
# filename=fname:上面获取的文件名(返回的其实是disk_images['kernel_id']);
# image_id=disk_images['kernel_id']:这里image_id和filename的值是一样的;
# user_id=instance['user_id'];
# project_id=instance['project_id'];

# fetch_func=libvirt_utils.fetch_image:通过直接下载或者glance客户端下载方式下载获取的image镜像数据文件的方法;
# 这里还没有执行这个方法,这里只是先指定要运行这个方法,继续跟踪进去,会执行这个方法;
# 简单解释一下这个方法:
# 通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
# 从获取的镜像中解析出来的输出数据data;
# 如果获取的数据data的格式不是raw,并且系统定义了要强制备份文件为raw格式;
# 则转换获取的镜像数据文件格式为raw;
# 如果没有设置强制备份文件格式为raw,则直接返回文件数据;
raw('kernel').cache(fetch_func=libvirt_utils.fetch_image,
context=context,
filename=fname,
image_id=disk_images['kernel_id'],
user_id=instance['user_id'],
project_id=instance['project_id'])

if disk_images['ramdisk_id']:
# get_cache_fname:基于给定的image镜像ID的SHA1哈希算法,返回文件名;
# 返回disk_images['ramdisk_id']给fname;
fname = imagecache.get_cache_fname(disk_images, 'ramdisk_id')
# cache:从模板创建符合要求的镜像image;
raw('ramdisk').cache(fetch_func=libvirt_utils.fetch_image,
context=context,
filename=fname,
image_id=disk_images['ramdisk_id'],
user_id=instance['user_id'],
project_id=instance['project_id'])

# extract_instance_type:从实例的system_metadata属性中提取instance_type信息;
inst_type = instance_types.extract_instance_type(instance)

if not booted_from_volume:
# get_cache_fname:基于给定的image镜像ID的SHA1哈希算法,返回文件名;
# 返回disk_images['image_id']给fname;
root_fname = imagecache.get_cache_fname(disk_images, 'image_id')

# 'm1.tiny': dict(mem=512, vcpus=1, root_gb=0, eph_gb=0, flavid=1),
# 'm1.small': dict(mem=2048, vcpus=1, root_gb=20, eph_gb=0, flavid=2),
# 'm1.medium': dict(mem=4096, vcpus=2, root_gb=40, eph_gb=0, flavid=3),
# 'm1.large': dict(mem=8192, vcpus=4, root_gb=80, eph_gb=0, flavid=4),
# 'm1.xlarge': dict(mem=16384, vcpus=8, root_gb=160, eph_gb=0, flavid=5)
size = instance['root_gb'] * 1024 * 1024 * 1024

if size == 0 or suffix == '.rescue':
size = None

# cache:从模板创建符合要求的镜像image;
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'])

# 获取虚拟机系统的版本名;
os_type_with_default = instance['os_type']
if not os_type_with_default:
os_type_with_default = 'default'

ephemeral_gb = instance['ephemeral_gb']
if 'disk.local' in disk_mapping:
fn = functools.partial(self._create_ephemeral,
fs_label='ephemeral0',
os_type=instance["os_type"])
fname = "ephemeral_%s_%s" % (ephemeral_gb, os_type_with_default)
size = ephemeral_gb * 1024 * 1024 * 1024

# cache:从模板创建符合要求的镜像image;
image('disk.local').cache(fetch_func=fn,
filename=fname,
size=size,
ephemeral_size=ephemeral_gb)

for eph in driver.block_device_info_get_ephemerals(block_device_info):
fn = functools.partial(self._create_ephemeral,
fs_label='ephemeral%d' % eph['num'],
os_type=instance["os_type"])
size = eph['size'] * 1024 * 1024 * 1024
fname = "ephemeral_%s_%s" % (eph['size'], os_type_with_default)

# cache:从模板创建符合要求的镜像image;
image(blockinfo.get_eph_disk(eph)).cache(
fetch_func=fn,
filename=fname,
size=size,
ephemeral_size=eph['size'])

if 'disk.swap' in disk_mapping:
mapping = disk_mapping['disk.swap']
swap_mb = 0

swap = driver.block_device_info_get_swap(block_device_info)
if driver.swap_is_usable(swap):
swap_mb = swap['swap_size']
elif (inst_type['swap'] > 0 and
not block_device.volume_in_mapping(
mapping['dev'], block_device_info)):
swap_mb = inst_type['swap']

if swap_mb > 0:
size = swap_mb * 1024 * 1024

# cache:从模板创建符合要求的镜像image;
image('disk.swap').cache(fetch_func=self._create_swap,
filename="swap_%s" % swap_mb,
size=size,
swap_mb=swap_mb)

# 驱动配置;
if configdrive.required_by(instance):
LOG.info(_('Using config drive'), instance=instance)
extra_md = {}
if admin_pass:
extra_md['admin_pass'] = admin_pass

# InstanceMetadata:虚拟机实例元数据类;
# 获取虚拟机实例元数据操作类的实例化对象;
# instance:虚拟机实例信息;
# content=files:注入文件;
# extra_md=extra_md:密码;
inst_md = instance_metadata.InstanceMetadata(instance, content=files, extra_md=extra_md)

# ConfigDriveBuilder:获取配置驱动文件类的实例化对象;
# 在类的实例化过程中,主要完成了以下工作:
# 建立存储配置驱动临时文件的目录文件;
# 根据instance_md获取实例元数据,并根据版本version更新完善不同版本的EC2类型的实例元数据;
# 并把实例元数据写入到配置驱动的临时存储文件;

# 根据完善后的不同版本的实例元数据,生成ISO格式或vfat格式的镜像文件,默认是ISO格式的;
with configdrive.ConfigDriveBuilder(instance_md=inst_md) as cdb:
# 生成镜像文件以后放置的路径;
configdrive_path = basepath(fname='disk.config')
LOG.info(_('Creating config drive at %(path)s'),
{'path': configdrive_path}, instance=instance)

# 根据配置参数选择,生成ISO格式或vfat格式的镜像文件,默认是ISO格式的;
try:
cdb.make_drive(configdrive_path)
except exception.ProcessExecutionError, e:
with excutils.save_and_reraise_exception():
LOG.error(_('Creating config drive failed '
'with error: %s'),
e, instance=instance)

# File injection
# 文件注入;
elif CONF.libvirt_inject_partition != -2:
# 要注入文件的目标分区号;
target_partition = None
if not instance['kernel_id']:
# 如果不是kernel_id镜像;
target_partition = CONF.libvirt_inject_partition
if target_partition == 0:
target_partition = None
# 如果虚拟机实例类型为lxc,则目标分区号设置为None;
if CONF.libvirt_type == 'lxc':
target_partition = None

# 如果定义了开机时注入ssh公钥,而且实例中具有'key_data'数据,则获取这个'key_data'数据;
# libvirt_inject_key:这个参数定义了在开机时,是否注入ssh公钥;
# 参数的默认值为True;
if CONF.libvirt_inject_key and instance['key_data']:
key = str(instance['key_data'])
else:
key = None

# get_injected_network_template:根据给定的网络信息返回一个渲染好的网络模板;
net = netutils.get_injected_network_template(network_info)

# 获取虚拟机实例的元数据metadata;
metadata = instance.get('metadata')

# libvirt_inject_password:这个参数定义了在开机时,是否注入管理员密码;
# 参数的默认值为False;
if not CONF.libvirt_inject_password:
admin_pass = None

# 如果key, net, metadata, admin_pass, files有一项不为none,就执行下面的代码;
if any((key, net, metadata, admin_pass, files)):
# If we're not using config_drive, inject into root fs
injection_path = image('disk').path
img_id = instance['image_ref']

for inj in ('key', 'net', 'metadata', 'admin_pass', 'files'):
if locals()[inj]:
LOG.info(_('Injecting %(inj)s into image '
'%(img_id)s'), locals(), instance=instance)
# inject_data:注入指定的项目到指定的磁盘镜像;
# injection_path:要注入磁盘镜像的存储路径;
# key, net, metadata, admin_pass, files:要注入的项目;
# partition=target_partition:要注入磁盘的分区号;
# use_cow=CONF.use_cow_images:这个参数定义了是否使用cow格式的镜像文件,默认值为True;
try:
disk.inject_data(injection_path,
key, net, metadata, admin_pass, files,
partition=target_partition,
use_cow=CONF.use_cow_images,
mandatory=('files',))
except Exception as e:
with excutils.save_and_reraise_exception():
LOG.error(_('Error injecting data into image '
'%(img_id)s (%(e)s)') % locals(),
instance=instance)

if CONF.libvirt_type == 'uml':
libvirt_utils.chown(image('disk').path, 'root')

方法_create_image主要完成了三部分的工作,下面将进行具体的解析,其他代码解析见上文中代码的注释:

1.调用类Image下的方法cache,来实现对各种类型磁盘镜像的建立,这里以“kernel”为例,来分析代码的实现过程。

if disk_images['kernel_id']:
# get_cache_fname:基于给定的image镜像ID的SHA1哈希算法,返回文件名;
# 返回disk_images['kernel_id']给fname;
fname = imagecache.get_cache_fname(disk_images, 'kernel_id')

# raw('kernel'):
# return image('kernel', image_type='raw')

# cache:从模板创建符合要求的镜像image;
# 方法参数解析:
# fetch_func=libvirt_utils.fetch_image(见下面详解):通过直接下载或者glance客户端下载方式下载获取的image镜像数据文件的方法;
# context:上下文信息;
# filename=fname:上面获取的文件名(返回的其实是disk_images['kernel_id']);
# image_id=disk_images['kernel_id']:这里image_id和filename的值是一样的;
# user_id=instance['user_id'];
# project_id=instance['project_id'];

# fetch_func=libvirt_utils.fetch_image:通过直接下载或者glance客户端下载方式下载获取的image镜像数据文件的方法;
# 这里还没有执行这个方法,这里只是先指定要运行这个方法,继续跟踪进去,会执行这个方法;
# 简单解释一下这个方法:
# 通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
# 从获取的镜像中解析出来的输出数据data;
# 如果获取的数据data的格式不是raw,并且系统定义了要强制备份文件为raw格式;
# 则转换获取的镜像数据文件格式为raw;
# 如果没有设置强制备份文件格式为raw,则直接返回文件数据;
raw('kernel').cache(fetch_func=libvirt_utils.fetch_image,
context=context,
filename=fname,
image_id=disk_images['kernel_id'],
user_id=instance['user_id'],
project_id=instance['project_id'])
首先调用方法get_cache_fname,获取镜像id值image_id,具体来看代码实现:

def get_cache_fname(images, key):
"""
基于给定的image镜像ID的SHA1哈希算法,返回image_id;
"""

# 从images获取image_id;
image_id = str(images[key])
# remove_unused_kernels:这个参数定义了是否要删除不用的内核镜像;
# 参数的默认值是不开启删除不用的内核镜像的功能,但是后续版本会开启这个功能的;
if not CONF.remove_unused_kernels and key in ['kernel_id', 'ramdisk_id']:
return image_id
else:
return hashlib.sha1(image_id).hexdigest()
接下来分析最重要的语句raw('kernel').cache(......)来实现镜像的建立。

raw('kernel')根据image_type='raw',辗转反侧,实现的是对/nova/irt/libvirt/imagebackend.py中的类Raw进行初始化,获取它的实例化对象。具体来看Raw的代码(代码比较容易理解,无非是一些参数的初始化过程,这里就不多作解析):

class Raw(Image):
def __init__(self, instance=None, disk_name=None, path=None, snapshot_name=None):
super(Raw, self).__init__("file", "raw", is_block_dev=False)

self.path = (path or os.path.join(libvirt_utils.get_instance_path(instance), disk_name))
self.snapshot_name = snapshot_name
self.preallocate = CONF.preallocate_images != 'none'
并进一步来看其父类的初始化方法:

class Image(object):
__metaclass__ = abc.ABCMeta

def __init__(self, source_type, driver_format, is_block_dev=False):
"""
Image类的初始化;

:source_type:块或者文件;
:driver_format:raw或者qcow2;
"""
self.source_type = source_type
self.driver_format = driver_format
self.is_block_dev = is_block_dev
self.preallocate = False

# CONF.instances_path:这个参数定义了实例在磁盘disk上的存储路径;
# 获取locks的路径lock_path;
下面来分析方法cache的实现过程:

我们先来看看方法cache的参数:

raw('kernel').cache(fetch_func=libvirt_utils.fetch_image,
                                context=context,
                                filename=fname,
                                image_id=disk_images['kernel_id'],
                                user_id=instance['user_id'],
                                project_id=instance['project_id'])

fetch_func=libvirt_utils.fetch_image:这个参数实际上指定了实现获取镜像的方法,所指定的方法能够实现通过直接下载或者glance客户端下载方式下载获取的image镜像数据文件。这里还没有执行这个方法,这里只是先指定要运行这个方法,继续跟踪进去方法cache中,会执行这里指定的方法,来实现获取image镜像数据。

context:上下文信息。

filename=fname:上面获取的文件名(返回的其实是disk_images['kernel_id']);

image_id=disk_images['kernel_id']:这里image_id和filename的值是一样的;

user_id=instance['user_id']:获取user id值;

project_id=instance['project_id']):获取project id值;

下面就来看方法cache的具体实现过程:

def cache(self, fetch_func, filename, size=None, *args, **kwargs):
"""
从模板创建符合要求的镜像image;

# 调用之一传进来的参数:
# fetch_func=libvirt_utils.fetch_image:通过直接下载或者glance客户端下载方式下载获取的image镜像数据文件的方法(建立基本image镜像的函数;);
# context:上下文信息;
# filename=fname:获取的文件名,即镜像目录中的文件名称(返回的其实是disk_images['kernel_id']);
# image_id=disk_images['kernel_id']:这里image_id和filename的值是一样的;
# user_id=instance['user_id'];
# project_id=instance['project_id'];

size: 所建立镜像的byte大小(可选);
"""

# synchronized:这个方法实现了一个同步装饰器;
# 确保同一时刻只有一个线程在运行方法call_if_not_exists;
# filename:镜像目录中的文件名称(传进来的其实是disk_images['kernel_id']);
@lockutils.synchronized(filename, 'nova-', external=True, lock_path=self.lock_path)
def call_if_not_exists(target, *args, **kwargs):
"""
如果还没有获取镜像数据,则调用方法fetch_image获取数据;
如果指定的镜像格式为lvm,则调用方法fetch_image获取数据;
注:这个方法真正被调用是在def create_image这个方法中;
"""

# target:传进来的是存储实例镜像的正确完整路径;

# fetch_func=libvirt_utils.fetch_image:这个方法所执行的功能如下:
# 通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
# 从获取的镜像中解析出来的输出数据data;
# 如果获取的数据data的格式不是raw,并且系统定义了要强制备份文件为raw格式;
# 则转换获取的镜像数据文件格式为raw;
# 如果没有设置强制备份文件格式为raw,则直接返回文件数据;
if not os.path.exists(target):
fetch_func(target=target, *args, **kwargs)
# 这个参数定义了虚拟机镜像的格式,可以接受的格式包括raw、qcow2、lvm。
# 如果参数指定的是default,则由use_cow_images变量来指定;
elif CONF.libvirt_images_type == "lvm" and 'ephemeral_size' in kwargs:
fetch_func(target=target, *args, **kwargs)

# 这条语句通过两个配置参数得到了实例在磁盘disk上的完整的存储路径;
# CONF.instances_path:这个参数定义了实例在磁盘disk上的存储路径;
# CONF.base_dir_name:这个参数定义了缓存镜像的存储路径,这不是一个完整的路径,只是一个文件夹的名称;参数的默认值为'_base';
base_dir = os.path.join(CONF.instances_path, CONF.base_dir_name)

# 如果base_dir不存在,就按照base_dir的信息建立一个路径;
if not os.path.exists(base_dir):
fileutils.ensure_tree(base_dir)
# 得到完整的镜像路径名;
base = os.path.join(base_dir, filename)

# 如果base指示的镜像信息还没有建立,就建立一个符合要求的镜像;
if not os.path.exists(self.path) or not os.path.exists(base):
# create_image:从模板建立一个指定格式的镜像;
# 注:一共有三个create_image方法,分别实现建立COW、raw和LVM格式的实例镜像;
# 注:这里需要仔细的研究,看看调用的cmd命令行是最终如何实现实例镜像的建立的;
# call_if_not_exists:如果还没有获取镜像数据,则调用方法fetch_image获取数据,如果指定的镜像格式为lvm,则调用方法fetch_image获取数据;
# base:完整的镜像路径名称;
# size:所建立镜像的byte大小(可选);
self.create_image(call_if_not_exists, base, size, *args, **kwargs)

if size and self.preallocate and self._can_fallocate():
utils.execute('fallocate', '-n', '-l', size, self.path)
我们来逐条进行代码的解析:

(1)base_dir = os.path.join(CONF.instances_path, CONF.base_dir_name)
        # 如果base_dir不存在,就按照base_dir的信息建立一个路径;
        if not os.path.exists(base_dir):
            fileutils.ensure_tree(base_dir)
        # 得到完整的镜像路径名;
        base = os.path.join(base_dir, filename)

这段代码实现的功能是得到实例在磁盘上的完整的路径;

首先,通过两个配置参数,得到了实例在磁盘disk上的完整的存储路径(文件夹);

其中,CONF.instances_path:这个参数定义了实例在磁盘disk上的存储路径;CONF.base_dir_name:这个参数定义了缓存镜像的存储路径,这不是一个完整的路径,只是一个文件夹的名称,参数的默认值为'_base';

如果配置文件指定的路径不存在就建立一个;

然后,合成路径和文件名,从而得到磁盘上存储实例的完整的路径;

(2)if not os.path.exists(self.path) or not os.path.exists(base):
            self.create_image(call_if_not_exists, base, size, *args, **kwargs)

这段代码实现的功能就是如果实例镜像还没有建立,则建立指定格式的实例镜像;

具体实现这个过程的就是方法create_image,经过阅读代码得知,一共有三个create_image方法,分别实现建立COW、raw和LVM格式的实例镜像,具体调用哪个方法,是根据前面指定的实例格式在前面初始化相应的类的时候就已经确定了(如类Raw的初始化中)。

这里我们仅仅以raw格式镜像的建立方法create_image的分析为例,看看如何实现指定格式实例镜像的建立。先来看代码:

def create_image(self, prepare_template, base, size, *args, **kwargs):
"""
从模板建立一个raw格式镜像;
# 调用之一传进来的参数:
# prepare_template:建立实例模板的方法;
# prepare_template=call_if_not_exists:如果还没有获取镜像数据,则调用方法fetch_image获取数据,如果指定的镜像格式为lvm,则调用方法fetch_image获取数据;
# base:完整的镜像路径名称;
# size:所建立镜像的byte大小(可选);

调用prepare_template传进来的方法建立一个raw格式的镜像;
"""

# synchronized:这个方法实现了一个同步装饰器;
# 确保同一时刻只有一个线程在运行copy_raw_image方法;
@lockutils.synchronized(base, 'nova-', external=True, lock_path=self.lock_path)
def copy_raw_image(base, target, size):
# copy_image:复制磁盘镜像到一个存在的目录中(src拷贝到dest);
libvirt_utils.copy_image(base, target)
# 根据定义的size镜像大小,改变当前镜像的大小;
# 如果当前镜像文件大小大于规定的大小,则不变;
# 如果当前镜像文件大小小于规定的大小,则增加镜像文件的大小到规定的值;
if size:
disk.extend(target, size)

# 如果kwargs中没有定义'image_id'这个属性;
generating = 'image_id' not in kwargs
if generating:
#Generating image in place
# target=self.path:由类的初始化方法可知:
# self.path = os.path.join(libvirt_utils.get_instance_path(instance), disk_name)
# target=self.path:确定实例存储的正确的完整路径;

# 调用上面指定的方法建立符合要求的实例镜像;
prepare_template(target=self.path, *args, **kwargs)
else:
# 调用上面指定的方法建立符合要求的实例镜像;
prepare_template(target=base, *args, **kwargs)
# remove_path_on_error:如果出现任何异常,都要删除path指定的文件;
# copy_raw_image:复制磁盘镜像到一个存在的目录中(base拷贝到path);
if not os.path.exists(self.path):
with utils.remove_path_on_error(self.path):
copy_raw_image(base, self.path, size)
这个方法实际上就是调用prepare_template传进来的方法,来完成指定格式的镜像文件的创建。

而prepare_template传进来的方法,实际上就是前面方法cache中所定义的参数:fetch_func=libvirt_utils.fetch_image

这里我们就进一步分析方法fetch_image,来看到底如何建立指定格式的实例镜像。

def fetch_image(context, target, image_id, user_id, project_id):
"""
通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
从获取的镜像中解析出来的输出数据data;
如果获取的数据data的格式不是raw,并且系统定义了要强制备份文件为raw格式;
转换获取的镜像数据文件格式为raw;

注:target传进来的是实例镜像正确的完整的存储路径;
"""

# fetch_to_raw:
# 以给定的image_href中解析image_id值;
# 通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
# 从获取的镜像中解析出来包含镜像信息的对象data
# 如果镜像文件格式不是raw,并且系统定义了要强制备份文件为raw格式;
# 则转换获取的镜像数据文件格式为raw;
# 如果没有设置强制备份文件格式为raw,则直接返回文件数据;
images.fetch_to_raw(context, image_id, target, user_id, project_id)
这里调用了方法fetch_to_raw,我们来看这个方法:

def fetch_to_raw(context, image_href, path, user_id, project_id):
"""
以给定的image_href中解析image_id值;
通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
从获取的镜像中解析出来包含镜像信息的对象data
如果镜像文件的格式不是raw,并且系统定义了要强制备份文件为raw格式;
则转换获取的镜像数据文件格式为raw;
如果没有设置强制备份文件格式为raw,则直接返回文件数据;

注:path传进来的是实例镜像正确的完整的存储路径;
"""
path_tmp = "%s.part" % path
# fetch:
# 以给定的image_href中解析image_id值;
# 通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据;
# 把下载的镜像数据拷贝到path_tmp文件之中;
fetch(context, image_href, path_tmp, user_id, project_id)

# 如果出现任何异常,都要删除path_tmp指定的文件;
with utils.remove_path_on_error(path_tmp):
# qemu_img_info:返回一个QemuImgInfo类的对象;
# 这个对象包含了从path_tmp指定的qemu镜像信息中解析出来的输出数据out;
data = qemu_img_info(path_tmp)

# 获取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
if backing_file is not None:
raise exception.ImageUnacceptable(image_id=image_href,
reason=_("fmt=%(fmt)s backed by: %(backing_file)s") % locals())

# 如果获取的镜像文件的格式不是raw,并且系统定义了要强制备份文件为raw格式;
# 转换path_tmp指定的镜像文件格式为raw;
# 重命名path_tmp为path;
# CONF.force_raw_images:这个参数定义了是否要强制备份镜像文件为raw格式;
# 参数的默认值为True;
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):
# 转换path_tmp指定的镜像文件格式为raw,存储于staged,删除原有path_tmp文件;
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,看看它完成了哪些功能。

a.fetch(context, image_href, path_tmp, user_id, project_id)

这条语句实现了调用方法fetch从给定的image_href中解析image_id值,通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据,把下载的镜像数据拷贝到path_tmp文件之中。(后面会详细解析方法fetch)

b.with utils.remove_path_on_error(path_tmp):
        data = qemu_img_info(path_tmp)

        # 获取path_tmp中保存的镜像的文件格式;
        fmt = data.file_format
        ......

        # 获取镜像的备份文件;
        backing_file = data.backing_file
        ......

首先方法remove_path_on_error实现了一旦出现任何异常,都会删除path_tmp指定的文件,即前面获取的镜像数据。

然后获取从qemu-img信息中解析出来的关于path_tmp镜像的对象信息。

接下来,从data对象中获取镜像的文件格式。

再获取镜像的备份文件信息;

c.if fmt != "raw" and CONF.force_raw_images:
        ......
        with utils.remove_path_on_error(staged):
            # 转换path_tmp指定的镜像文件格式为raw,存储于staged,删除原有path_tmp文件;
            convert_image(path_tmp, staged, 'raw')
            os.unlink(path_tmp)

            data = qemu_img_info(staged)
            if data.file_format != "raw":
            ......
            os.rename(staged, path)
   else:
       os.rename(path_tmp, path)

如果镜像文件格式不是raw,并且配置文件定义了要强制镜像备份文件为raw格式,则:

转换path_tmp指定的镜像文件格式为raw;

并重命名路径为path;

如果镜像文件格式是raw,或者配置文件没有强制指定格式为raw,则:

直接重命名path_tmp路径为path。

至此,完成了获取指定格式镜像文件的这个方法fetch_to_raw的流程。

下面我们进一步解析方法fetch,来看看到底是如何下载镜像文件的。来看方法fetch的实现代码:

def fetch(context, image_href, path, _user_id, _project_id):
"""
以给定的image_href中解析image_id值;
通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据到path指定的文件image_file;
"""

# 目前glance中还没有auth验证,所以我们假设我们的访问是已经被验证的;

# get_remote_image_service:建立一个image_service并且从给定的image_href中解析它的ID值;
# 并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
# 每次调用的时候将会使用到这个对象;
(image_service, image_id) = glance.get_remote_image_service(context, image_href)

# remove_path_on_error:如果出现任何异常,都要删除path指定的文件;
# 这条语句的意思是,在确保path没有任何错误异常的情况下,再执行后面的语句;
with utils.remove_path_on_error(path):
# 以读的方式打开path所指定的文件;
# download:通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据到文件image_file;
with open(path, "wb") as image_file:
image_service.download(context, image_id, image_file)
在这个方法中,完成任务的实际上是两条语句,即:

(image_service, image_id) = glance.get_remote_image_service(context, image_href)和image_service.download(context, image_id, image_file)

我们来看这两条语句:

首先方法get_remote_image_service实现的是建立一个image_service并且从给定的image_href中解析它的ID值,返回两个值赋值给image_service和image_id;这个方法的实现的过程中还建立一个glance的客户端对象,具体方法是应用相关参数实例化一个新的glanceclient.Client对象,每次调用的时候将会使用到这个对象。具体来看方法get_remote_image_service的代码:

def get_remote_image_service(context, image_href):
"""
建立一个image_service并且从给定的image_href中解析它的ID值;
并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
每次调用的时候将会使用到这个对象;

# 调用之一传进来的参数:
# context:上下文信息;
# image_ref:实例的镜像相关属性信息;
"""
LOG.debug(_("fetching image %s from glance") % image_href)

if '/' not in str(image_href):
image_service = get_default_image_service()
return image_service, image_href

try:
# 从image_href中解析出image_id, glance_host, glance_port, use_ssl个值出来;
# 可见image_href是个复合的参数;
(image_id, glance_host, glance_port, use_ssl) = _parse_image_ref(image_href)

# GlanceClientWrapper(glance客户端包装类的初始化方法)
# 这个初始化方法实现了建立一个glance客户端,每次调用的时候将会使用;
# 具体实现是应用相关参数实例化一个新的glanceclient.Client对象,返回这个对象给glance_client;
glance_client = GlanceClientWrapper(context=context,host=glance_host, port=glance_port, use_ssl=use_ssl)
except ValueError:
raise exception.InvalidImageRef(image_href=image_href)

# GlanceImageService类的初始化方法实现了首先根据client值判断是否已经建立了glance客户端对象;
# 如果没有建立,则会调用GlanceClientWrapper类的初始化方法建立一个glance客户端;
image_service = GlanceImageService(client=glance_client)
return image_service, image_id
这里就不对这个方法进行逐条解析了,因为我已经做了比较详细的注释,可以直接看代码注释就好了。

其次,调用方法download实现通过直接下载或者glance客户端下载方式,下载image_id指定的镜像数据到打开的path指定的文件image_file。具体来看方法download的代码:

def download(self, context, image_id, data=None):
"""
通过直接下载或者glance客户端下载方式,下载镜像数据到文件data;

这个方法的大致执行步骤如下:

判断由配置参数定义的直接下载的URL地址列表中是否有值;
如果有,说明可以下载镜像数据;
获取glance的镜像中定义的'direct_url'属性值location,它表示了后端存储镜像数据的位置;
然后从location中解析出来要下载数据的存储的文件路径;
根据解析的文件路径,把镜像数据拷贝下载出来;

如果没有,说明不可以直接下载镜像数据:
则建立glance客户端,尝试连接glance;
连接成功以后获取镜像数据;
"""

# allowed_direct_url_schemes:这个参数定义了可以通过direct_url直接下载的URL地址列表;
# 参数的默认值为空;
if 'file' in CONF.allowed_direct_url_schemes:
# 获取glance的镜像中定义的'direct_url'属性值,它表示了后端存储的位置;
location = self.get_location(context, image_id)

# 把URL解析成为六个部分:
# <scheme>://<netloc>/<path>;<params>?<query>#<fragment>
o = urlparse.urlparse(location)

# o.scheme == "file":这表示是本地文件传输协议,用于访问本地计算机中的文件;
# 以读的方式打开从URL解析出来path部分;
# 把数据从类文件对象f中拷贝出来写入类文件对象data,以length=16*1024的格式;
if o.scheme == "file":
with open(o.path, "r") as f:
# copyfileobj:把数据从类文件对象f中拷贝出来写入类文件对象data,以length=16*1024的格式;
# f:以读的方式打开从URL解析出来path部分,也是要拷贝数据的类文件对象;
# data:要写入数据的类文件对象;
shutil.copyfileobj(f, data)
return

try:
# _client:
# 实现了首先根据client值判断是否已经建立了glance客户端对象;
# 如果没有建立,则会调用GlanceClientWrapper类的初始化方法建立一个glance客户端;

# call:
# 调用一个glance客户端的方法;
# 尝试一定次数连接glance,如果连接成功,这时会获取glance客户端对象,并返回client.images.data;
image_chunks = self._client.call(context, 1, 'data', image_id)
except Exception:
_reraise_translated_image_exception(image_id)

if data is None:
return image_chunks
else:
for chunk in image_chunks:
data.write(chunk)
实际上,方法download由两部分组成,即两种获取镜像数据的实现方式,一种是通过url直接下载镜像数据,另一种是通过glance客户端从glance上下载镜像数据。

首先来看语句if 'file' in CONF.allowed_direct_url_schemes

配置参数CONF.allowed_direct_url_schemes定义了可以通过direct_url直接下载的URL地址列表。这里就是判断这个列表中是否有值存在。如果有,说明可以通过URL来直接下载镜像数据。后面的几条语句具体实现了如果通过具体的URL来下载镜像数据,具体解析可以直接看代码注释。

但是系统默认CONF.allowed_direct_url_schemes的值为空,也就是说,系统默认不通过URL直接下载镜像数据这种方式。所以要通过glance客户端,从glance上来获取镜像文件。

具体是通过下面的语句来实现这个过程的:

image_chunks = self._client.call(context, 1, 'data', image_id)

这条语句实现了首先根据client值判断是否已经建立了glance客户端对象,如果没有建立,则会调用GlanceClientWrapper类的初始化方法建立一个glance客户端。

然后调用方法call连接一个glance客户端,尝试一定次数连接glance,如果连接成功,这时会获取glance客户端对象,并返回client.images.data,赋值给image_chunks,从而实现镜像数据的获取。

具体实现来看方法call的代码:

def call(self, context, version, method, *args, **kwargs):
"""
调用一个glance客户端的对象,调用其中的get方法,获取image镜像;
尝试一定次数连接glance,如果连接成功,这时会获取glance客户端对象,并返回client.images.get;
如果到达最大尝试连接次数都没有连接成功,则就会引发异常的;

# 调用之一传进来的参数:
# context:上下文信息;
# 1:传到下一个方法中,作为version参数;
# 'get':作为方法参数,传到下一个函数;
# image_id:获取的image镜像ID;
"""

# 各种异常;
retry_excs = (glanceclient.exc.ServiceUnavailable,
glanceclient.exc.InvalidEndpoint,
glanceclient.exc.CommunicationError)

# glance_num_retries:这个参数定义了当从glance下载image镜像时,重试的次数;
# 默认为0应该是表示会下载无数次;
# 加1是为了排除无数次下载尝试的可能,而定义为只有1次重新下载的机会;
# 计算从glance下载image镜像重试机会的次数;
num_attempts = 1 + CONF.glance_num_retries

# 从1循环到num_attempts;
for attempt in xrange(1, num_attempts + 1):

# 得到glance客户端对象,如果没有定义,则新建立一个客户端对象;
# _create_onetime_client:建立并返回一个正确的glanceclient.Client客户端,它会被用于一次call;

# context:上下文信息;
# version:版本;
client = self.client or self._create_onetime_client(context, version)

# 这里传进来的一个method是get,所以返回的是client.images.get;
# getattr(client.images, method)(*args, **kwargs)调用的就是客户端对象类中的get方法,获取image镜像;
try:
return getattr(client.images, method)(*args, **kwargs)
except retry_excs as e:
host = self.host
port = self.port
extra = "retrying"
error_msg = _("Error contacting glance server "
"'%(host)s:%(port)s' for '%(method)s', %(extra)s.")

# 如果达到了最大的尝试下载次数,则会引发异常,提示glance连接失败;
if attempt == num_attempts:
extra = 'done trying'
LOG.exception(error_msg, locals())
raise exception.GlanceConnectionFailed(
host=host, port=port, reason=str(e))
LOG.exception(error_msg, locals())
time.sleep(1)
这个方法中最重要的语句就是:

getattr(client.images, method)(*args, **kwargs)

这里传进来的method是get,所以执行的就是方法client.images.get,来获取image镜像数据。

其他的代码解析可以直接参考代码注释就可以了。

至此,镜像数据的获取过程解析完成。

下面,我们回到方法cache中,分析最后一条语句:

(3)if size and self.preallocate and self._can_fallocate():
            utils.execute('fallocate', '-n', '-l', size, self.path)

这里实现的是根据情况调用fallocate命令,为获取的镜像数据预分配大小为size的磁盘空间。但是在类Image初始化的过程中,preallocate被赋值为False,也就是说系统默认是不为获取的镜像数据预分配磁盘空间的。

至此,以“kernel”为例,调用方法cache获取指定格式磁盘镜像文件的实现过程全部分析完成。

我们回到最开始的方法_create_image,可以看到,后面根据具体情况还有“ramdisk”、“disk”、“disk.local”、“disk.swap”等类型的镜像文件的获取过程的实现,其实具体的方法和“kernel”镜像数据的获取方法基本类似,这里不再进行详细的分析。

本篇文章的最开始我们说过,方法_create_image主要完成了三部分的工作,第一是不同类型镜像文件的获取实现,第二是驱动配置的实现,第三是文件注入的实现。这里已经完成了第一部分代码的解析工作,由于篇幅的原因,我将会在下一篇博文中继续解析方法_create_image第二部分和第三部分代码实现的解析工作,详细请见 OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机启动源码实现(3)