Openstack Cinder中建立volume过程的源码解析(4)----以及taskflow相关解析

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


在上一篇博客中,在cinder模块中实现客户端发送过来的请求信息操作的主要的步骤已经全部解析完成。在本篇博客中,我将解析方法cinder.api.v1.volumes.VolumeController.create,来进一步解析cinder是如何实现卷的建立的。而且在这部分内容中,应用了taskflow库(或者可以理解成一种设计模式)来实现卷的建立过程。

目前openstack的源代码随着模块和功能的增加和完善,代码量快速增长,功能的实现复杂度也随之增加,引进taskflow库能够实现方便的代码管理,并且增加功能实现的安全性。简单来说,当实现一个功能时,应用taskflow模式能够实现对flow执行的管理,能够开始、中止、重新开始以及逆转回滚等操作,比如当执行某个flow操作出现异常时,可以视具体情况尝试进行flow的逆转回滚操作,实现回溯到flow执行之前的状态。

在本篇博客中,我会依据卷的建立为例,具体解析taskflow在cinder模块中卷建立过程中的应用情况。

我们来看代码/cinder/api/v1/volumes.py----class VolumeController----def create的具体源码实现:

    @wsgi.serializers(xml=VolumeTemplate)
    @wsgi.deserializers(xml=CreateDeserializer)
    def create(self, req, body):
        """
        Creates a new volume.
        建立新卷的API;
        
        req = POST /v1/ecf0109bda814fa1a548af63f9ada370/volumes HTTP/1.0
              Accept: application/json
              Accept-Encoding: gzip, deflate, compress
              Content-Length: 294
              Content-Type: application/json
              Host: 172.21.5.164:8776
              User-Agent: python-cinderclient
              X-Auth-Project-Id: admin
              X-Auth-Token: MIIQKQYJKoZI......TBvg==
              X-Domain-Id: None
              X-Domain-Name: None
              X-Identity-Status: Confirmed
              X-Project-Domain-Id: None
              X-Project-Domain-Name: None
              X-Project-Id: ecf0109bda814fa1a548af63f9ada370
              X-Project-Name: admin
              X-Role: _member_,admin
              X-Roles: _member_,admin
              X-Service-Catalog: 
              [{"endpoints_links": [], "endpoints": [....], "type": "compute", "name": "nova"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "network", "name": "neutron"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "volumev2", "name": "cinder_v2"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "s3", "name": "swift_s3"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "image", "name": "glance"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "metering", "name": "ceilometer"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "volume", "name": "cinder"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "ec2", "name": "nova_ec2"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "object-store", "name": "swift"}, 
               {"endpoints_links": [], "endpoints": [....], "type": "identity", "name": "keystone"}]
              X-Tenant: admin
              X-Tenant-Id: ecf0109bda814fa1a548af63f9ada370
              X-Tenant-Name: admin
              X-User: admin
              X-User-Domain-Id: None
              X-User-Domain-Name: None
              X-User-Id: d2ee2dd06c9e49098a3d2a278c650b6c
              X-User-Name: admin
              {"volume": {"status": "creating", 
                          "availability_zone": null, 
                          "source_volid": null, 
                          "display_description": null, 
                          "snapshot_id": null, 
                          "user_id": null, 
                          "size": 1, 
                          "display_name": "shinian01", 
                          "imageRef": null, 
                          "attach_status": "detached", 
                          "volume_type": null, 
                          "project_id": null, 
                          "metadata": {}}}
        
        body = {u'volume': {u'status': u'creating', 
                            u'user_id': None, 
                            u'imageRef': None, 
                            u'availability_zone': None, 
                            'scheduler_hints': {}, 
                            u'attach_status': u'detached', 
                            u'display_description': None, 
                            u'metadata': {}, 
                            u'source_volid': None, 
                            u'snapshot_id': None, 
                            u'display_name': u'shinian01', 
                            u'project_id': None, 
                            u'volume_type': None, 
                            u'size': 1}}
        """        
        if not self.is_valid_body(body, 'volume'):
            raise exc.HTTPUnprocessableEntity()

        LOG.debug('Create volume request body: %s', body)
        
        # 获取cinder的上下文信息;
        context = req.environ['cinder.context']
        volume = body['volume']

        kwargs = {}

        # 获取请求信息中的volume_type信息;
        req_volume_type = volume.get('volume_type', None)
        #req_volume_type = None
        
        # 通过名称或给定id获取单个的卷的类型;
        if req_volume_type:
            try:
                if not uuidutils.is_uuid_like(req_volume_type):
                    # 根据名称获取单个卷的类型;
                    kwargs['volume_type'] = volume_types.get_volume_type_by_name(context, req_volume_type)
                else:
                    # 根据给定id来检索获取单个的卷的类型;
                    kwargs['volume_type'] = volume_types.get_volume_type(context, req_volume_type)
            except exception.VolumeTypeNotFound:
                explanation = 'Volume type not found.'
                raise exc.HTTPNotFound(explanation=explanation)

        # 从请求信息的body部分获取元数据信息;
        kwargs['metadata'] = volume.get('metadata', None)
        # kwargs['metadata'] = {}

        # 从请求信息的body部分获取快照id信息;
        snapshot_id = volume.get('snapshot_id')
        # snapshot_id = None
        
        # 如果给定快照id信息,则根据snapshot_id获取指定的卷的快照。如果没有给定快照id信息,则赋值为None;
        if snapshot_id is not None:
            # get_snapshot:获取snapshot_id指定卷的快照;
            kwargs['snapshot'] = self.volume_api.get_snapshot(context, snapshot_id)
        else:
            kwargs['snapshot'] = None

        # 从请求信息的body部分获取source_volid信息;
        source_volid = volume.get('source_volid')
        # source_volid = None
        
        # 如果给定原卷的id信息,则根据source_volid获取指定的原卷。如果没有给定原卷的id信息,则赋值为None;
        if source_volid is not None:
            # 根据volume_id获取volume;
            kwargs['source_volume'] = self.volume_api.get_volume(context, source_volid)
        else:
            kwargs['source_volume'] = None

        # 从请求信息的body部分获取size信息;
        size = volume.get('size', None)
        # size = 1
        
        # 如果size为None,则从kwargs['snapshot']中或者kwargs['source_volume']中获取卷的大小值;
        if size is None and kwargs['snapshot'] is not None:
            size = kwargs['snapshot']['volume_size']
        elif size is None and kwargs['source_volume'] is not None:
            size = kwargs['source_volume']['size']

        LOG.audit(_("Create volume of %s GB"), size, context=context)

        image_href = None
        image_uuid = None
        
        # 检测self.extensions中是否包含'os-image-create';
        if self.ext_mgr.is_loaded('os-image-create'):
            # NOTE(jdg): misleading name "imageRef" as it's an image-id
            image_href = volume.get('imageRef')
            
            if image_href:
                # 从image_href获取uuid;
                image_uuid = self._image_uuid_from_href(image_href)
                kwargs['image_id'] = image_uuid

        # 从请求信息的body部分获取availability_zone信息;
        kwargs['availability_zone'] = volume.get('availability_zone', None)
        # kwargs['availability_zone'] = None

        new_volume = self.volume_api.create(context,
                                            size,
                                            volume.get('display_name'), # 要建立卷的名称;
                                            volume.get('display_description'),
                                            **kwargs)

        # TODO(vish): Instance should be None at db layer instead of
        #             trying to lazy load, but for now we turn it into
        #             a dict to avoid an error.
        # 转换new_volume格式;
        new_volume = dict(new_volume.iteritems())

        self._add_visible_admin_metadata(context, new_volume)

        # _translate_volume_detail_view:获取指定卷详细的重要属性信息;
        retval = _translate_volume_detail_view(context, new_volume, image_uuid)

        return {'volume': retval}
