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

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


接续上篇博文,我们继续来解析方法_create_image中的第二部分和第三部分,即驱动配置和文件注入部分的代码。

2.驱动配置部分代码解析

先来看方法_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 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)

首先经语句if configdrive.required_by(instance)判断,如果系统规定总是创建config drive,则进行后面的驱动配置操作。

接着调用语句inst_md = instance_metadata.InstanceMetadata(instance, content=files, extra_md=extra_md)实现类InstanceMetadata的初始化,获取虚拟机实例元数据操作类的实例化对象;

先来看看类InstanceMetadata的初始化方法:

class InstanceMetadata():
    """
    虚拟机实例元数据;
    """

    def __init__(self, instance, address=None, content=[], extra_md=None,
                 conductor_api=None):
        # 实例信息;
        self.instance = instance
        # 密码信息;
        self.extra_md = extra_md

        # 根据配置参数“use_local”具体的选择初始化类conductor_api.LocalAPI或者是conductor_api.API;
        # 获取并返回类的实例对象;
        # LocalAPI类:conductor API 的本地版本,这个类处理了本地数据库的更新等操作,而不是通过RPC;
        # API类:这个类通过RPC处理了数据库的更新等操作;
        # 这里简单解释一下nova conductor服务,在Grizzly版的Nova中,nova-conductor是在nova-compute之上的新的服务层;
        # 它使得nova-compute不再直接访问数据库;
        if conductor_api:
            self.conductor_api = conductor_api
        else:
            self.conductor_api = conductor.API()

        # 获取admin的上下文信息;
        ctxt = context.get_admin_context()

        # 类conductor_api.LocalAPI或conductor_api.API的实例化对象;
        capi = self.conductor_api
        # 根据host获取可用的域(zone);
        self.availability_zone = ec2utils.get_availability_zone_by_host(instance['host'], capi)
        # 为实例返回包含IP信息的字典;
        self.ip_info = ec2utils.get_ip_info_for_instance(ctxt, instance)
        # 获取实例的安全组信息;
        self.security_groups = capi.security_group_get_by_instance(ctxt, instance)
        # 通过实例instance获取所有的块设备映射;
        self.mappings = _format_instance_mapping(capi, ctxt, instance)

        if instance.get('user_data', None) is not None:
            self.userdata_raw = base64.b64decode(instance['user_data'])
        else:
            self.userdata_raw = None

        self.ec2_ids = capi.get_ec2_ids(ctxt, instance)

        self.address = address

        # expose instance metadata.
        self.launch_metadata = {}
        for item in instance.get('metadata', []):
            self.launch_metadata[item['key']] = item['value']

        self.password = password.extract_password(instance)

        self.uuid = instance.get('uuid')

        self.content = {}
        self.files = []

        # get network info, and the rendered network template
        # 获取admin的上下文信息;
        ctxt = context.get_admin_context()
        # 返回实例相关的所有网络信息;
        # 并根据instance中新的实例信息更新表示一个实例信息缓存记录的数据表;
        network_info = network.API().get_instance_nw_info(ctxt, instance, conductor_api=capi)

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

        if cfg:
            key = "%04i" % len(self.content)
            self.content[key] = cfg
            self.network_config = {"name": "network_config",
                'content_path': "/%s/%s" % (CONTENT_DIR, key)}

        for (path, contents) in content:
            key = "%04i" % len(self.content)
            self.files.append({'path': path,
                'content_path': "/%s/%s" % (CONTENT_DIR, key)})
            self.content[key] = contents
类的初始化方法完成了一些变量和参数的初始化过程,我们对其中比较重要的一些语句进行解析。

首先来看下面的语句:

if conductor_api:
    self.conductor_api = conductor_api
else:
    self.conductor_api = conductor.API()

由传入的参数知道conductor_api的值为none,所以直接执行self.conductor_api = conductor.API()。这条语句是对类API进行了初始化,并获取了实例化对象。具体来看代码:

def API(*args, **kwargs):
    """
    根据配置参数“use_local”具体的选择初始化类conductor_api.LocalAPI或者是conductor_api.API;
    获取并返回类的实例对象;
    LocalAPI类:conductor API 的本地版本,这个类处理了本地数据库的更新等操作,而不是通过RPC;
    API类:这个类通过RPC处理了数据库的更新等操作;
    这里简单解释一下nova conductor服务,在Grizzly版的Nova中,nova-conductor是在nova-compute之上的新的服务层;
    它使得nova-compute不再直接访问数据库;
    """
    use_local = kwargs.pop('use_local', False)
    if oslo.config.cfg.CONF.conductor.use_local or use_local:
        api = conductor_api.LocalAPI
    else:
        api = conductor_api.API
    return api(*args, **kwargs)
就像注释中写的那样,这里根据配置参数“use_local”具体的选择初始化类conductor_api.LocalAPI或者是conductor_api.API,获取并返回类的实例对象。

其中,LocalAPI类是conductor API 的本地版本,这个类处理了本地数据库的更新等操作,而不是通过RPC;而API类则通过RPC处理了数据库的更新等操作。

这里需要简单解释一下nova conductor服务,在Grizzly版的Nova中,nova-conductor是在nova-compute之上的新的服务层,它使得nova-compute不再直接访问数据库。

我们回到类InstanceMetadata的初始化方法中,我们可以看到很多变量和参数的初始化赋值过程中,尤其是涉及到数据库查询的变量赋值中,都是通过nova-conductor这个服务层进行实现的,这个服务层实际上增强了代码的可扩展性和安全性。

我们再回到方法_create_image中,来看语句configdrive.ConfigDriveBuilder(instance_md=inst_md),这条鱼据实现的是对ConfigDriveBuilder这个构建配置驱动器的类进行实例的初始化,并获取类的初始化实例对象。具体来看这个类的初始化方法的代码:

class ConfigDriveBuilder(object):
    """
    构建配置驱动器;
    """

    def __init__(self, instance_md=None):
        """
        建立存储配置驱动临时文件的目录文件;
        根据instance_md获取实例元数据,并根据版本version更新完善不同版本的EC2类型的实例元数据;
        并把实例元数据写入到配置驱动的临时存储文件;
        """
        self.imagefile = None
        
        # 建立配置驱动临时文件所放置的目录文件;
        # config_drive_tempdir:这个参数定义了要建立的配置驱动临时文件所放置的目录;
        # 参数的默认值为tempfile.tempdir,这里定义为NONE;
        self.tempdir = tempfile.mkdtemp(dir=CONF.config_drive_tempdir,
                                        prefix='cd_gen_')

        # 把instance_md中的元数据写入到目录文件tempdir中;
        
        # 根据instance_md获取实例元数据,并根据版本version更新完善不同版本的EC2类型的实例元数据;
        # 并把实例元数据写入到配置驱动的临时存储文件;
        if instance_md is not None:
            self.add_instance_metadata(instance_md)
这个初始化过程中,最重要的语句就是self.add_instance_metadata(instance_md),它通过调用方法add_instance_metadata来实现把instance_md中的元数据写入到驱动配置的临时目录文件tempdir中。我们进一步来看方法add_instance_metadata的实现:

def add_instance_metadata(self, instance_md):
        """
        获取实例元数据,并根据版本version更新不同版本的EC2类型的实例元数据;
        并把实例元数据写入到配置驱动的临时存储文件;
        """
        # metadata_for_config_drive:获取实例元数据,并根据版本version更新不同版本的EC2类型的实例元数据;
        # 不同版本的元数据都转换为json格式,并对应到相应的路径,以字典的形式(path,value)返回;
        
        # 把data(实例元数据)写入到配置驱动的临时存储文件;
        for (path, value) in instance_md.metadata_for_config_drive():
            self._add_file(path, value)
            LOG.debug(_('Added %(filepath)s to config drive'),
                      {'filepath': path})
再来看方法metadata_for_config_drive:

