基于镜像卷启动的虚机快照代码分析

基于ocata版本进行跟踪分析

1、nova-api接受并处理请求的入口函数nova/api/openstack/compute/servers.py/ServersController._action_create_image

nova/api/openstack/compute/servers.py
class ServersController(wsgi.Controller):
   @wsgi.action('createImage')
    @common.check_snapshots_enabled
    @validation.schema(schema_servers.create_image, '2.0', '2.0')
    @validation.schema(schema_servers.create_image, '2.1')
    def _action_create_image(self, req, id, body):
        """Snapshot a server instance."""
        # 从req中获取请求的上下文,并验证执行权限
        context = req.environ['nova.context']
        context.can(server_policies.SERVERS % 'create_image')
        
        # 从body中解析出传递的参数,快照名称及属性信息
        entity = body["createImage"]
        image_name = common.normalize_name(entity["name"])
        metadata = entity.get('metadata', {})

        # Starting from microversion 2.39 we don't check quotas on createImage
        if api_version_request.is_supported(
                req, max_version=
                api_version_request.MAX_IMAGE_META_PROXY_API_VERSION):
            # 检查快照属性的相关配额信息
            common.check_img_metadata_properties_quota(context, metadata)
        #通过虚机uuid,从数据库中获取虚机实例的信息,返回的是一个实例对象
        instance = self._get_server(context, req, id)
        #从数据库bloack_device_mapping表里面获取该虚机所有的块设备映射信息
        bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context, instance.uuid)

        try:
            # 判断虚机是基于镜像启动还是基于磁盘启动
            if compute_utils.is_volume_backed_instance(context, instance,bdms):
                context.can(server_policies.SERVERS %'create_image:allow_volume_backed')
                #基于磁盘启动的虚机快照入口
                image = self.compute_api.snapshot_volume_backed(
                                                       context,
                                                       instance,
                                                       image_name,
                                                       extra_properties=metadata)
            else:
                #基于镜像启动的虚机快照入口
                image = self.compute_api.snapshot(context,
                                                  instance,
                                                  image_name,
                                                  extra_properties=metadata)
        .........

        # build location of newly-created image entity
        image_id = str(image['id'])
        #根据glance配置为该镜像生成url
        image_ref = glance.generate_image_url(image_id)

        resp = webob.Response(status_int=202)
        resp.headers['Location'] = image_ref
        return resp

因此可以看到,执行基于磁盘启动虚机快照时,实际走的是“ compute_api.snapshot_volume_backed ”

