OpenStack建立实例完整过程源码详细分析(10)

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


我们接续博文 OpenStack建立实例完整过程源码详细分析(9),来继续分析OpenStack建立实例完整过程源码实现。

在上一篇博文中,我们具体分析了磁盘配额管理相关的方法实现,完成了对方法/nova/compute/api.py----_check_num_instances_quota的源码解析。这里我们回到方法/nova/compute/api.py----_validate_and_provision_instance中。来继续对这个方法的实现进行解析。

回顾一下,这个方法主要完成的是对建立实例所调用的方法_create_instance中输入的各个参数进行检查验证,其中完成了两项非常重要的任务,即资源的配额管理和为新建立实例在数据库中新建数据条目等准备工作。

首先,回顾方法_validate_and_provision_instance的源码:

    def _validate_and_provision_instance(self, context, instance_type,
                                         image_href, kernel_id, ramdisk_id,
                                         min_count, max_count,
                                         display_name, display_description,
                                         key_name, key_data, security_groups,
                                         availability_zone, user_data,
                                         metadata, injected_files,
                                         access_ip_v4, access_ip_v6,
                                         requested_networks, config_drive,
                                         block_device_mapping,
                                         auto_disk_config, reservation_id,
                                         scheduler_hints):
        """
        验证所有的输入参数;
        返回要建立实例的各类信息;
        这个方法中做了很多事,稍后会好好总结;"""

        if not metadata:
            metadata = {}
        if not security_groups:
            security_groups = ['default']
        if not instance_type:
            instance_type = instance_types.get_default_instance_type()
        if not min_count:
            min_count = 1
        if not max_count:
            max_count = min_count
        block_device_mapping = block_device_mapping or []
        
        if min_count > 1 or max_count > 1:
            if any(map(lambda bdm: 'volume_id' in bdm, block_device_mapping)):
                msg = _('Cannot attach one or more volumes to multiple' ' instances')
                raise exception.InvalidRequest(msg)
        if instance_type['disabled']:
            raise exception.InstanceTypeNotFound(instance_type_id=instance_type['id'])

        if user_data:
            l = len(user_data)
            if l > MAX_USERDATA_SIZE:
                raise exception.InstanceUserDataTooLarge(
                    length=l, maxsize=MAX_USERDATA_SIZE)
            try:
                base64.decodestring(user_data)
            except base64.binascii.Error:
                raise exception.InstanceUserDataMalformed()

        # 根据配额资源限制计算所要建立实例的数目,并获取了分配好的资源(块存储)的UUID的列表;
        num_instances, quota_reservations = self._check_num_instances_quota(context, instance_type, min_count, max_count)
        """
        _check_num_instances_quota:根据磁盘配额资源限制确定所要建立实例的数目;
        num_instances:返回值max_count表示建立实例的最大数目;
        quota_reservations: 返回值reservations表示建立的预定(分配)的资源的UUID的列表;
        (???)已经分配了磁盘资源;
        """

        # 尝试建立实例;
        try:
            instances = []
            instance_uuids = []

            # 对磁盘配额的元数据属性、磁盘配额的注入文件以及安全组和网络进行检测;
            self._check_metadata_properties_quota(context, metadata)
            """
            检测metadata参数;                   
            强制对磁盘配额的元数据属性的限制进行检测;
            进行了简单的磁盘配额限制的检测,并构造了一个符合要求的资源列表;
            """
            self._check_injected_file_quota(context, injected_files)
            """
            检测injected_files参数;
            强制对磁盘配额注入文件的限制进行检测,如果超出任何限制会引发异常;
            具体限制包括注入文件数目、注入文件路径长度和注入文件内容长度三项指标;
            """
            self._check_requested_secgroups(context, security_groups)
            """
            检测security_groups参数;                    
            检测所要求的安全组是否存在,并且属于指定的对象,如果不存在则会引发异常;
            """
            self._check_requested_networks(context, requested_networks)
            """
            检测requested_networks参数;                    
            检测所需求的网络是否属于指定的对象(工程),
            并且为每个网络提供的固定的IP地址是否处在同一个网段中;
            """

            # 检测image_href参数;
            # 如果image_href值为真,则创建image_service并从给定的image_href解析它的ID值,赋给image_id;
            # 并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
            # 获取image_service的状态,以此判断image_service是否创建成功;
            if image_href:
                (image_service, image_id) = glance.get_remote_image_service(context, image_href)
                image = image_service.show(context, image_id)
                if image['status'] != 'active':
                    raise exception.ImageNotActive(image_id=image_id)
            else:
                image = {}

            # 检测instance_type['memory_mb']和instance_type['root_gb']参数;
            if instance_type['memory_mb'] < int(image.get('min_ram') or 0):
                raise exception.InstanceTypeMemoryTooSmall()
            if instance_type['root_gb'] < int(image.get('min_disk') or 0):
                raise exception.InstanceTypeDiskTooSmall()

            # 为实例获取合适的内核和ramdisk的值;
            kernel_id, ramdisk_id = self._handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image)
            """
            为实例选择合适的内核和ramdisk,返回获取到的内核和ramdisk的值kernel_id和ramdisk_id;
            内核和ramdisk可以选择以下三种中的一种:
            1.通过建立实例的要求来决定;
            2.从image(镜像)来继承;
            3.通过应用  ‘null_kernel’标志强制不使用;
            注:如果是第三种情况,则还建立了image_service服务。而且建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象。
            """

            # 处理config_drive这个参数;
            config_drive_id = None
            if config_drive and not utils.is_valid_boolstr(config_drive):
                config_drive_id = config_drive
                config_drive = None

                # 检测config_drive参数,确保config_drive镜像是存在的;
                # 如果config_drive值是正确无误的,则创建cd_image_service并从给定的config_drive_id解析它的ID值,赋给config_drive_id;
                # 并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
                # 获取cd_image_service的状态,以此判断cd_image_service是否创建成功;
                cd_image_service, config_drive_id = glance.get_remote_image_service(context, config_drive_id)
                cd_image_service.show(context, config_drive_id)

            # 处理key_data这个参数;
            if key_data is None and key_name:
                key_pair = self.db.key_pair_get(context, context.user_id,key_name)
                key_data = key_pair['public_key']

            # 从镜像元数据中获取root_device_name值,如果这个值没有被指定,则返回None值;
            root_device_name = block_device.properties_root_device_name(image.get('properties', {}))

            # 从参数availability_zone中分离出availability_zone值和forced_host值;
            availability_zone, forced_host = self._handle_availability_zone(availability_zone)

            # 从instance_type这个参数中保存instance type属性到实例的system_metadata数据中,格式如下所示:
            # [prefix]instance_type_[key]
            # 保存instance type信息到system_metadata当中,按照一定的格式;
            system_metadata = instance_types.save_instance_type_info(dict(), instance_type)

            # 保存所有参数选项的字典;
            base_options = {
                'reservation_id': reservation_id,
                'image_ref': image_href,
                'kernel_id': kernel_id or '',
                'ramdisk_id': ramdisk_id or '',
                'power_state': power_state.NOSTATE,
                'vm_state': vm_states.BUILDING,
                'config_drive_id': config_drive_id or '',
                'config_drive': config_drive or '',
                'user_id': context.user_id,
                'project_id': context.project_id,
                'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),
                'instance_type_id': instance_type['id'],
                'memory_mb': instance_type['memory_mb'],
                'vcpus': instance_type['vcpus'],
                'root_gb': instance_type['root_gb'],
                'ephemeral_gb': instance_type['ephemeral_gb'],
                'display_name': display_name,
                'display_description': display_description or '',
                'user_data': user_data,
                'key_name': key_name,
                'key_data': key_data,
                'locked': False,
                'metadata': metadata,
                'access_ip_v4': access_ip_v4,
                'access_ip_v6': access_ip_v6,
                'availability_zone': availability_zone,
                'root_device_name': root_device_name,
                'progress': 0,
                'system_metadata': system_metadata}

            # 从image镜像中继承相关属性信息;
            # 包括os_type、architecture、vm_mode和auto_disk_config;
            options_from_image = self._inherit_properties_from_image(image, auto_disk_config)

            # 相关属性更新到字典base_options当中;
            base_options.update(options_from_image)

            # num_instances表示要建立实例的最大数目;
            LOG.debug(_("Going to run %s instances...") % num_instances)

            # 从调度提示信息scheduler_hints中获取过滤器信息;
            filter_properties = dict(scheduler_hints=scheduler_hints)
            
            # 获取filter_properties['force_hosts']的值;(注:这里forced_host是怎么来的还需要进一步研究明白;)
            if forced_host:
                check_policy(context, 'create:forced_host', {})
                filter_properties['force_hosts'] = [forced_host]

            # num_instances表示要建立实例的最大数目;
            # xrange(num_instances)表示从0到num_instances-1的序列;
            for i in xrange(num_instances):
                # 拷贝base_options对象;
                options = base_options.copy()              
                # 为每一个新的实例在数据库中建立新的条目,包括任何更新的表(如安全组等等);
                instance = self.create_db_entry_for_new_instance(context, instance_type, image, options, security_groups, block_device_mapping, num_instances, i)

                # 实例instance添加到instances中;
                instances.append(instance)
                # instance['uuid']添加到instance_uuids中;
                instance_uuids.append(instance['uuid'])
                
                # 验证一个实例instance中的所有的块设备映射;
                self._validate_bdm(context, instance)
                # send a state update notification for the initial create to
                # show it going from non-existent to BUILDING
                # 为实例的初始化建立发送一个状态更新通知,表示实例状态从不存在到建立;
                # send_update_with_states:在一个实例中,发送compute.instance.update来通知实例的任何的改变信息;
                notifications.send_update_with_states(context, instance, None, vm_states.BUILDING, None, None, service="api")

        # 在引发异常的情况下
        except Exception:
            # 先保存当前的异常Exception,运行一些代码以后,再引发这个异常;
            # 这个挺有意思的;
            with excutils.save_and_reraise_exception():
                try:
                    # 循环取出每个实例的UUID值;
                    # 根据每个实例的UUID值,删除具体的实例;
                    # instance_destroy:销毁当前要建立的实例,如果实例不存在,则引发异常;
                    # 返回删除后的实例对象;
                    for instance_uuid in instance_uuids:
                        self.db.instance_destroy(context, instance_uuid)
                finally:
                    # 配额quota_reservations预约信息的删除;
                    QUOTAS.rollback(context, quota_reservations)

        # 提交设定好的reservations预约信息;
        QUOTAS.commit(context, quota_reservations)

        # 生成包含请求建立实例的信息的字典;
        request_spec = {
            'image': jsonutils.to_primitive(image), # 转换镜像image对象到primitives格式;
            'instance_properties': base_options, # 实例属性;
            'instance_type': instance_type, # 实例类型;
            'instance_uuids': instance_uuids, # 实例UUID;
            'block_device_mapping': block_device_mapping, # 块设备映射信息;
            'security_group': security_groups, # 安全组信息;
        }

        # 返回要建立实例的各类信息;
        return (instances, request_spec, filter_properties)