def metadata_for_config_drive(self):
        """
        Yields (path, value) tuples for metadata elements.
        获取实例元数据,并根据版本version更新不同版本的EC2类型的实例元数据;
        不同版本的元数据都转换为json格式,并对应到相应的路径,以字典的形式(path,value)返回;
        """
        
        # EC2 style metadata
        # EC2类型的实例元数据;
        # 从第一个版本开始,遍历所有的时间版本,循环更新实例的元数据(其中有定义跳过的版本);
        # 每一个版本的实例元数据都转化成json格式,并对应到相应的路径;
        for version in VERSIONS + ["latest"]:
            if version in CONF.config_drive_skip_versions.split(' '):
                continue

            # 获取实例的元数据,并根据version来完善实例的元数据;
            data = self.get_ec2_metadata(version)
            if 'user-data' in data:
                filepath = os.path.join('ec2', version, 'user-data')
                yield (filepath, data['user-data'])
                del data['user-data']

            try:
                del data['public-keys']['0']['_name']
            except KeyError:
                pass

            filepath = os.path.join('ec2', version, 'meta-data.json')
            yield (filepath, json.dumps(data['meta-data']))

        for version in OPENSTACK_VERSIONS + ["latest"]:
            path = 'openstack/%s/%s' % (version, MD_JSON_NAME)
            yield (path, self.lookup(path))

            path = 'openstack/%s/%s' % (version, UD_NAME)
            if self.userdata_raw is not None:
                yield (path, self.lookup(path))

        for (cid, content) in self.content.iteritems():
            yield ('%s/%s/%s' % ("openstack", CONTENT_DIR, cid), content)

以上的两个方法add_instance_metadata和metadata_for_config_drive最终实现了获取实例元数据,并根据版本version更新不同版本的EC2类型的实例元数据,并把实例元数据写入到配置驱动的临时存储文件当中。这里不再对这两个方法的具体实现进行代码解析,可以直接看我的代码注释即可。

至此,语句configdrive.ConfigDriveBuilder(instance_md=inst_md)解析完成。

我们再回到方法_create_image之中,继续看驱动配置的执行代码。之后最重要的一条语句就是cdb.make_drive(configdrive_path)。这条语句实现了根据配置参数选择,生成ISO格式或vfat格式的镜像文件,默认是ISO格式的。

我们来具体看方法make_drive的代码:

def make_drive(self, path):
        """
        path:生成镜像文件以后放置的路径;
        根据配置参数选择,生成ISO格式或vfat格式的镜像文件,默认是ISO格式的;
        """
        
        # 这个参数定义了配置驱动的格式,iso9660或者是vfat;
        # 参数的默认值为iso9660;
        # _make_iso9660:生成ISO格式的镜像文件;
        if CONF.config_drive_format == 'iso9660':
            self._make_iso9660(path)
        elif CONF.config_drive_format == 'vfat':
            self._make_vfat(path)
        else:
            raise exception.ConfigDriveUnknownFormat(
                format=CONF.config_drive_format)
配置参数config_drive_format定义了配置驱动的格式,iso9660或者是vfat。由于参数的默认值为iso9660,所以这里默认调用方法_make_iso9660来生成ISO格式的镜像文件。进一步来看方法_make_iso9660的代码:

def _make_iso9660(self, path):
        """
        生成ISO格式的镜像文件;
        """
        publisher = "%(product)s %(version)s" % {
            'product': version.product_string(),
            'version': version.version_string_with_package()
            }

        utils.execute(CONF.mkisofs_cmd,
                      '-o', path,
                      '-ldots',
                      '-allow-lowercase',
                      '-allow-multidot',
                      '-l',
                      '-publisher',
                      publisher,
                      '-quiet',
                      '-J',
                      '-r',
                      '-V', 'config-2',
                      self.tempdir,
                      attempts=1,
                      run_as_root=False)

可见这里通过配置参数CONF.mkisofs_cmd调用命令genisoimage,来实现ISO格式的镜像文件的建立。

至此,方法_create_image中的第二部分,驱动配置的实现解析完成。(但是理解上还不到位)

3.文件注入部分代码解析

