zoukankan      html  css  js  c++  java
  • OpenStack快照分析:(三)从磁盘启动云主机离线(在线)快照分析

    1.                磁盘启动云主机,离线(在线)快照

    1.1.         nova-api处理过程

    磁盘启动的云主机在做离线快照时,还是首先是nova-api接收请求,函数入口和前述一样,还是 nova/api/openstack/compute/servers.py/ServersController._action_create_image下面一起来看看:

    @wsgi.response(202)
    @
    extensions.expected_errors((400, 403, 404, 409))
    # 定义关联的API接口
    @
    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', {})
        snapshot_id = entity.get(
    "snapshot_id", None)

       
    # 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)
       
        instance =
    self._get_server(context, req, id)

        snapshot = snapshot_current(context
    , instance, self.compute_rpcapi)
       
    if snapshot:  # if there are snapshots, then create an image with snashots.
           
    if not snapshot_id:
                snapshot_id = snapshot[
    "id"]
            image = snapshot_create_image(context
    , snapshot_id, instance, self.compute_rpcapi, entity)
       
    else:
           
    #从数据库中获取实例对象(InstanceV2)及块设备映射列表
            bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context
    , instance.uuid)

           
    # 判断实例是镜像启动还是磁盘启动
           
    if compute_utils.is_volume_backed_instance(context, instance, bdms):
               
    # 校验执行权限
                context.can(server_policies.SERVERS %
                           
    'create_image:allow_volume_backed')
        
           # 这里执行的是磁盘启动方式的快照,传递的参数包括:
                # 1
    、关于权限的上下文context
                # 2
    、虚拟机的实例对象instance
                # 3
    、快照的名称image_name
                #
    本章节讲的磁盘启动的云主机快照就是进入该分支进行操作。
                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)

       
    if api_version_request.is_supported(req, '2.45'):
           
    return {'image_id': image['id']}

       
    # build location of newly-created image entity
       
    image_id = str(image['id'])
        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/API.snapshot_volume_backed

    @check_instance_state(vm_state=[vm_states.ACTIVE, vm_states.STOPPED, vm_states.SUSPENDED])
    def snapshot_volume_backed(self, context, instance, name, extra_properties=None):
       
    """Snapshot the given volume-backed instance.从实例的system_metadata生成镜像属性(排除不可继承属性),如下:
          
    # {
           #     u'min_disk': u'20',
           #     'is_public': False,
           #     'min_ram': u'0',
           #     'properties': {
           #         'base_image_ref': u''
           #      },
           #     'name': u'snapshot1'
           # }

        :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)
       
    image_meta['size'] = 0
        
    #
    清除镜像metadata属性中的container_formatdisk_forma属性
       
    for attr in ('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

       
    # 从数据库中获取该云主机所关联的所有块设备,结果会返回一个BlockDeviceMappingList对象
        bdms = objects.BlockDeviceMappingList.get_by_instance_uuid(context
    , instance.uuid)
       
    # 接下来开始做快照的操作,注意,云主机挂在了多少个卷设备,就要做多少次快照
        mapping = [] 
    # list of BDM dicts that can go into the image properties
        # Do some up-front filtering of the list of BDMs from
        # which we are going to create snapshots.
       
    volume_bdms = []
       
    for bdm in bdms:
           
    if bdm.no_device:
               
    # 映射关系中没有块设备,则忽略此条映射
               
    continue
            if
    bdm.is_volume:
               
    # These will be handled below.此映射包含块设备,加入到volume_bdms,准备做快照
               
    volume_bdms.append(bdm)
           
    else:
                mapping.append(bdm.get_image_mapping())

       
    # Check limits in Cinder before creating snapshots to avoid going over
        # quota in the middle of a list of volumes. This is a best-effort check
        # but concurrently running snapshot requests from the same project
        # could still fail to create volume snapshots if they go over limit.
       
    # 在创建快照之前,需要首先在Cinder中检查配额限制,以避免超过配额限制
       
    if volume_bdms:
            limits =
    self.volume_api.get_absolute_limits(context)
            total_snapshots_used = limits[
    'totalSnapshotsUsed']
            max_snapshots = limits[
    'maxTotalSnapshots']
           
    # -1 means there is unlimited quota for snapshots
           
    if (max_snapshots > -1 and
                   
    len(volume_bdms) + total_snapshots_used > max_snapshots):
                LOG.debug(
    'Unable to create volume snapshots for instance. '
                          'Currently has %s snapshots, requesting %s new '
                          'snapshots, with a limit of %s.'
    ,
                         
    total_snapshots_used, len(volume_bdms),
                         
    max_snapshots, instance=instance)
               
    raise exception.OverQuota(overs='snapshots')

        quiesced =
    False
       
    # 判断虚拟机的状态,如果虚拟机处于active,则通过rpc通知虚拟机进入静默状态(异常处理省略
       
    if instance.vm_state == vm_states.ACTIVE:
            LOG.info(
    "Attempting to quiesce instance before volume "
                     "snapshot."
    , instance=instance)
           
    self.compute_rpcapi.quiesce_instance(context, instance)
            quiesced =
    True

       
    # 定义一个获取云主机上的及具体卷信息的方法,返回云主机的卷映射
        @
    wrap_instance_event(prefix='api')
       
    def snapshot_instance(self, context, instance, bdms):
           
    for bdm in volume_bdms:
                
    # create snapshot based on volume_id
               
    # 根据卷的volume_id从数据库获取卷的详细信息
                
    volume = self.volume_api.get(context, bdm.volume_id)
               
    # 组装出一个貌似是desc的消息,比如快照名称是snapshot1,则这里就是snapshot for snapshot1
                
    name = _('snapshot for %s') % image_meta['name']
                LOG.debug(
    'Creating snapshot from volume %s.', volume['id'], instance=instance)
               
    # 调用cinderapi create_snapshot_force创建新的卷
                #
    “create_snapshot_force”中实际上是通过cinderclient来调用volume_snapshots.create
                #
    来发起创建卷的请求,具体是由cinder-volume来完成卷的快照,返回的内容为卷快照的信息,
                #
    格式如:
                #{
                #  'status': u'creating',
                #  'display_name': u'snapshot for snapshot1',
                #  'created_at': u'2016-06-24T09:23:00.517279',
                #  'display_description': u'',
                #  'volume_size': 20,
                #  'volume_id': u'60e16af2-0684-433c-a1b6-c1af1c2523fc',
                #  'progress': None,
                #  'project_id': u'25520b29dce346d38bc4b055c5ffbfcb',
                #  'id': u'cede2421-ea68-4a8e-937d-c27074b9024b',
                #  'size': 20
                # }

                snapshot = self.volume_api.create_snapshot_force(
                    context
    , volume['id'], name, volume['display_description'])
               
    # 接着会根据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
                # }
                mapping_dict = block_device.snapshot_from_bdm(snapshot['id'], bdm)
               
    # 过滤掉已经在数据库中存在的字段
                mapping_dict = mapping_dict.get_image_mapping()
               
    # 将云主机所有的映射关系都添加到mapping
                mapping.append(mapping_dict)
            
    return mapping

       
    self._record_action_start(context, instance, instance_actions.CREATE_IMAGE)
       
    # 调用“snapshot_instance”,获取云主机所有的mapping关系
        mapping = snapshot_instance(
    self, context, instance, bdms)

       
    # 如果此时卷的文件系统已静默,这里则进行解冻处理,实现过程就是通过rpc.case发送异步请求给nova-compute
        # nova-compute
    接收到消息后,会等到快照完成后对文件系统进行解冻(需要agent支持)
       
    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)

    最后一步通过调用image_apicreate来创建image,跟踪过去实际上就只有一句session.create(context, image_info, data=data),调用RESTful API来创建image

    至此,nova-api的工作已经做完,总结一下,nov-api主要是 完成了一下工作:

    l  如果是在线快照,则冻结/解冻结文件系统

    l  创建glance数据库镜像记录(包含所有卷的快照信息)

    1.2.         cinder创建磁盘快照

    1.2.1.  cinder-api处理过程

    上节中讲到在创建卷快照的时候,nova-api在处理请求时,使用cinderclient调用volume_apicinder-api)来通过http方式发送快照的请求,cinder-api会接受该请求,处理代码如下:

    @wsgi.response(http_client.ACCEPTED)
    @
    validation.schema(snapshot.create)
    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']
        snapshot = body[
    'snapshot']
       
    # 获取快照的metadata信息,snapshot_id
        kwargs[
    'metadata'] = snapshot.get('metadata', None)
        volume_id = snapshot[
    'volume_id']
       
    # 从数据库中获取卷信息
        volume =
    self.volume_api.get(context, volume_id)
       
    # 这里是获取传递进来的参数中是否使用强制快照,force=True表示采取强制快照
        force = snapshot.get(
    'force', False)
       
    # 参数类型转换,如果是非True/False的值,则抛异常
        force = strutils.bool_from_string(force
    , strict=True)
        LOG.info(
    "Create snapshot from volume %s", volume_id)
       
    # 验证快照名及快照描述是否合法,长度不能超过256个字符
       
    self.validate_name_and_description(snapshot, check_length=False)
       
    # NOTE(thingee): v2 API allows name instead of display_name
       
    # display_name代替name参数
       
    if 'name' in snapshot:
            snapshot[
    'display_name'] = snapshot.pop('name')
       
    # 开始进行快照的操作,根据force值得不同走不通的分支,其实都是对_create_snapshot的封装
       
    if force:
            new_snapshot =
    self.volume_api.create_snapshot_force(
                context
    ,
               
    volume,
               
    snapshot.get('display_name'),
               
    snapshot.get('description'),
               
    **kwargs)
       
    else:
            new_snapshot =
    self.volume_api.create_snapshot(
                context
    ,
               
    volume,
               
    snapshot.get('display_name'),
               
    snapshot.get('description'),
               
    **kwargs)
        req.cache_db_snapshot(new_snapshot)

       
    return self._view_builder.detail(req, new_snapshot)

    cindervolumeapi.pyAPI类中,有这两个方法:

    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(
    "Snapshot create request issued successfully.", resource=result)
       
    return result

    def create_snapshot_force(self, context, volume, name, description, metadata=None):
        result =
    self._create_snapshot(context, volume, name, description, True, metadata)
        LOG.info(
    "Snapshot force create request issued successfully.", resource=result)
       
    return result

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

    def _create_snapshot(self, context, volume, name, description, force=False, metadata=None,
                        
    cgsnapshot_id=None, group_snapshot_id=None):
        """
    根据上文的分析:force = True

       
    该方法完成如下功能:
        1.
    执行卷状态条件判断,如果卷处于维护状态,迁移过程中,副本卷,
           force=False
    且不是可用状态,则抛异常
        2.
    执行用户快照配额管理,用户可以为不同的卷类型设置不同的配额信息,如:
           volumes, gigabytes,snapshots,
    我这里使用的是ceph rbd,例如:
             {
                 'gigabytes': 20,
                 'snapshots_ceph': 1,
                 'gigabytes_ceph': 20,
                 'snapshots': 1
             }
         
    用户默认配额如下:
             {
                 'gigabytes': 1000,
                 'snapshots_ceph': -1,
                 'snapshots': 10,
                 'gigabytes_ceph': -1
             }
          
    如果配额不足则会抛异常
        3.
    创建快照条目,例如(创建卷快照要先在cinder数据库创建snapshot数据库条目):
          {
             'status': u'creating',
             'volume_type_id': 'd494e240-17b3-4d35-a5a1-2923d8677d79',
             'display_name': u'snapshot for snapshot1',
             'user_id': 'b652f9bd65844f739684a20ed77e9a0f',
             'display_description': u'',
             'cgsnapshot_id': None,
             'volume_size': 20,
             'encryption_key_id': None,
             'volume_id': '60e16af2-0684-433c-a1b6-c1af1c2523fc',
             'progress': u'0%',
             'project_id': '25520b29dce346d38bc4b055c5ffbfcb',
             'metadata': {}
          }
          
    卷快照完成后,会在Dashboard的云硬盘快照面板显示一条名为'snapshot for snapshot1'的卷快照记录
        """
        # 我理解这一步应该是继续保证卷操作处于冻结状态,并且是可进行快照,检查配额是否可用
        volume.assert_not_frozen()
     
      # cindersnapshot数据表中创建一条快照记录,即会在云硬盘快照面板显示一条名为“snapshot for snapshot1”的记录
        snapshot =
    self.create_snapshot_in_db(
            context
    , volume, name, description, force, metadata, cgsnapshot_id,
           
    True, group_snapshot_id)
       
    kwargs = {'snapshot_id': snapshot.id,
                 
    'volume_properties': objects.VolumeProperties(size=volume.size)}
       
    # 调用rpc.casecreate_snapshot的消息投递到消息队列该消息
       
    self.scheduler_rpcapi.create_snapshot(context, volume, snapshot,
                                             
    volume.service_topic_queue,
                                             
    objects.RequestSpec(**kwargs))
       
    return snapshot

    至此,cinder-api的处理结束!

    小结:卷快照过程中,cinder-api的操作总结为如下两个方面:

    l  卷状态条件检查及配额检查

    l  创建glance数据库快照记录(记录的是单个卷快照的信息)

    1.2.2.  Cinder-volume的处理过程

    cinder-volume从消息队列接收到来自cinder-api的创建快照的请求消息后,cinder-volume就会调用其VolumeManager.create_snapshot方法进行处理,代码位置:cinder/volume/manager.py,如下:

    @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"""
           
    # 确保存储驱动已经初始化,否则抛出异常
           
    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

           
    # 调用后端存储驱动执行快照,例如使用RBDDriver,下文具体分析
            model_update =
    self.driver.create_snapshot(snapshot)
           
    # 完成之后,更新数据库条目,若返回的是None,则不执行
           
    if model_update:
                snapshot.update(model_update)
                snapshot.save()

       
    except Exception:
           
    # 若之前几步操作出现问题,则将快照的状态置为error
           
    with excutils.save_and_reraise_exception():
                snapshot.status = fields.SnapshotStatus.ERROR
                snapshot.save()

       
    # cinder的数据库中获取卷的信息
        vol_ref =
    self.db.volume_get(context, snapshot.volume_id)
        # 如果该卷的bootable属性为True,表示该卷是启动卷,表示云主机是通过卷启动的,即系统盘,
        #
    如果是非启动卷,则跳过
       
    if vol_ref.bootable:
           
    try:
               
    # 用卷的metadata信息来更新snapshotmetadata信息,需要保证系统盘的元数据与其快照的元数据一致
               
    self.db.volume_glance_metadata_copy_to_snapshot(context, snapshot.id, snapshot.volume_id)
           
    except exception.GlanceMetadataNotFound:
              
     # 更新snapshot的元数据如果抛出GlanceMetadataNotFound
                #
    表示从glance中找不到卷的元数据信息,可以直接跳过
               
    pass
            except
    exception.CinderException as ex:
                LOG.exception(
    "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()
               
    raise exception.MetadataCopyFailure(reason=six.text_type(ex))

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

    从上面的代码中可以找到,执行快照其实是调用底层的后端存储来做的,即“driver.create_snapshot(snapshot)”,针对不同的存储类型,会有不同的处理方式,这也就是OpenStack的一个设计理念,只提供一个框架,具体功能的实现则是交给对应的provider来做,只要你提供的功能的调用符合OpenStack的接口标准便可以。

    IDE中点击进入driver.create_snapshot方法,会出现以下选择框:

     

    可以看出,cinder-volume提供有很多的后端存储驱动,比如EMCHPESVC等,查看整个驱动目录,支持:

           

    比如,我们使用ceph作为后端存储,这时候就会使用RBD的驱动,快照调用的方法为:

    def create_snapshot(self, snapshot):
       
    """Creates an rbd snapshot."""
       
    with RBDVolumeProxy(self, snapshot.volume_name) as volume:
            snap = utils.convert_str(snapshot.name)
            volume.create_snap(snap)
            volume.protect_snap(snap)

    其过程也就是创建一个Image对象,然后直接调用librbd相关的方法执行秒级快照,不做具体分析了。

    小结:cinder-volume快照功能很简单:调用后端存储执行快照,然后更新glance数据库快照记录

    阅读完上面的分析,相信读者会发现上面的快照过程中cinder执行的就是卷的快照,nova实现的是云主机信息及其镜像记录的处理。事实确实也如此:快照执行完成后,会在Dashboard的镜像面板显示一条镜像记录,在卷快照面板显示一条或者多条(如果有多个卷的话)卷快照记录。

  • 相关阅读:
    关于XCode5打开工程闪退的一种解决方案
    【转】iOS应用崩溃日志揭秘
    Cocos2d-X中字符串的处理
    【转】不要把大脑当做磁盘
    【ybt金牌导航6-3-2】区间计数(分块)(二分)
    【luogu P3807】【模板】卢卡斯定理/Lucas 定理(含 Lucas 定理证明)
    【ybt金牌导航6-2-2】【luogu CF600E】树上众数 / Lomsat gelral(树上启发式合并)
    【ybt金牌导航6-1-5】最大割(线段树分治)(线性基)
    【luogu CF1100F】Ivan and Burgers
    【ybt金牌导航8-1-4】【luogu P4151】路径最大异或和 / 最大XOR和路径
  • 原文地址:https://www.cnblogs.com/qianyeliange/p/9713146.html
Copyright © 2011-2022 走看看