c.try......except......

在这个try......except......结构中,总体上来讲主要进行了两大部分的工作,第一,继续对传入这个方法的参数进行检测,第二,为每一个新的实例在数据库中建立新的条目。

我们先来看对参数的继续检测:

c.1

# 对磁盘配额的元数据属性、磁盘配额的注入文件以及安全组和网络进行检测;
            
# 简单的磁盘配额限制检测,系统对资源配额数据有一定的限制,会据此来检测磁盘资源配额是否符合要求;
# 而且对metadata中的数据进行检测,确保metadata中数据的长度在0-255之间;
self._check_metadata_properties_quota(context, metadata)
            
# 检测injected_files参数;
# 强制对磁盘配额注入文件的限制进行检测,如果超出任何限制会引发异常;
# 具体限制包括注入文件数目、注入文件路径长度和注入文件内容长度三项指标;
self._check_injected_file_quota(context, injected_files)
            
# 检测security_groups参数;                    
# 检测所要求的安全组是否存在,并且属于指定的对象,如果不存在则会引发异常;
self._check_requested_secgroups(context, security_groups)
            
# 检测requested_networks参数;                    
# 检测所需求的网络是否属于指定的对象(工程),并且为每个网络提供的固定的IP地址是否处在同一个网段中;
self._check_requested_networks(context, requested_networks)