我们先来看方法_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;
        """

        ......

        # 文件注入;
        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)

实际上,文件注入部分实现的就是把key(ssh公钥),net(渲染好的网络模板),metadata(虚拟机实例的元数据),admin_pass(用户密码)和files(传进来的已编码的文件)等注入到建立好的磁盘镜像之中。而实现这个过程的最重要的语句就是:

disk.inject_data(injection_path,
                 key, net, metadata, admin_pass, files,
                 partition=target_partition,
                 use_cow=CONF.use_cow_images,
                 mandatory=('files',))

我们来进一步看看方法inject_data的具体代码实现:

def inject_data(image, key=None, net=None, metadata=None, admin_password=None,
                files=None, partition=None, use_cow=False, mandatory=()):
    """
    注入指定的项目到磁盘镜像image;
    
    # image:要注入磁盘镜像的存储路径;
    # key, net, metadata, admin_pass, files:要注入的项目;
    # partition:要注入磁盘的分区号;
    # use_cow:这个参数定义了是否使用cow格式的镜像文件,默认值为True;
    """
    LOG.debug(_("Inject data image=%(image)s key=%(key)s net=%(net)s "
                "metadata=%(metadata)s admin_password=ha-ha-not-telling-you "
                "files=%(files)s partition=%(partition)s use_cow=%(use_cow)s")
              % locals())
    fmt = "raw"
    if use_cow:
        fmt = "qcow2"
    try:
        fs = vfs.VFS.instance_for_image(image, fmt, partition)
        fs.setup()
    except Exception as e:
        for inject in mandatory:
            inject_val = locals()[inject]
            if inject_val:
                raise
        LOG.warn(_('Ignoring error injecting data into image '
                   '(%(e)s)') % locals())
        return False

    try:
        return inject_data_into_fs(fs, key, net, metadata, admin_password, files, mandatory)
    finally:
        fs.teardown()

在这个方法中首先调用方法instance_for_image实现为挂载建立好的镜像准备磁盘,并调用方法setup实现磁盘的挂载。

再调用方法inject_data_into_fs实现相关文件的文件注入。我们来看方法inject_data_into_fs的代码:

def inject_data_into_fs(fs, key, net, metadata, admin_password, files, mandatory=()):
    """
    注入指定数据到已经挂载的文件系统;
    """
    status = True
    for inject in ('key', 'net', 'metadata', 'admin_password', 'files'):
        inject_val = locals()[inject]
        inject_func = globals()['_inject_%s_into_fs' % inject]
        if inject_val:
            try:
                inject_func(inject_val, fs)
            except Exception as e:
                if inject in mandatory:
                    raise
                LOG.warn(_('Ignoring error injecting %(inject)s into image '
                           '(%(e)s)') % locals())
                status = False
    return status
在这个方法中主要是通过不同字符串的匹配,来调用不同的方法,从而实现向磁盘镜像注入不同的文件信息。

例如这个方法中可以具体实现调用方法_inject_key_into_fs、_inject_net_into_fs、_inject_metadata_into_fs、_inject_admin_password_into_fs和_inject_files_into_fs,具体实现key、net、metadata、admin_password和files等文件的注入过程。

我们以方法_inject_net_into_fs为例,来解析向磁盘镜像注入文件的实现过程。来看方法_inject_net_into_fs的代码实现:

def _inject_net_into_fs(net, fs):
    """
    Inject /etc/network/interfaces into the filesystem rooted at fs.
    net is the contents of /etc/network/interfaces.
    注入文件/etc/network/interfaces(即net)到文件系统fs的根目录;
    """

    LOG.debug(_("Inject key fs=%(fs)s net=%(net)s") %
              locals())
    netdir = os.path.join('etc', 'network')
    fs.make_path(netdir)
    fs.set_ownership(netdir, "root", "root")
    fs.set_permissions(netdir, 0744)

    netfile = os.path.join('etc', 'network', 'interfaces')
    # 注入指定的文件到指定的文件系统;
    _inject_file_into_fs(fs, netfile, net)
这个方法实现了注入文件/etc/network/interfaces(即net)到文件系统fs的根目录,在这个方法中,具体调用了方法_inject_file_into_fs来实现了向磁盘镜像注入指定的文件信息。

再来看方法_inject_file_into_fs的代码实现:

def _inject_file_into_fs(fs, path, contents, append=False):
    """
    注入指定的文件到指定的文件系统;
    """
    LOG.debug(_("Inject file fs=%(fs)s path=%(path)s append=%(append)s") %
              locals())
    # 追加contents到path指定文件的结尾;
    if append:
        fs.append_file(path, contents)
    # 用contents替换path指定文件的内容;
    else:
        fs.replace_file(path, contents)
这个方法的实现比较好理解,直接看我的代码注释即可。

至此,方法_create_image中的第三部分,即文件注入已完全解析完成。

从而,方法_create_image也已经解析完成。

在下篇博文 OpenStack基于Libvirt的虚拟化平台调度实现----Nova虚拟机启动源码实现(4)当中,我们将回到方法spawn中,继续对Nova虚拟机启动源码实现进行解析

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值