基于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