这部分代码分别实现了对磁盘配额的limit属性、注入文件、安全组以及网络参数进行了检测。我们先来看语句:self._check_metadata_properties_quota(context, metadata),这条语句实现了对磁盘配额limit属性的检测以及对metadata中的数据规格进行检测。具体来看方法_check_metadata_properties_quota:

def _check_metadata_properties_quota(self, context, metadata=None):
        """     
        简单的磁盘配额限制检测,资源配额数据将会被系统需求所限制,会据此来检测磁盘资源配额是否符合要求;
        而且对metadata中的数据进行检测,确保metadata中数据的长度在0-255之间;
        """
        if not metadata:
            metadata = {}
        num_metadata = len(metadata) # metadata的数目;
        
        # limit_check:简单的磁盘配额限制检测,并构造一个资源列表;
        # 这个资源列表将会被values.items所限制,会据此来检测磁盘资源配额是否符合要求;
        # 如果资源超出配额限制,则会引发异常;
        try:
            QUOTAS.limit_check(context, metadata_items=num_metadata)
        except exception.OverQuota as exc:
            pid = context.project_id
            LOG.warn(_("Quota exceeded for %(pid)s, tried to set "
                       "%(num_metadata)s metadata properties") % locals())
            quota_metadata = exc.kwargs['quotas']['metadata_items']
            raise exception.MetadataLimitExceeded(allowed=quota_metadata)

        # 确保metadata中数据的长度在0-255之间;
        for k, v in metadata.iteritems():
            if len(k) == 0:
                msg = _("Metadata property key blank")
                LOG.warn(msg)
                raise exception.InvalidMetadata(reason=msg)
            if len(k) > 255:
                msg = _("Metadata property key greater than 255 characters")
                LOG.warn(msg)
                raise exception.InvalidMetadataSize(reason=msg)
            if len(v) > 255:
                msg = _("Metadata property value greater than 255 characters")
                LOG.warn(msg)
                raise exception.InvalidMetadataSize(reason=msg)
我们可以看到,具体实现磁盘配额limit属性检测的是方法limit_check,我们来看其具体实现:

def limit_check(self, context, project_id=None, **values):
        """
        简单的磁盘配额限制检测,并构造一个资源列表;
        这个资源列表将会被values.items所限制,会据此来检测磁盘资源配额是否符合要求;
        如果资源超出配额限制,则会引发异常;
        """
        return self._driver.limit_check(context, self._resources, values,project_id=project_id)
def limit_check(self, context, resources, values, project_id=None):
        """
        简单的磁盘配额限制检测,并构造一个资源列表;
        这个资源列表将会被values.items所限制,会据此来检测磁盘资源配额是否符合要求;
        如果资源超出配额限制,则会引发异常;
        """

        # 确认没有值小于0;
        unders = [key for key, val in values.items() if val < 0]
        if unders:
            raise exception.InvalidQuotaValue(unders=sorted(unders))

        # 如果project_id值为None,则使用context中的project_id值;
        if project_id is None:
            project_id = context.project_id

        # _get_quotas:这是一个辅助的方法,能够通过keys值为特定的磁盘资源进行配额的检查;
        # 返回磁盘配额资源的limit信息;
        quotas = self._get_quotas(context, resources, values.keys(),has_sync=False, project_id=project_id)

        # 检查磁盘配额,并构造一个资源列表,这个资源列表将会被values.items所限制,以至于符合请求信息;
        overs = [key for key, val in values.items()
                 if quotas[key] >= 0 and quotas[key] < val]
        if overs:
            raise exception.OverQuota(overs=sorted(overs), quotas=quotas, usages={})
我们再来看方法_get_quotas,它实现了获取磁盘配额资源的limit信息:

def _get_quotas(self, context, resources, keys, has_sync, project_id=None):
        """
        这是一个辅助的方法,能够通过keys值为特定的磁盘资源进行配额的检查;
        返回磁盘配额资源的limit信息;
        """

        # 筛选资源;
        if has_sync:
            sync_filt = lambda x: hasattr(x, 'sync')#判断对象x是否包含sync的特性;
        else:
            sync_filt = lambda x: not hasattr(x, 'sync')#判断对象x是否不包含sync的特性;
        desired = set(keys)
        sub_resources = dict((k, v) for k, v in resources.items()
                             if k in desired and sync_filt(v))

        # 确保所有磁盘配额资源都是已知的,否则引发异常,提示某些磁盘配额资源是未知的;
        if len(keys) != len(sub_resources):
            unknown = desired - set(sub_resources.keys())
            raise exception.QuotaResourceUnknown(unknown=sorted(unknown))

        # get_project_quotas:根据给定的资源选项,对于给定的对象(project_id)进行磁盘配额检索;
        # 返回磁盘配额信息,包括资源的limit信息、in_use信息和reserved信息(后两个是根据参数usages的值确定是否要获取的);
        quotas = self.get_project_quotas(context, sub_resources,project_id,context.quota_class, usages=False)

        return dict((k, v['limit']) for k, v in quotas.items())
进一步来看方法get_project_quotas,它实现了根据给定的资源选项,对于给定的对象(project_id)进行磁盘配额检索,返回磁盘配额信息,包括资源的limit信息、in_use信息和reserved信息(后两个是根据参数usages的值确定是否要获取的)。来看实现代码:

def get_project_quotas(self, context, resources, project_id,
                           quota_class=None, defaults=True,
                           usages=True):
        """
        根据给定的资源选项,对于给定的对象(project_id)进行磁盘配额检索;
        返回磁盘配额信息,包括资源的limit信息、in_use信息和reserved信息(后两个是根据参数usages的值确定是否要获取的);
        """

        quotas = {}
        # 对于一个给定的项目,检索它的所有的磁盘配额;
        # 根据project_id查询数据库中相应项目的数据库信息,获取其中的hard_limit值,也就是获取规定的资源最大限额值;
        project_quotas = db.quota_get_all_by_project(context, project_id)
        
        if usages:
            # 检索一个给定资源的当前的所有相关的使用情况;
            # 根据project_id查询数据库,获取当前资源的使用信息和预留信息;
            project_usages = db.quota_usage_get_all_by_project(context,project_id)

        # 通过适当的类获取配额信息。如果project ID匹配context中的一个,那么使用context中的quota_class类,否则,使用所提供的quota_class类(如果有的话)。
        # 选取合适的类,为后面做准备;
        if project_id == context.project_id:
            quota_class = context.quota_class
        if quota_class:
            # 根据给定的配额类,检索所有相关的配额hard_limit信息;
            class_quotas = db.quota_class_get_all_by_name(context, quota_class)
        else:
            class_quotas = {}

        for resource in resources.values():
            if not defaults and resource.name not in project_quotas:
                continue

            # 获取资源的配额的limit信息;
            quotas[resource.name] = dict(
                limit=project_quotas.get(resource.name, class_quotas.get(resource.name, resource.default)),
                )
            
            if usages:
                usage = project_usages.get(resource.name, {})
                quotas[resource.name].update(
                    in_use=usage.get('in_use', 0),
                    reserved=usage.get('reserved', 0),
                    )

        return quotas
这个方法的结构是依据project_id获取磁盘配额的limit属性、依据project_id获取当前资源的使用信息和预留信息、依据quota_class获取磁盘配额的limit属性,最后综合前面得到的数据,获取资源配额的limit信息。

这里我们仅以方法quota_get_all_by_project为例,看看是如何实现查询数据库的:

def quota_get_all_by_project(context, project_id):
    """
    根据project_id查询数据库中相应项目的数据库信息,获取其中的hard_limit值,也就是获取规定的某一资源最大限额值;
    """
    nova.context.authorize_project_context(context, project_id)

    rows = model_query(context, models.Quota, read_deleted="no").\
                   filter_by(project_id=project_id).\
                   all()

    result = {'project_id': project_id}
    for row in rows:
        result[row.resource] = row.hard_limit

    return result
在获取了磁盘资源配额的limit属性之后,我们回到方法limit_check中,会看到如下的一段源码:

        overs = [key for key, val in values.items()
                 if quotas[key] >= 0 and quotas[key] < val]
        if overs:
            raise exception.OverQuota(overs=sorted(overs), quotas=quotas,
                                      usages={})
这段代码实现的是依据values.items来检测磁盘资源配额是否符合要求,如果资源超出配额限制,则会引发异常。

我们再回到方法_check_metadata_properties_quota中,可以看到这样的一部分代码:

        # 确保metadata中数据的长度在0-255之间;
        for k, v in metadata.iteritems():
            if len(k) == 0:
                msg = _("Metadata property key blank")
                LOG.warn(msg)
                raise exception.InvalidMetadata(reason=msg)
            if len(k) > 255:
                msg = _("Metadata property key greater than 255 characters")
                LOG.warn(msg)
                raise exception.InvalidMetadataSize(reason=msg)
            if len(v) > 255:
                msg = _("Metadata property value greater than 255 characters")
                LOG.warn(msg)
                raise exception.InvalidMetadataSize(reason=msg)
这部分代码实现的功能是对metadata数据规格的检测,确保metadata中数据的长度在0-255之间。

至此,方法_check_metadata_properties_quota解析完成,我们回到方法_validate_and_provision_instance,来看其中的语句:self._check_injected_file_quota(context, injected_files),这条语句完成的功能是检测injected_files参数,强制对磁盘配额注入文件的限制进行检测,如果超出任何限制会引发异常,具体限制包括注入文件数目、注入文件路径长度和注入文件内容长度三项指标。

再来看语句_check_requested_secgroups,其完成的功能是检测security_groups参数,检测所要求的安全组是否存在,并且属于指定的对象,如果不存在则会引发异常。