nova/compute/api.py
@profiler.trace_cls("compute_api")
class API(base.Base):
    """API for interacting with the compute manager."""
   def snapshot_volume_backed(self, context, instance, name,
                               extra_properties=None):
        """Snapshot the given volume-backed instance.

        :param instance: nova.objects.instance.Instance object
        :param name: name of the backup or snapshot
        :param extra_properties: dict of extra image properties to include

        :returns: the new image metadata
        """
        #获取虚机的metadata属性
        image_meta = self._initialize_instance_snapshot_metadata(instance, name, extra_properties)------s1步
        # the new image is simply a bucket of properties (particularly the
        # block device mapping, kernel and ramdisk IDs) with no image data,
        # hence the zero size
        新镜像只是一堆属性(特别是块设备映射、内核和ramdisk id),没有映像数据,因此大小为零
        image_meta['size'] = 0
        for attr in ('container_format', 'disk_format'):---清除镜像metadata属性中的 container_format,disk_format 属性
            image_meta.pop(attr, None)
        properties = image_meta['properties']
        # clean properties before filling
        # 清除properties属性里面的'block_device_mapping', 'bdm_v2', 'root_device_name'相关属性值
        for key in ('block_device_mapping', 'bdm_v2', 'root_device_name'):
            properties.pop(key, None)
        # 将实例中的‘root_device_name’属性更新到properties属性里,image_meta的最终内容如:
        # {
        #     'name': u'snapshot1',
        #     u'min_ram': u'0',
        #     u'min_disk': u'20',
        #     'is_public': False,
        #     'properties': {
        #         u'base_image_ref': u'',
        #         'root_device_name': u'/dev/vda'
        #     },
        #     'size': 0
        # }
        if instance.root_device_name:
            properties['root_device_name'] = instance.root_device_name

        quiesced = False
        if instance.vm_state == vm_states.ACTIVE:
            try:
                # 判断虚拟机的状态,如果虚拟机处于active,则通过rpc通知虚拟机进入静默状态
                self.compute_rpcapi.quiesce_instance(context, instance)
                quiesced = True
            except (exception.InstanceQuiesceNotSupported,
                    exception.QemuGuestAgentNotEnabled,
                    exception.NovaException, NotImplementedError) as err:
                if strutils.bool_from_string(instance.system_metadata.get(
                        'image_os_require_quiesce')):
                    raise
                else:
                    LOG.info(_LI('Skipping quiescing instance: '
                                 '%(reason)s.'), {'reason': err},
                             instance=instance)
        # 从数据库中获取该虚机所关联的所有块设备,结果会返回一个BlockDeviceMappingList对象
        bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context, instance.uuid)

        mapping = []
        #做快照的操作,虚机挂在了多少个卷设备,就要做多少次快照
        for bdm in bdms:--------s2步
            #映射关系中没有块设备,则忽略此条映射
            if bdm.no_device:
                continue

            if bdm.is_volume:----这个读取的是block_device_mapping表里面的destination_type字段
                # create snapshot based on volume_id
                volume = self.volume_api.get(context, bdm.volume_id)----调用cinderclient去,根据卷的volume_id从数据库获取卷的详细信息---s4步
                # NOTE(yamahata): Should we wait for snapshot creation?
                #                 Linux LVM snapshot creation completes in
                #                 short time, it doesn't matter for now.
                name = _('snapshot for %s') % image_meta['name']----快照的名字进行组装,比如快照名称是snapshot1,则这里就是snapshot for snapshot1
                LOG.debug('Creating snapshot from volume %s.', volume['id'],
                          instance=instance)
                snapshot = self.volume_api.create_snapshot_force(------------s3步,调用cinderclient,给cinder发送强制创建快照的请求
                    context, volume['id'], name, volume['display_description'])
                mapping_dict = block_device.snapshot_from_bdm(snapshot['id'],bdm)-----s5步
                #过滤掉已经在数据库中存在的字段
                mapping_dict = mapping_dict.get_image_mapping()
            else:
                mapping_dict = bdm.get_image_mapping()

            mapping.append(mapping_dict)----将云主机所有的映射关系都添加到mapping中
        #通过rpc.case发送异步请求给nova-compute
        # nova-compute接收到消息后,会等到快照完成后对文件系统进行解冻(
        if quiesced:
            self.compute_rpcapi.unquiesce_instance(context, instance, mapping)

        # 更新云主机metadata信息中的properties信息
        if mapping:
            properties['block_device_mapping'] = mapping
            properties['bdm_v2'] = True
            """
            #到这一步时,会到添加一条记录到glance快照(镜像)数据库条目
            #(会在Dashboard的镜像面板显示一条名为snapshot1的快照记录),
            # 快照的大部分信息都拷贝至系统盘属性,这是因为卷快照是可以直接用来启动云主机的,
            # 另外'block_device_mapping'属性中包含所有的volume设备快照信息(如果有的话),
            # 每个volume设备快照信息作为一条记录,记录在image_properties数据表;
            # 
              {
                 'name': u'snapshot1',
                 'min_ram': u'0',
                 'min_disk': u'20',
                 'is_public': False,
                 'properties': {
                         'bdm_v2': True,
                         'block_device_mapping': [{
                                  'guest_format': None,
                                  'boot_index': 0,
                                  'no_device': None,
                                  'image_id': None,
                                  'volume_id': None,
                                  'device_name': u'/dev/vda',
                                  'disk_bus': u'virtio',
                                  'volume_size': 20,
                                  'source_type': 'snapshot',
                                  'device_type': u'disk',
                                  'snapshot_id': u'cede2421-ea68-4a8e-937d-c27074b9024b',
                                  'destination_type': 'volume',
                                  'delete_on_termination': False
                         }],
                         'base_image_ref': u'',
                         'root_device_name': u'/dev/vda'
                 },
                 'size': 0
            }
            """
        return self.image_api.create(context, image_meta)

s1步,主要作用是根据虚机的镜像元数据初始化该虚机快照的元数据

