基於鏡像卷啓動的虛機快照代碼分析

基於ocata版本進行跟蹤分析html

一、nova-api接受並處理請求的入口函數nova/api/openstack/compute/servers.py/ServersController._action_create_imageweb

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表裏面的數據樣例api

 *************************** 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)app

根據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數據庫鏡像記錄(包含全部卷的快照信息)異步

二、cinder-api服務的相關處理ide

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數據庫快照記錄(記錄的是單個卷快照的信息)

三、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

相關文章
相關標籤/搜索