再来看语句_check_requested_networks,其完成的功能是检测requested_networks参数,检测所需求的网络是否属于指定的对象(工程),并且为每个网络提供的固定的IP地址是否处在同一个网段中。

c.2

            if image_href:
                (image_service, image_id) = glance.get_remote_image_service(context, image_href)
                image = image_service.show(context, image_id)
                if image['status'] != 'active':
                    raise exception.ImageNotActive(image_id=image_id)
            else:
                image = {}
这部分代码实现的功能是如果image_href值为真,则创建image_service并从给定的image_href解析它的ID值,赋给image_id。并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象。获取image_service的状态,以此判断image_service是否创建成功。

c.3

            if instance_type['memory_mb'] < int(image.get('min_ram') or 0):
                raise exception.InstanceTypeMemoryTooSmall()
            if instance_type['root_gb'] < int(image.get('min_disk') or 0):
                raise exception.InstanceTypeDiskTooSmall()
这部分代码实现的功能是检测instance_type['memory_mb']和instance_type['root_gb']参数。

c.4

kernel_id, ramdisk_id = self._handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image)

这条语句实现的功能是根据传入的相关参数为实例获取合适的内核和ramdisk的值,具体看方法_handle_kernel_and_ramdisk的实现源码:

def _handle_kernel_and_ramdisk(context, kernel_id, ramdisk_id, image):
        """           
        为实例选择合适的内核和ramdisk,返回获取到的内核和ramdisk的值kernel_id和ramdisk_id;
        内核和ramdisk可以选择以下三种中的一种:
        1.通过建立实例的要求来决定;
        2.从image(镜像)来继承;
        3.通过应用  ‘null_kernel’标志强制不使用;
        注:如果是第三种情况,则还建立了image_service服务。而且建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象。
        """

        # 如果没有指定内核和ramdisk的值,则从image(镜像)来继承;      
        # 首先获取image(镜像)的属性;
        # 再分别取内核和ramdisk的值赋给kernel_id和ramdisk_id;
        image_properties = image.get('properties', {})

        if kernel_id is None:
            kernel_id = image_properties.get('kernel_id')

        if ramdisk_id is None:
            ramdisk_id = image_properties.get('ramdisk_id')

        # 通过应用‘null_kernel’标志强制不使用;
        # 内核和ramdisk的值都设为None;
        if kernel_id == str(CONF.null_kernel):
            kernel_id = None
            ramdisk_id = None

        # 如果内核和ramdisk的值是存在的(建立实例需求中设定的);     
        # get_remote_image_service:建立一个image_service并且从给定的kernel_id中解析它的ID值赋给kernel_id;
        # 并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
        # 获取image_service的状态,以此判断image_service是否创建成功;
        if kernel_id is not None:
            image_service, kernel_id = glance.get_remote_image_service(context, kernel_id)
            image_service.show(context, kernel_id)

        # get_remote_image_service:建立一个image_service并且从给定的ramdisk_id中解析它的ID值赋给ramdisk_id;
        # 并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
        # 获取image_service的状态,以此判断image_service是否创建成功;
        if ramdisk_id is not None:
            image_service, ramdisk_id = glance.get_remote_image_service(context, ramdisk_id)
            image_service.show(context, ramdisk_id)

        return kernel_id, ramdisk_id
c.5 其他参数的验证和获取;

            # 处理config_drive这个参数;
            config_drive_id = None
            if config_drive and not utils.is_valid_boolstr(config_drive):
                # config_drive is volume id
                config_drive_id = config_drive
                config_drive = None

                # Ensure config_drive image exists
                # 检测config_drive参数,确保config_drive镜像是存在的;
                # 如果config_drive值是正确无误的,则创建cd_image_service并从给定的config_drive_id解析它的ID值,赋给config_drive_id;
                # 并建立一个glance的客户端,具体方法是应用相关参数实例化一个新的glanceclient.Client对象;
                # 获取cd_image_service的状态,以此判断cd_image_service是否创建成功;
                cd_image_service, config_drive_id = glance.get_remote_image_service(context, config_drive_id)
                cd_image_service.show(context, config_drive_id)

            # 处理key_data这个参数;
            if key_data is None and key_name:
                key_pair = self.db.key_pair_get(context, context.user_id,key_name)
                key_data = key_pair['public_key']

            # 从镜像元数据中获取root_device_name值,如果这个值没有被指定,则返回None值;
            root_device_name = block_device.properties_root_device_name(image.get('properties', {}))

            # 从参数availability_zone中分离出availability_zone值和forced_host值;
            availability_zone, forced_host = self._handle_availability_zone(availability_zone)

            # 从instance_type这个参数中保存instance type属性到实例的system_metadata数据中,格式如下所示:
            # [prefix]instance_type_[key]
            # 保存instance type信息到system_metadata当中,按照一定的格式;
            system_metadata = instance_types.save_instance_type_info(dict(), instance_type)

            # 保存所有参数选项的字典;
            base_options = {
                'reservation_id': reservation_id,
                'image_ref': image_href,
                'kernel_id': kernel_id or '',
                'ramdisk_id': ramdisk_id or '',
                'power_state': power_state.NOSTATE,
                'vm_state': vm_states.BUILDING,
                'config_drive_id': config_drive_id or '',
                'config_drive': config_drive or '',
                'user_id': context.user_id,
                'project_id': context.project_id,
                'launch_time': time.strftime('%Y-%m-%dT%H:%M:%SZ',time.gmtime()),
                'instance_type_id': instance_type['id'],
                'memory_mb': instance_type['memory_mb'],
                'vcpus': instance_type['vcpus'],
                'root_gb': instance_type['root_gb'],
                'ephemeral_gb': instance_type['ephemeral_gb'],
                'display_name': display_name,
                'display_description': display_description or '',
                'user_data': user_data,
                'key_name': key_name,
                'key_data': key_data,
                'locked': False,
                'metadata': metadata,
                'access_ip_v4': access_ip_v4,
                'access_ip_v6': access_ip_v6,
                'availability_zone': availability_zone,
                'root_device_name': root_device_name,
                'progress': 0,
                'system_metadata': system_metadata}

            # 从image镜像中继承相关属性信息;
            # 包括os_type、architecture、vm_mode和auto_disk_config;
            options_from_image = self._inherit_properties_from_image(image, auto_disk_config)

            # 相关属性更新到字典base_options当中;
            base_options.update(options_from_image)

            # num_instances表示要建立实例的最大数目;
            LOG.debug(_("Going to run %s instances...") % num_instances)

            # 从调度提示信息scheduler_hints中获取过滤器信息;
            filter_properties = dict(scheduler_hints=scheduler_hints)
            
            # 获取filter_properties['force_hosts']的值;(注:这里forced_host是怎么来的还需要进一步研究明白;)
            if forced_host:
                check_policy(context, 'create:forced_host', {})
                filter_properties['force_hosts'] = [forced_host]