nova/compute/api.py
class API(base.Base):
    def _initialize_instance_snapshot_metadata(self, instance, name,
                                               extra_properties=None):
        """Initialize new metadata for a snapshot of the given instance.
        :param instance: nova.objects.instance.Instance object
        :param name: string for name of the snapshot
        :param extra_properties: dict of extra metadata properties to include
        :returns: the new instance snapshot metadata
        """
        image_meta = utils.get_image_from_system_metadata(instance.system_metadata)-------获取虚机的镜像源数据
        image_meta.update({'name': name,'is_public': False})----把镜像元数据的中镜像的名字,更改为快照的名字

        # Delete properties that are non-inheritable
        properties = image_meta['properties']
        for key in CONF.non_inheritable_image_properties:----删除镜像数据中不能继承的属性,
            properties.pop(key, None)

        # The properties in extra_properties have precedence
        properties.update(extra_properties or {})
        return image_meta
返回值为
 {
     u'min_disk': u'20',
     'is_public': False,
     'min_ram': u'0',
     'properties': {
         'base_image_ref': u''
      },
     'name': u'snapshot1'
 }

 s2 步 block_device_mapping表里面的数据样例

 *************************** 376. row ***************************
           created_at: 2019-07-18 12:35:24
           updated_at: 2019-07-18 12:35:46
           deleted_at: 2019-07-18 12:35:46
                   id: 24081
          device_name: /dev/vdb
delete_on_termination: 0
          snapshot_id: NULL
            volume_id: e6f0ddca-74cf-40c9-8db6-d64f32d8ded4
          volume_size: NULL
            no_device: NULL
      connection_info: null
        instance_uuid: f9846a41-72c2-4e67-99d3-8391fae7a3ce
              deleted: 24081
          source_type: volume
     destination_type: volume
         guest_format: NULL
          device_type: NULL
             disk_bus: NULL
           boot_index: NULL
             image_id: NULL
                  tag: NULL
*************************** 377. row ***************************
           created_at: 2019-07-19 03:02:57----------基于镜像启动block_device_mapping形式
           updated_at: 2019-07-19 03:02:57
           deleted_at: 2019-07-19 03:10:42
                   id: 24084
          device_name: /dev/vda
delete_on_termination: 1
          snapshot_id: NULL
            volume_id: NULL
          volume_size: NULL
            no_device: 0
      connection_info: NULL
        instance_uuid: 99f10424-2ad3-4cdb-8ca5-ebd166d5853c
              deleted: 24084
          source_type: image
     destination_type: local
         guest_format: NULL
          device_type: disk
             disk_bus: NULL
           boot_index: 0
             image_id: 3006d221-72a6-4fe0-bcdc-d4ace809d8c7
                  tag: NULL

s5步  block_device.snapshot_from_bdm(snapshot['id'],bdm)

根据bdm信息,来构建快照的dict格式属性信息,返回一个BlockDeviceDict对象,属性如下:
 {
  'guest_format': None, 
  'boot_index': 0, 
  'no_device': None, 
  'connection_info': None, 
  'snapshot_id': u'cede2421-ea68-4a8e-937d-c27074b9024b',
  'volume_size': 20, 
  'device_name': u'/dev/vda', 
  'disk_bus': u'virtio', 
  'image_id': None, 
  'source_type': 'snapshot', 
  'device_type': u'disk', 
  'volume_id': None, 
  'destination_type': 'volume', 
  'delete_on_termination': False
 }

nova-api主要是 完成了以下工作:
1)如果是在线快照,则冻结/解冻结文件系统
2)创建glance数据库镜像记录(包含所有卷的快照信息)

2、cinder-api服务的相关处理

nova-api服务里面s3步的操作,调用cinder的api create_snapshot_force 创建新的卷,实为cinder api的接受请求,进行相关的处理

其详解如下,
from nova.volume import cinder
self.volume_api = volume_api or cinder.API()
snapshot = self.volume_api.create_snapshot_force(context, volume['id'], name, volume['display_description'])
nova/volume/cinder.py
    def create_snapshot_force(self, context, volume_id, name, description):
        item = cinderclient(context).volume_snapshots.create(volume_id,True,name,description)
        return _untranslate_snapshot_summary_view(context, item)

因此可以看出,实际调用的是cinder client的 volume_snapshots 的 create 方法,其在cinder api的入口函数为