这个方法很好理解,主要完成了三部分内容:

1.通过req和body获取建立卷所需的相关参数,因为建立卷的方法有几种,后面会分析到,具体采用哪种卷的建立方法,是根据req和body中的相关参数决定的,而且卷的建立也许要确定一些参数,例如卷的大小等等;

2.调用方法create实现卷的建立;

3.获取建立卷后的反馈信息,实现格式转换,并获取其中重要的属性信息,以便上层调用生成卷的建立的响应信息;


我们再来看其中调用的方法create,实际上调用的是方法/cinder/volume/api.py----class API----def create,我们来看其具体的源码实现:

    def create(self, context, size, name, description, snapshot=None,
               image_id=None, volume_type=None, metadata=None,
               availability_zone=None, source_volume=None,
               scheduler_hints=None, backup_source_volume=None):
        """
        实现建立卷的操作;
        """

        def check_volume_az_zone(availability_zone):
            """
            验证availability_zone是否是可用的(即是否包含在可用zone的列表中);
            """
            try:
                # _valid_availabilty_zone:验证availability_zone是否是可用的(即是否包含在可用zone的列表中);
                return self._valid_availabilty_zone(availability_zone)
            except exception.CinderException:
                LOG.exception(_("Unable to query if %s is in the "
                                "availability zone set"), availability_zone)
                return False

        # 所要建立卷的规格数据信息;
        create_what = {
            'size': size,
            'name': name,
            'description': description,
            'snapshot': snapshot,
            'image_id': image_id,
            'volume_type': volume_type,
            'metadata': metadata,
            'availability_zone': availability_zone,
            'source_volume': source_volume,
            'scheduler_hints': scheduler_hints,
            'key_manager': self.key_manager,
            'backup_source_volume': backup_source_volume,
        }
        
        # 构建并返回用于建立卷的flow;
        # self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI();
        # self.volume_rpcapi = volume_rpcapi.VolumeAPI();
        # self.image_service = (image_service or glance.get_default_image_service())
        # check_volume_az_zone:验证availability_zone是否是可用的(即是否包含在可用zone的列表中);
        # create_what:所要建立卷的规格数据信息;
        (flow, uuid) = create_volume.get_api_flow(self.scheduler_rpcapi,
                                                  self.volume_rpcapi,
                                                  self.db,
                                                  self.image_service,
                                                  check_volume_az_zone,
                                                  create_what)

        # 应用assert关键字来声明flow是真的;
        assert flow, _('Create volume flow not retrieved')
        
        
        # 运行用于建立卷的flow;
        flow.run(context)
        
        # 如果flow的运行状态不为states.SUCCESS,则引发异常;
        if flow.state != states.SUCCESS:
            raise exception.CinderException(_("Failed to successfully complete"
                                              " create volume workflow"))

        # Extract the volume information from the task uuid that was specified
        # to produce said information.
        # 通过task的uuid值获取建立卷的信息;
        volume = None
        try:
            volume = flow.results[uuid]['volume']
        except KeyError:
            pass

        # Raise an error, nobody provided it??
        # 应用assert关键字来声明volume是真的;
        assert volume, _('Expected volume result not found')
        
        return volume