以上是一些比较容易理解的参数的验证和处理,这里不做详细的解析,可以直接看代码中我的注释。

c6.实例建立的一些相关工作;

先来看相关的代码:

            for i in xrange(num_instances):
                # 拷贝base_options对象;
                options = base_options.copy()              
                # 为每一个新的实例在数据库中建立新的条目,包括任何更新的表(如安全组等等);
                instance = self.create_db_entry_for_new_instance(context, instance_type, image, options, security_groups, block_device_mapping, num_instances, i)

                # 实例instance添加到instances中;
                instances.append(instance)
                # instance['uuid']添加到instance_uuids中;
                instance_uuids.append(instance['uuid'])
                
                # 验证一个实例instance中的所有的块设备映射;
                self._validate_bdm(context, instance)
                # 为实例的初始化建立发送一个状态更新通知,表示实例状态从不存在到建立;
                # send_update_with_states:在一个实例中,发送compute.instance.update来通知实例的任何的改变信息;
                notifications.send_update_with_states(context, instance, None, vm_states.BUILDING, None, None, service="api")
可见,这里遍历所有要建立的实例,分别为每一个新的实例在数据库中建立新的条目,再验证实例中的块设备映射,而且为实例的初始化建立发送一个状态更新的通知,表示实例状态从不存在到建立。

我们先来看为新的实例在数据库中建立新的条目的实现过程,具体来看方法create_db_entry_for_new_instance的代码:

def create_db_entry_for_new_instance(self, context, instance_type, image,
            base_options, security_group, block_device_mapping, num_instances,
            index):
        """
        为新的实例在数据库中建立新的条目,包括任何更新的表(如安全组等等);
        """
        
        #_populate_instance_for_create:建立一个新的实例的开端;
        #也就是首先执行instance = base_options;
        #然后再补充一些实例的相关信息到instance这个字典中;
        #返回设置好信息的实例字典给;                         
        #期间还进行了一些操作,包括:
        #存储image镜像的属性信息,以便后面我们能够用到它们;
        #对目前这个image镜像实例的来源,即那个基础镜像的记录信息进行保存;          
        instance = self._populate_instance_for_create(base_options,image, security_group)

        #确定实例的显示名称和主机名称(display_name和hostname);
        self._populate_instance_names(instance, num_instances)

        #确定实例的关机和终止状态信息(shutdown_terminate);
        #如果块设备映射(block_device_mapping)为真或者是镜像属性信息中的映射属性(image_properties.get('mappings'))为真
        #或者是镜像属性信息中的块设备映射属性(image_properties.get('block_device_mapping'))为真的时候,
        #实例的关机和终止状态instance['shutdown_terminate']就为假(是不是就是表示实例系统正在创建或者是运行中);
        self._populate_instance_shutdown_terminate(instance, image,block_device_mapping)

        # ensure_default安全组在实例建立前会被调用;
        # ensure_default:确保context有一个安全组,如果没有就建立一个;
        self.security_group_api.ensure_default(context)
        
        #根据context和values建立一个新的实例,并记录在数据库中
        #建立了实例相关的数据表;
        #补充修正字典instance中的参数信息;
        instance = self.db.instance_create(context, instance)

        # 如果要建立实例的最大数目大于1;
        # 这里涉及到一个一个请求生成多个实例的情况,这种情况下实例的命名相关遵循名称模板来进行;
        # 当一个请求创建多个实例的时候,multi_instance_display_name_template这个参数的设置能够使所有的实例都具有唯一的主机名和DISPLAY_NAME;
        # 默认的格式是'%(name)s-%(uuid)s';
        # 我的理解是uuid应该是不同的;
        # 根据名称模板生成实例的相关名称属性,并更新实例的这个属性;
        if num_instances > 1:
            instance = self._apply_instance_name_template(context, instance, index)

        # 确定实例块设备映射信息;
        self._populate_instance_for_bdm(context, instance, instance_type, image, block_device_mapping)

        return instance
这个方法条理比较清晰,主要功能是由几个具体方法实现的。

(1)instance = self._populate_instance_for_create(base_options,image, security_group)

这条语句主要实现了建立一个新的实例的开端,进行了一些必要的初始化工作;

(2)self._populate_instance_names(instance, num_instances)

这条语句主要实现了确定实例display_name和hostname;

(3)self._populate_instance_shutdown_terminate(instance, image,block_device_mapping)