cinder/api/v2/snapshots.py
class SnapshotsController(wsgi.Controller):
    @wsgi.response(202)
    def create(self, req, body):
        """Creates a new snapshot."""
        # 根据上下文的分析,当nova-api等其他client在发送创建卷快照的请求之后,本方法会接受到请求
        # 方法接收到的参数有:
        # req:Request对象,包含有本次请求的上下内容,包含有用于鉴权的凭证等内容
        # body:快照的属性信息,包含有如下内容:
        #  {
        #      u'snapshot': {
        #           u'volume_id': u'60e16af2-0684-433c-a1b6-c1af1c2523fc',
        #           u'force': True,
        #           u'description': u'',
        #           u'name': u'snapshot for snapshot1',
        #           u'metadata': {}
        #      }
        #  }
        kwargs = {}
        #获取上下文的context信息和获取快照属性中的信息
        context = req.environ['cinder.context']
        self.assert_valid_body(body, 'snapshot')
        snapshot = body['snapshot']
        #获取快照的metadata信息,snapshot_id
        kwargs['metadata'] = snapshot.get('metadata', None)

        try:
            volume_id = snapshot['volume_id']
        except KeyError:
            msg = _("'volume_id' must be specified")
            raise exc.HTTPBadRequest(explanation=msg)
        #从数据库中获取卷信息
        volume = self.volume_api.get(context, volume_id)
        #获取传递进来的参数中是否使用强制快照,force=True表示采取强制快照
        force = snapshot.get('force', False)
        msg = _LI("Create snapshot from volume %s")
        LOG.info(msg, volume_id)
        #验证快照名及快照描述是否合法,长度不能超过256个字符
        self.validate_name_and_description(snapshot)

        # NOTE(thingee): v2 API allows name instead of display_name
        # 用display_name代替name参数
        if 'name' in snapshot:
            snapshot['display_name'] = snapshot.pop('name')
        try:
        #参数类型转换,如果是非True/False的值,则抛异常
            force = strutils.bool_from_string(force, strict=True)
        except ValueError as error:
            err_msg = encodeutils.exception_to_unicode(error)
            msg = _("Invalid value for 'force': '%s'") % err_msg
            raise exception.InvalidParameterValue(err=msg)
        # 开始进行快照的操作,根据force值的不同走不通的分支
        if force:
            new_snapshot = self.volume_api.create_snapshot_force(-----s2.1
                context,
                volume,
                snapshot.get('display_name'),
                snapshot.get('description'),
                **kwargs)
        else:
            new_snapshot = self.volume_api.create_snapshot(-----s2.2
                context,
                volume,
                snapshot.get('display_name'),
                snapshot.get('description'),
                **kwargs)
        req.cache_db_snapshot(new_snapshot)
        return self._view_builder.detail(req, new_snapshot)
"""
from cinder import volume
self.volume_api = volume.API()
"""

cinder/volume/api.py
class API(base.Base):
    def create_snapshot_force(self, context,
                              volume, name,
                              description, metadata=None):
        result = self._create_snapshot(context, volume, name, description,
                                       True, metadata)
        LOG.info(_LI("Snapshot force create request issued successfully."),
                 resource=result)
        return result

    def create_snapshot(self, context,
                        volume, name, description,
                        metadata=None, cgsnapshot_id=None,
                        group_snapshot_id=None):
        result = self._create_snapshot(context, volume, name, description,
                                       False, metadata, cgsnapshot_id,
                                       group_snapshot_id)
        LOG.info(_LI("Snapshot create request issued successfully."),
                 resource=result)
        return result

    def _create_snapshot(self, context,
                         volume, name, description,
                         force=False, metadata=None,
                         cgsnapshot_id=None,
                         group_snapshot_id=None):
        #保证卷操作处于冻结状态,并且是可进行快照,检查配额是否可用
        volume.assert_not_frozen()
        #在cinder的snapshot数据表中创建一条快照记录,即会在云硬盘快照面板显示一条名为“snapshot for snapshot1”的记录
        snapshot = self.create_snapshot_in_db(----s2.3
            context, volume, name,
            description, force, metadata, cgsnapshot_id,
            True, group_snapshot_id)
        # 调用rpc.case将create_snapshot的消息投递到消息队列该消息
        self.volume_rpcapi.create_snapshot(context, volume, snapshot)---s2.4 给cinder-volume发送rpc请求信息
        return snapshot    

可以看到两个方法都是调用了“ _create_snapshot ”,只是在传递第5个参数 force 时不一样,同时force为False时,
需要传递其他几个参数(实际上也为空)