在这个方法中主要实完成了四部分内容:

1.构建字典create_what,实现整合建立卷的具体参数;

2.构建并返回用于建立卷的flow;

3.执行构建的用于建立卷的flow;

4.从flow中获取建立卷的反馈信息;

1.先来看一下语句变量create_what的输出实例:

    create_what = {'backup_source_volume': None,
                   'description': None,
                   'availability_zone': None,
                   'image_id': None,
                   'scheduler_hints': None,
                   'name': u'shinian01',
                   'source_volume': None,
                   'volume_type': None,
                   'snapshot': None,
                   'size': 1,
                   'key_manager': ,
                   'metadata': {}}

2.构建并返回用于建立卷的flow

我们重点来看第2点,即如何构建用于建立卷的flow;

来看语句:

(flow, uuid) = create_volume.get_api_flow(self.scheduler_rpcapi,
                                          self.volume_rpcapi,
                                          self.db,
                                          self.image_service,
                                          check_volume_az_zone,
                                          create_what)

我们先来简单分析一下传入方法get_api_flow中的几个变量信息:

self.scheduler_rpcapi = scheduler_rpcapi.SchedulerAPI();

这个参数用于当请求信息中没有指定要建立卷的目标主机时,通过调度器API中的方法根据具体情况应用合适的调度算法实现选取目标主机并进行建立卷的操作;

self.volume_rpcapi = volume_rpcapi.VolumeAPI();

这个参数指定了volume RPC API客户端,实现通过RPC对卷进行远程的相关操作的执行;

self.image_service = (image_service or glance.get_default_image_service());

这个参数指定了镜像服务的接口API,主要用于当根据镜像实现卷的建立这种情况中;

check_volume_az_zone:验证availability_zone是否是可用的(即是否包含在可用zone的列表中);

create_what:所要建立卷的规格数据信息;

OK!我们来看方法get_api_flow,这个方法主要实现了构建并返回用于建立卷的flow,我们来看其源码实现:

def get_api_flow(scheduler_rpcapi, volume_rpcapi, db,
                 image_service,
                 az_check_functor,
                 create_what):
    """
    构建并返回用于建立卷的flow;
    flow将会做如下的事情:
    1.获取类Flow的初始化对象;
    2.注入字典信息create_what到flow中;
    3.验证用于后续操作的相关参数,返回经过验证的参数信息;
    4.根据给定的建立卷的大小和类型信息,对资源配额信息进行检测,检测建立卷的可行性;
      并实现对数据库中资源配额信息的更新,并且保留建立新卷之前的相关资源配额信息;
    5.在数据库中为给定的卷建立相关条目;
    6.提交新的资源配额的预留信息到数据库中;
    7.远程调用实现卷的建立操作;
    """

    # 获取flow任务的名称;
    # ACTION = 'volume:create'
    # flow_name = 'volume_create_api'
    flow_name = ACTION.replace(":", "_") + "_api"
    
    # 获取类Flow的初始化对象;
    # Flow:线性工作流管理类;
    api_flow = linear_flow.Flow(flow_name)
    
    # 添加一个给定的task到flow;
    # InjectTask:这个类实现了注入字典信息create_what到flow中;
    # create_what是建立卷所要遵守的规范信息(字典);
    api_flow.add(base.InjectTask(create_what, addons=[ACTION]))
    
    # 添加一个给定的task到flow;
    # ExtractVolumeRequestTask:实现提取并验证输入的请求信息,并返回经过验证的参数信息,以便用于其他task当中;
    # 这个task的主要任务是提取和验证输入的值,这些输入的值将形成一个潜在的卷的请求信息;
    # 并且实现根据一组条件对这些输入值进行验证,并将这些输入值转换成有效的集合;
    # 并返回这些经过验证和转换的输入值,这些输入值将会应用于其他task中。
    # image_service:获取默认的镜像服务类
    # az_check_functor:验证availability_zone是否是可用的(即是否包含在可用zone的列表中);
    api_flow.add(ExtractVolumeRequestTask(image_service, az_check_functor))
    
    # 添加一个给定的task到flow;
    # QuotaReserveTask:根据给定的大小值和给定的卷类型信息实现保存单一的卷;
    # 根据给定的建立新卷的大小和类型,对资源配额信息进行检测,检测建立卷的可行性;
    # 根据给定的建立新卷的大小和类型,实现对数据库中资源配额信息的更新;
    # 保留建立新卷之前的相关资源配额信息,用于一旦卷建立的执行出现异常时,调用逆转回滚方法实现资源配额信息的恢复;
    # QuotaReserveTask:根据给定的大小值和给定的卷类型信息实现保存单一的卷;
    api_flow.add(QuotaReserveTask())
    
    # 添加一个给定的task到flow;
    # EntryCreateTask:在数据库中为给定的卷建立相关条目;
    v_uuid = api_flow.add(EntryCreateTask(db))
    
    # 添加一个给定的task到flow;
    # QuotaCommitTask:提交新的资源配额的预留信息到数据库中;
    #(假设卷建立成功, 如果卷的建立出现异常,将会执行本flow中的回滚操作);
    api_flow.add(QuotaCommitTask())

    # 添加一个给定的task到flow;
    # 这个task实现了当出现错误时,设置指定id的卷的状态为ERROR;
    api_flow.add(OnFailureChangeStatusTask(db))

    # 添加一个给定的task到flow;
    # 远程调用实现卷的建立操作;
    api_flow.add(VolumeCastTask(scheduler_rpcapi, volume_rpcapi, db))


    # Note(harlowja): this will return the flow as well as the uuid of the
    # task which will produce the 'volume' database reference (since said
    # reference is returned to other callers in the api for further usage).
    # 返回相关状态的变化信息,并获取flow中task的uuid值;
    return (flow_utils.attach_debug_listeners(api_flow), v_uuid)
我们可以看到关于taskflow的构建脉络很清晰的,这个方法中主要通过三部,实现了用于建立卷的flow的构建:

1.初始化一个Flow类;

2.按顺序添加相关的task到flow中;

3.返回添加task后的flow给调用者;

OK!这篇博客先写到这里,在下一篇博客中,将会关注用于卷的建立的flow中都添加了哪些task,当然,taskflow的应用还没有分析完成,后面也会继续对其进行分析和总结。

你可能感兴趣的:(OpenStack源码分析)