这条语句主要实现了确定实例的关机和终止状态信息(说实话,没有太明白这条语句的功能是什么,实现了什么到底);

(4)self.security_group_api.ensure_default(context)

这条语句主要实现了确保context有一个安全组,如果没有就建立一个;

(5)instance = self.db.instance_create(context, instance)

这条语句主要实现了根据context和values建立一个新的实例,并记录在数据库中,建立了实例相关的数据表,补充修正字典instance中的参数信息。

具体实现我们来看方法instance_create源码:

def instance_create(context, values):
    """
    Create a new Instance record in the database.
    根据context和values建立一个新的实例,并记录在数据库中
    建立了实例相关的数据表;
    补充修正字典values(instance)中的参数信息;

    context - request context object 请求的上下文对象信息;
    values - dict containing column values.包含column值的字典;
    
    注:在/nova/compute/api.py的create_db_entry_for_new_instance方法中,是这样调用这个方法的:
    instance_create(context, instance)
    所以说这个values传进来的是字典instance;
    """
    
    # 补充修正字典values(instance)中的参数信息;
    # (???)为什么拷贝,拷贝过后也都是指向同一个对象;
    values = values.copy()
    
    # 获取values(instance)的'metadata';
    # _metadata_refs返回的是一个列表,作为values['metadata']的key-values中的values;
    # 列表中每一个值都是一个models.InstanceMetadata建立的数据结构;
    # _metadata_refs把values.get('metadata')这个字典中的key-values对对应的赋值到了models.InstanceMetadata所建立的数据结构中的字段key和字段values中;
    # 每一个新建数据结构都是列表(字典values(instance)中'metadata'所对应的的键值是一个列表)中的一个元素;
    # 目前values(instance)的数据结构是:本身是字典,字典的每个键值是列表,列表中的每个元素是models.InstanceMetadata所建立的数据表;
    values['metadata'] = _metadata_refs(values.get('metadata'), models.InstanceMetadata)
    
    # 获取values(instance)的'system_metadata';
    values['system_metadata'] = _metadata_refs(values.get('system_metadata'), models.InstanceSystemMetadata)

    # Instance:表示一个来宾VM的类;
    # 构建数据表instances的数据结构;
    instance_ref = models.Instance()
    
    # 如果values(instance)的uuid值没有定义,则随机生成一个uuid值;
    if not values.get('uuid'):
        values['uuid'] = str(uuid.uuid4())
        
    # 从InstanceInfoCache类中获取实例的缓存信息;
    # InstanceInfoCache:表示一个实例的缓存信息的类;
    # 构建数据表instance_info_caches的数据结构;
    instance_ref['info_cache'] = models.InstanceInfoCache()
    info_cache = values.pop('info_cache', None)
    if info_cache is not None:
        instance_ref['info_cache'].update(info_cache)
    
    # 获取安全组的相关信息,以字典的形式存储;
    security_groups = values.pop('security_groups', [])
    instance_ref.update(values)
代码相对比较好理解,这里不作详细的解析,直接看我的代码注释即可。

(6)instance = self._apply_instance_name_template(context, instance, index)

当要建立的实例数目大于1的时候,执行这条语句,说明建立的不是第一个实例。它主要实现了应用实例的名称模板,根据名称模板生成实例的相关名称属性,并更新实例的这个属性。

(7)self._populate_instance_for_bdm(context, instance, instance_type, image, block_device_mapping)

这条语句主要实现了确定实例块设备映射信息的功能。

方法的最后返回instance这个代表新建立的实例信息的字典。

至此方法create_db_entry_for_new_instance解析完成。

我们回到方法_validate_and_provision_instance之中,在完成为新的实例在数据库中建立新的条目操作之后,还要调用方法send_update_with_states来实现发送一个通知,表示新的实例状态从没有改变为建立。我们来简单看一下方法send_update_with_states,这个方法的功能就是通知实例的任何的改变的信息。具体来看看代码的实现:

def send_update_with_states(context, instance, old_vm_state, new_vm_state,
        old_task_state, new_task_state, service="compute", host=None,
        verify_states=False):
    """
    在一个实例中,发送compute.instance.update来通知实例的任何的改变信息;
    """

    #notify_on_state_change这个参数如果设置的话,表示发送compute.instance.update通知实例状态的变化;
    #参数默认设置为None,表示不通知实例状态的变化;
    #参数如果设置为"vm_state",表示只通知虚拟机状态的变化;
    #参数如果设置为"vm_and_task_state",表示通知虚拟机和任务状态的变化;
    #如果参数设置为None,则说明不通知实例任何的变化,直接返回;
    if not CONF.notify_on_state_change:
        return

    # fire_update表示是否要发送实例状态变化通知;
    fire_update = True

    if verify_states:
        # 检测我们是否需要发送表示实例状态变化的通知;
        fire_update = False
                
        # 如果notify_on_state_change不为空,而且verify_states为真,还有实例的新旧状态不同;
        # 表示条件满足需要通知实例状态变化的条件,所以fire_update设置为True;
        if old_vm_state != new_vm_state:
            fire_update = True
        
        # 如果notify_on_state_change设置为vm_and_task_state,并且实例的新旧状态不同;
        # 则设置fire_update为True,表示要发送状态的变化通知;
        elif CONF.notify_on_state_change:
            if (CONF.notify_on_state_change.lower() == "vm_and_task_state" and old_task_state != new_task_state):
                fire_update = True

    # 如果fire_update为真;
    # 发送compute.instance.update通知到observers告知关于实例状态的变化;
    if fire_update:
        try:
            _send_instance_update_notification(context, instance,
                    old_vm_state=old_vm_state, old_task_state=old_task_state,
                    new_vm_state=new_vm_state, new_task_state=new_task_state,
                    service=service, host=host)
        except Exception:
            LOG.exception(_("Failed to send state update notification"),
                    instance=instance)