cinder-api的操作总结为如下两个方面:
1)卷状态条件检查及配额检查
2)创建glance数据库快照记录(记录的是单个卷快照的信息)

3、cinder-volume服务对快照的处理

s2.4步中,cinder-api通过rpc给cinder-volume服务发送创建快照的请求,cinder-volume服务接受到请求,并处理,其函数入口为manager.py文件中create_snapshot方法

cinder/volume/manager.py
class VolumeManager(manager.CleanableManager,
                    manager.SchedulerDependentManager):
    """Manages attachable block storage devices."""
    @objects.Snapshot.set_workers
    def create_snapshot(self, context, snapshot):
        """Creates and exports the snapshot."""
        # 获取请求上下文
        context = context.elevated()
         # 通过消息队列,通知ceilometer快照发生变化
        self._notify_about_snapshot_usage(
            context, snapshot, "create.start")

        try:
        """异常处理代码,有任何异常则退出并设置快照状态为error"""
            # NOTE(flaper87): Verify the driver is enabled
            # before going forward. The exception will be caught
            # and the snapshot status updated.
            # 确保存储驱动已经初始化,否则抛出异常
            utils.require_driver_initialized(self.driver)

            # Pass context so that drivers that want to use it, can,
            # but it is not a requirement for all drivers.
            snapshot.context = context
            # 调用后端存储驱动执行快照
            model_update = self.driver.create_snapshot(snapshot)---------s3.1# 完成之后,更新数据库条目,若返回的是None,则不执行
            if model_update:
                snapshot.update(model_update)
                snapshot.save()
        except Exception as error:
        # 若之前几步操作出现问题,则将快照的状态置为error
            with excutils.save_and_reraise_exception():
                snapshot.status = fields.SnapshotStatus.ERROR
                snapshot.save()

                self.db.snapshot_metadata_update(
                    context,
                    snapshot.id,
                    {'error': six.text_type(error)},
                    False)
        # 从cinder的数据库中获取卷的信息
        vol_ref = self.db.volume_get(context, snapshot.volume_id)
        # 如果该卷的bootable属性为True,表示该卷是启动卷,表示云主机是通过卷启动的,即系统盘,
        # 如果是非启动卷,则跳过
        if vol_ref.bootable:
            try:
             # 用卷的metadata信息来更新snapshot的metadata信息,需要保证系统盘的元数据与其快照的元数据一致
                self.db.volume_glance_metadata_copy_to_snapshot(
                    context, snapshot.id, snapshot.volume_id)
            except exception.GlanceMetadataNotFound:
                # 更新snapshot的元数据如果抛出GlanceMetadataNotFound,
                # 表示从glance中找不到卷的元数据信息,可以直接跳过
                # If volume is not created from image, No glance metadata
                # would be available for that volume in
                # volume glance metadata table
                pass
            except exception.CinderException as ex:
                LOG.exception(_LE("Failed updating snapshot"
                                  " metadata using the provided volumes"
                                  " %(volume_id)s metadata"),
                              {'volume_id': snapshot.volume_id},
                              resource=snapshot)
                # 如果抛出cinder方面的异常,则有可能是快照出现问题,则直接将快照的状态置为error
                snapshot.status = fields.SnapshotStatus.ERROR
                snapshot.save()

                self.db.snapshot_metadata_update(context,
                                                 snapshot.id,
                                                 {'error': six.text_type(ex)},
                                                 False)
                raise exception.MetadataCopyFailure(reason=six.text_type(ex))
        # 若一路过来没有出现异常,则代表快照完成,将快照状态标记为可用,进度为100%,并保存状态
        snapshot.status = fields.SnapshotStatus.AVAILABLE
        snapshot.progress = '100%'
        snapshot.save()
        # 通过消息队列,通知ceilometer快照完成
        self._notify_about_snapshot_usage(context, snapshot, "create.end")
        LOG.info(_LI("Create snapshot completed successfully"),
                 resource=snapshot)
        return snapshot.id

从上面的代码中可以找到,执行快照其实是调用底层的后端存储来做的,即s3.1 步的“driver.create_snapshot(snapshot)”,
针对不同的存储类型,会有不同的处理方式
因此cinder-volume服务快照功能很简单:调用后端存储执行快照,然后更新glance数据库快照记录

学习过程中,参考如下博客 https://www.cnblogs.com/qianyeliange/p/9713146.html

原文地址:https://www.cnblogs.com/potato-chip/p/11226831.html