可以看到,这里进一步调用了方法_send_instance_update_notification实现通知发送的功能。
def _send_instance_update_notification(context, instance, old_vm_state=None,
            old_task_state=None, new_vm_state=None, new_task_state=None,
            service="compute", host=None):
    """
    发送compute.instance.update通知到observers告知关于实例状态的变化;
    """

    # 获取一个实例的详细信息;
    # 存储到字典instance_info中,并返回给payload;
    payload = info_from_instance(context, instance, None, None)

    # 获取新虚拟机和新任务的状态;
    if not new_vm_state:
        new_vm_state = instance["vm_state"]
    if not new_task_state:
        new_task_state = instance["task_state"]

    # 存储虚拟机新旧状态和任务新旧状态信息的字典;
    states_payload = {
        "old_state": old_vm_state,
        "state": new_vm_state,
        "old_task_state": old_task_state,
        "new_task_state": new_task_state,
    }

    # 存储实例详细信息的字典payload中增加虚拟机和任务新旧状态的元素;
    payload.update(states_payload)

    # 增加时间段审核部分信息;(???获取的是什么时间?目前理解的是实例使用的开始和结束时间;)
    # 获取相关审核使用期限的开始和结束;
    # 如果current_period为真的话;
    # 返回当前阶段的审核时间段;
    # 即,上一次的结束时间作为目前时间段的开始时间,当前的时间作为目前时间段的结束时间;
    # 如果current_period为假的话;
    # 返回上一个时间段的审核时间;
    (audit_start, audit_end) = audit_period_bounds(current_period=True)
    payload["audit_period_beginning"] = audit_start
    payload["audit_period_ending"] = audit_end

    # 增加带宽使用信息;
    # bandwidth_usage:获取指定的审计期间,实例的带宽使用信息;
    bw = bandwidth_usage(instance, audit_start)
    payload["bandwidth"] = bw

    # 获取发布信息者的ID;
    publisher_id = notifier_api.publisher_id(service, host)
    # notify:使用指定的驱动程序发布通知;
    notifier_api.notify(context, publisher_id, 'compute.instance.update', notifier_api.INFO, payload)
进而来看方法notify:

def notify(context, publisher_id, event_type, priority, payload):
    """
    使用指定的驱动程序发布通知;

    param publisher_id: 该信息的worker_type.host源;
    param event_type: 字面上的事件的类型;(如建立实例)
    param priority: python记录日志的级别;(DEBUG, WARN, INFO, ERROR, CRITICAL)
    param payload: 一个保存属性信息的字典;

    信息格式的示例:
    Message example::

        {'message_id': str(uuid.uuid4()),
         'publisher_id': 'compute.host1',
         'timestamp': timeutils.utcnow(),
         'priority': 'WARN',
         'event_type': 'compute.create_instance',
         'payload': {'instance_id': 12, ... }}

    """
    
    # log_levels = (DEBUG, WARN, INFO, ERROR, CRITICAL)
    if priority not in log_levels:
        raise BadPriorityException(
            _('%s not in valid priorities') % priority)

    # 确保一切都是JSON格式可序列化的;
    # to_primitive转换一个复杂的对象payload到primitives格式;
    payload = jsonutils.to_primitive(payload, convert_instances=True)

    # 格式符合信息格式的示例:
    msg = dict(message_id=str(uuid.uuid4()),
               publisher_id=publisher_id,
               event_type=event_type,
               priority=priority,
               payload=payload,
               timestamp=str(timeutils.utcnow()))
    
    # _get_drivers:基于配置文件,添加一个或多个发送通知的运行时的驱动程序并返回;
    # notification_driver:这个参数定义了执行发送通知操作的驱动程序或者是驱动程序列表;
    # 循环每一个驱动程序;
    # notify:通过RPC发送一个通知;
    for driver in _get_drivers():
        try:
            driver.notify(context, msg)
        except Exception as e:
            LOG.exception(_("Problem '%(e)s' attempting to "
                            "send to notification system. "
                            "Payload=%(payload)s")
                          % dict(e=e, payload=payload))


_drivers = None
可见,最后还是通过RPC机制来实现发送一个通知。

d.return (instances, request_spec, filter_properties)

在方法_validate_and_provision_instance的最后,返回要建立实例的各类信息;

其中,instances是代表若干个(一个或多个)新建立的实例的信息的集合;

request_spec是生成的包含请求建立实例的信息的字典,其具体形式为:

request_spec = {
            'image': jsonutils.to_primitive(image), # 转换镜像image对象到primitives格式;
            'instance_properties': base_options, # 实例属性;
            'instance_type': instance_type, # 实例类型;
            'instance_uuids': instance_uuids, # 实例UUID;
            'block_device_mapping': block_device_mapping, # 块设备映射信息;
            'security_group': security_groups, # 安全组信息;
        }

至此,方法_validate_and_provision_instance解析完成,这个方法主要完成的是对建立实例所调用的方法_create_instance中输入的各个参数进行检查验证,其中完成了两项非常重要的任务,即资源的配额管理和为新建立实例在数据库中新建数据条目等准备工作。

在下一篇博文中,我们将回到方法/nova/compute/api.py----def _create_instance中,继续对这个方法中发送要运行实例('run_instance')的请求消息到远程节点的功能实现部分进行解析。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值