neutron-server加载neutron_lbaas service plugins

 

Table of Contents

0. systemctl status neutron-server:获取neutron-server 脚本入口... 2

1. /usr/bin/neutron-server:Main 函数入口... 2

2. cmd/eventlet/server/__init__.py:main函数实现... 3

3. server/wsgi_eventlet.py:wsgi_eventlet.eventlet_wsgi_server 函数实现... 3

4. service.py:serve_wsgi的实现... 4

4. service.py:NeutronApiService实现... 4

5. common/config.py:config.load_paste_app实现... 6

6. ../oslo_service/wsgi.py:wsgi.Loader.load_app的实现... 6

7. /usr/share/neutron/neutron-dist.conf:paste.deploy形式加载neutron的应用... 8

8. api/v2/router.py:APIRouter.factory实现... 10

9. pecan_wsgi/app.py:v2_factory实现... 10

10. manager.py:NeutronManager实现... 11

单步调试断点设置... 15

 

 

理解此加载过程需要预先具备以下几点知识:

 

* wsgi基本概念

* paste.deploy应用加载方式

* python pecan应用框架

 

0. systemctl status neutron-server:获取neutron-server 脚本入口

 

systemctl status neutron-server 中获取neutron-server脚本入口

 

● neutron-server.service - OpenStack Neutron Server

   Loaded: loaded (/usr/lib/systemd/system/neutron-server.service; enabled; vendor preset: disabled)

   Active: active (running) since Mon 2021-02-15 21:59:30 PST; 4h 18min ago

 Main PID: 19888 (neutron-server)

    Tasks: 11

   Memory: 1.0G

   CGroup: /system.slice/neutron-server.service

           ├─19888 /usr/bin/python2 /usr/bin/neutron-server --config-file /usr/share/neutron/neutron-dist.conf --config-dir /usr/sh...

。。。

Hint: Some lines were ellipsized, use -l to show in full

 

1. /usr/bin/neutron-server:Main 函数入口

 

    #!/usr/bin/python2

    # PBR Generated from u'console_scripts'

 

    import sys

 

    from neutron.cmd.eventlet.server import main

 

    if __name__ == "__main__":

        sys.exit(main())

 

以下调试均在/usr/lib/python2.7/site-packages/neutron目录下。

 

2. cmd/eventlet/server/__init__.py:main函数实现

 

    from neutron import server

    from neutron.server import rpc_eventlet

    from neutron.server import wsgi_eventlet

 

    def main():

        server.boot_server(wsgi_eventlet.eventlet_wsgi_server)

 

boot_server: 在做了两件事:

 

    初始化配置文件, 此时暂时不展开。

    执行传入的参数(函数)。

 

3. server/wsgi_eventlet.py:wsgi_eventlet.eventlet_wsgi_server 函数实现

 

    def eventlet_wsgi_server():

        neutron_api = service.serve_wsgi(service.NeutronApiService)

        start_api_and_rpc_workers(neutron_api)

 

eventlet_wsgi_server函数中先是准备好wsgi服务相关要素,然后通过eventlet的方式启动。

 

此处我们主要探究wsgi服务准备部分:

 

    neutron_api = service.serve_wsgi(service.NeutronApiService)

 

4. service.py:serve_wsgi的实现

 

    def serve_wsgi(cls):

 

        try:

            service = cls.create()

            service.start()

        except Exception:

            with excutils.save_and_reraise_exception():

                LOG.exception('Unrecoverable error: please check log '

                            'for details.')

 

        registry.notify(resources.PROCESS, events.BEFORE_SPAWN, service)

        return service

 

调用传入的Service构造函数

启动服务:start

 

4. service.py:NeutronApiService实现

 

    class NeutronApiService(WsgiService):

        """Class for neutron-api service."""

        def __init__(self, app_name):

            profiler.setup('neutron-server', cfg.CONF.host)

            super(NeutronApiService, self).__init__(app_name)

 

        @classmethod

        def create(cls, app_name='neutron'):

            # Setup logging early

            config.setup_logging()

            service = cls(app_name)

            return service

 

NeutronApiService继承自WsgiService

 

    class WsgiService(object):

        """Base class for WSGI based services.

 

        For each api you define, you must also define these flags:

        :<api>_listen: The address on which to listen

        :<api>_listen_port: The port on which to listen

 

        """

 

        def __init__(self, app_name):

            self.app_name = app_name

            self.wsgi_app = None

 

        def start(self):

            self.wsgi_app = _run_wsgi(self.app_name)

 

        def wait(self):

            self.wsgi_app.wait()

 

其中 _run_wsgi实现如下:

    def _run_wsgi(app_name):

        app = config.load_paste_app(app_name)

        if not app:

            LOG.error('No known API applications configured.')

            return

        return run_wsgi_app(app)

 

    def run_wsgi_app(app):

        server = wsgi.Server("Neutron")

        server.start(app, cfg.CONF.bind_port, cfg.CONF.bind_host,

                    workers=_get_api_workers())

        LOG.info("Neutron service started, listening on %(host)s:%(port)s",

                {'host': cfg.CONF.bind_host, 'port': cfg.CONF.bind_port})

        return server

 

config.load_paste_app(app_name) 通过paste.deploy的方式加载neutron服务。

run_wsgi_app启动一个一个web server

 

5. common/config.py:config.load_paste_app实现

 

    def load_paste_app(app_name):

        """Builds and returns a WSGI app from a paste config file.

 

        :param app_name: Name of the application to load

        """

        loader = wsgi.Loader(cfg.CONF)

        app = loader.load_app(app_name)

        return app

 

6. ../oslo_service/wsgi.py:wsgi.Loader.load_app的实现

 

    class Loader(object):

        """Used to load WSGI applications from paste configurations."""

 

        def __init__(self, conf):

            """Initialize the loader, and attempt to find the config.

 

            :param conf: Application config

            :returns: None

 

            """

            conf.register_opts(_options.wsgi_opts)

            self.config_path = None

 

            config_path = conf.api_paste_config

            if not os.path.isabs(config_path):

                self.config_path = conf.find_file(config_path)

            elif os.path.exists(config_path):

                self.config_path = config_path

 

            if not self.config_path:

                raise ConfigNotFound(path=config_path)

 

        def load_app(self, name):

            """Return the paste URLMap wrapped WSGI application.

 

            :param name: Name of the application to load.

            :returns: Paste URLMap object wrapping the requested application.

            :raises: PasteAppNotFound

 

            """

            try:

                LOG.debug("Loading app %(name)s from %(path)s",

                        {'name': name, 'path': self.config_path})

                return deploy.loadapp("config:%s" % self.config_path, name=name)

            except LookupError:

                LOG.exception("Couldn't lookup app: %s", name)

                raise PasteAppNotFound(name=name, path=self.config_path)

 

Loader初始化函数中,以传入的api_paste_config为配置路径加载相应应用实现。

回到最初systemctl status neutron-server --full命令的输出,

api_paste_config在配置文件列表中的/usr/share/neutron/neutron-dist.conf

 

7. /usr/share/neutron/neutron-dist.conf:paste.deploy形式加载neutron的应用

 

配置项为:

 

    api_paste_config = /usr/share/neutron/api-paste.ini

 

内容如下:

 

    [composite:neutron]

    use = egg:Paste#urlmap

    /: neutronversions_composite

    /v2.0: neutronapi_v2_0

 

    [composite:neutronapi_v2_0]

    use = call:neutron.auth:pipeline_factory

    noauth = cors http_proxy_to_wsgi request_id catch_errors extensions neutronapiapp_v2_0

    keystone = cors http_proxy_to_wsgi request_id catch_errors authtoken keystonecontext extensions neutronapiapp_v2_0

 

    [composite:neutronversions_composite]

    use = call:neutron.auth:pipeline_factory

    noauth = cors http_proxy_to_wsgi neutronversions

    keystone = cors http_proxy_to_wsgi neutronversions

 

    [filter:request_id]

    paste.filter_factory = oslo_middleware:RequestId.factory

 

    [filter:catch_errors]

    paste.filter_factory = oslo_middleware:CatchErrors.factory

 

    [filter:cors]

    paste.filter_factory = oslo_middleware.cors:filter_factory

    oslo_config_project = neutron

 

    [filter:http_proxy_to_wsgi]

    paste.filter_factory = oslo_middleware.http_proxy_to_wsgi:HTTPProxyToWSGI.factory

 

    [filter:keystonecontext]

    paste.filter_factory = neutron.auth:NeutronKeystoneContext.factory

 

    [filter:authtoken]

    paste.filter_factory = keystonemiddleware.auth_token:filter_factory

 

    [filter:extensions]

    paste.filter_factory = neutron.api.extensions:plugin_aware_extension_middleware_factory

 

    [app:neutronversions]

    paste.app_factory = neutron.api.versions:Versions.factory

 

    [app:neutronapiapp_v2_0]

    paste.app_factory = neutron.api.v2.router:APIRouter.factory

 

    [filter:osprofiler]

    paste.filter_factory = osprofiler.web:WsgiMiddleware.factory

 

这里我们需要理解 paste.deploy配置文件的具体含义:

 

* composite:多个子项的组合,向外提供新的应用。

* filter:增加新的逻辑处理。

* app:指明此处声明的是一个实际应用

 

可以看到neutronapi_v2_0应用是由多个子处理过程包装而成的。

提供了两种应用模式:noauth keystone,模式的选择配置为

 

    /etc/neutron/neutron.conf:3:auth_strategy = keystone

 

这里我们仅关注:

 

    [app:neutronapiapp_v2_0]

    paste.app_factory = neutron.api.v2.router:APIRouter.factory

 

8. api/v2/router.py:APIRouter.factory实现

 

    class APIRouter(base_wsgi.Router):

 

        @classmethod

        def factory(cls, global_config, **local_config):

            if cfg.CONF.web_framework == 'pecan':

                return pecan_app.v2_factory(global_config, **local_config)

            return cls(**local_config)

 

factory函数声明格式是paste.deploy的标准形式。

* global_config中保存了来自上游的全局配置项CONF.xxx,

* local_config则保存了新增配置项,例如上边的"noauth", "keystone"都保存在local_config中。

 

函数的实现中,我们可以看到应用的框架使用pecan,这里需要了解pythonpecan应用框架。

此框架所解决的问题依旧是如何将请求uri解析、路由到具体的应用。

 

9. pecan_wsgi/app.py:v2_factory实现

 

    def v2_factory(global_config, **local_config):

        # Processing Order:

        #   As request enters lower priority called before higher.

        #   Reponse from controller is passed from higher priority to lower.

        app_hooks = [

            hooks.UserFilterHook(),  # priority 90

            hooks.ContextHook(),  # priority 95

            hooks.ExceptionTranslationHook(),  # priority 100

            hooks.BodyValidationHook(),  # priority 120

            hooks.OwnershipValidationHook(),  # priority 125

            hooks.QuotaEnforcementHook(),  # priority 130

            hooks.NotifierHook(),  # priority 135

            hooks.QueryParametersHook(),  # priority 139

            hooks.PolicyHook(),  # priority 140

        ]

        app = pecan.make_app(root.V2Controller(),

                            debug=False,

                            force_canonical=False,

                            hooks=app_hooks,

                            guess_content_type_from_ext=True)

        startup.initialize_all()

        return app

 

此函数实现主要完成了neutron核心模块及api映射关系的加载过程,包括核心的neutron资源:

network subnet port subnet pool.

 

    api/v2/attributes.py:

    CORE_RESOURCES = {net_def.RESOURCE_NAME: net_def.COLLECTION_NAME,

                    subnet_def.RESOURCE_NAME: subnet_def.COLLECTION_NAME,

                    subnetpool_def.RESOURCE_NAME: subnetpool_def.COLLECTION_NAME,

                    port_def.RESOURCE_NAME: port_def.COLLECTION_NAME}

 

关键api的映射是通过pecan controller router的的方式(相对于传统的wsgi路由对象的方式)。

V2Controllerstartup.initialize_all中均会用到一个单例的NeutronManager

V2Controller中并没有对NeutronManager实例化,该实例化发生在startup.initialize_all中。

NeutronManager初始化函数中,存在加载extension plugins的逻辑。

 

10. manager.py:NeutronManager实现

 

    @six.add_metaclass(profiler.TracedMeta)

    class NeutronManager(object):

        """Neutron's Manager class.

 

        Neutron's Manager class is responsible for parsing a config file and

        instantiating the correct plugin that concretely implements

        neutron_plugin_base class.

        """

        # TODO(armax): use of the singleton pattern for this class is vestigial,

        # and it is mainly relied on by the unit tests. It is safer to get rid

        # of it once the entire codebase (neutron + subprojects) has switched

        # entirely to using the plugins directory.

        _instance = None

        __trace_args__ = {"name": "rpc"}

 

        def __init__(self, options=None, config_file=None):

            # If no options have been provided, create an empty dict

            if not options:

                options = {}

 

            msg = validate_pre_plugin_load()

            if msg:

                LOG.critical(msg)

                raise Exception(msg)

 

            # NOTE(jkoelker) Testing for the subclass with the __subclasshook__

            #                breaks tach monitoring. It has been removed

            #                intentionally to allow v2 plugins to be monitored

            #                for performance metrics.

            plugin_provider = cfg.CONF.core_plugin

            LOG.info("Loading core plugin: %s", plugin_provider)

            # NOTE(armax): keep hold of the actual plugin object

            plugin = self._get_plugin_instance(CORE_PLUGINS_NAMESPACE,

                                            plugin_provider)

            directory.add_plugin(lib_const.CORE, plugin)

            msg = validate_post_plugin_load()

            if msg:

                LOG.critical(msg)

                raise Exception(msg)

 

            # load services from the core plugin first

            self._load_services_from_core_plugin(plugin)

            self._load_service_plugins()

            # Used by pecan WSGI

            self.resource_plugin_mappings = {}

            self.resource_controller_mappings = {}

            self.path_prefix_resource_mappings = defaultdict(list)

 

        def _load_service_plugins(self):

            """Loads service plugins.

 

            Starts from the core plugin and checks if it supports

            advanced services then loads classes provided in configuration.

            """

            plugin_providers = cfg.CONF.service_plugins

            plugin_providers.extend(self._get_default_service_plugins())

            LOG.debug("Loading service plugins: %s", plugin_providers)

            for provider in plugin_providers:

                if provider == '':

                    continue

 

                LOG.info("Loading Plugin: %s", provider)

                plugin_inst = self._get_plugin_instance('neutron.service_plugins',

                                                        provider)

 

                # only one implementation of svc_type allowed

                # specifying more than one plugin

                # for the same type is a fatal exception

                # TODO(armax): simplify this by moving the conditional into the

                # directory itself.

                plugin_type = plugin_inst.get_plugin_type()

                if directory.get_plugin(plugin_type):

                    raise ValueError(_("Multiple plugins for service "

                                    "%s were configured") % plugin_type)

 

                directory.add_plugin(plugin_type, plugin_inst)

 

                # search for possible agent notifiers declared in service plugin

                # (needed by agent management extension)

                plugin = directory.get_plugin()

                if (hasattr(plugin, 'agent_notifiers') and

                        hasattr(plugin_inst, 'agent_notifiers')):

                    plugin.agent_notifiers.update(plugin_inst.agent_notifiers)

 

                LOG.debug("Successfully loaded %(type)s plugin. "

                        "Description: %(desc)s",

                        {"type": plugin_type,

                        "desc": plugin_inst.get_plugin_description()})

 

_load_service_plugins函数中,通过读取service_plugins得到extension plugin的列表(模块路径)。

plugin_providers = cfg.CONF.service_plugins

 

此配置在/etc/neutron/neutron.conf或者/etc/neutron/neutron_lbaas.conf中:

 

    [DEFAULT]

    service_plugins = neutron_lbaas.services.loadbalancer.plugin.LoadBalancerPluginv2,router,metering

 

然后根据service_provider得到plugin_inst

 

            LOG.info("Loading Plugin: %s", provider)

            plugin_inst = self._get_plugin_instance('neutron.service_plugins',

                                                    provider)

 

    [service_providers]

    service_provider = LOADBALANCERV2:F5Networks:neutron_lbaas.drivers.f5.driver_v2.F5LBaaSV2Driver:default

 

细节的追踪为:此处更适合单步跟踪调试

    _get_plugin_instance

        -> load_class_for_provider

            -> utils.load_class_by_alias_or_classname

                -> stevedore/driver.py: driver.DriverManager

                    -> stevedore/named.py: NamedExtensionManager

                        -> stevedore/extension.py: ExtensionManager._load_plugins

                            -> stevedore/extension.py: ExtensionManager._load_one_plugin

 

单步调试断点设置

 

(Pdb) n

> /usr/lib/python2.7/site-packages/oslo_service/wsgi.py(352)load_app()

-> {'name': name, 'path': self.config_path})

(Pdb) p self.config_path

'/usr/share/neutron/api-paste.ini'

(Pdb) l

347             :raises: PasteAppNotFound

348

349             """

350             try:

351                 LOG.debug("Loading app %(name)s from %(path)s",

352  ->                       {'name': name, 'path': self.config_path})

353                 return deploy.loadapp("config:%s" % self.config_path, name=name)

 

(Pdb) b /usr/lib/python2.7/site-packages/neutron/api/v2/router.py:70

 

    66 class APIRouter(base_wsgi.Router):

    67

    68     @classmethod

    69     def factory(cls, global_config, **local_config):

    70         if cfg.CONF.web_framework == 'pecan':

    71             return pecan_app.v2_factory(global_config, **local_config)

    72         return cls(**local_config)

    73

    74     def __init__(self, **local_config):

    75         mapper = routes_mapper.Mapper()

    76         manager.init()

    77         plugin = directory.get_plugin()

    78         ext_mgr = extensions.PluginAwareExtensionManager.get_instance()

    79         ext_mgr.extend_resources("2.0", attributes.RESOURCE_ATTRIBUTE_MAP)

    80

    81         col_kwargs = dict(collection_actions=COLLECTION_ACTIONS,

    82                           member_actions=MEMBER_ACTIONS)

    83

 

(Pdb) b /usr/lib/python2.7/site-packages/neutron/pecan_wsgi/controllers/root.py:87

 

    84

    85     @utils.expose(generic=True)

    86     def index(self):

    87         if not pecan.request.path_url.endswith('/'):

    88             pecan.abort(404)

    89

    90         layout = []

    91         for name, collection in attributes.CORE_RESOURCES.items():

    92             href = urlparse.urljoin(pecan.request.path_url, collection)

    93             resource = {'name': name,

    94                         'collection': collection,

    95                         'links': [{'rel': 'self',

    96                                    'href': href}]}

    97             layout.append(resource)

    98         return {'resources': layout}

 

(Pdb) b /usr/lib/python2.7/site-packages/neutron/pecan_wsgi/controllers/root.py:114

 

    108     @utils.expose()

    109     def _lookup(self, collection, *remainder):

    110         # if collection exists in the extension to service plugins map then

    111         # we are assuming that collection is the service plugin and

    112         # needs to be remapped.

    113         # Example: https://neutron.endpoint/v2.0/lbaas/loadbalancers

    114         if (remainder and

    115                 manager.NeutronManager.get_resources_for_path_prefix(

    116                     collection)):

    117             collection = remainder[0]

    118             remainder = remainder[1:]

    119         controller = manager.NeutronManager.get_controller_for_resource(

    120             collection)

    121         if not controller:

    122             LOG.warning("No controller found for: %s - returning response "

    123                         "code 404", collection)

    124             pecan.abort(404)

    125         # Store resource and collection names in pecan request context so that

    126         # hooks can leverage them if necessary. The following code uses

    127         # attributes from the controller instance to ensure names have been

    128         # properly sanitized (eg: replacing dashes with underscores)

    129         request.context['resource'] = controller.resource

    130         request.context['collection'] = controller.collection

    131         # NOTE(blogan): initialize a dict to store the ids of the items walked

    132         # in the path for example: /networks/1234 would cause uri_identifiers

    133         # to contain: {'network_id': '1234'}

    134         # This is for backwards compatibility with legacy extensions that

    135         # defined their own controllers and expected kwargs to be passed in

    136         # with the uri_identifiers

    137         request.context['uri_identifiers'] = {}

    138         return controller, remainder

 

b /usr/lib/python2.7/site-packages/neutron/manager.py:116

 

    99 @six.add_metaclass(profiler.TracedMeta)

    100 class NeutronManager(object):

    101     """Neutron's Manager class.

    102

    103     Neutron's Manager class is responsible for parsing a config file and

    104     instantiating the correct plugin that concretely implements

    105     neutron_plugin_base class.

    106     """

    107     # TODO(armax): use of the singleton pattern for this class is vestigial,

    108     # and it is mainly relied on by the unit tests. It is safer to get rid

    109     # of it once the entire codebase (neutron + subprojects) has switched

    110     # entirely to using the plugins directory.

    111     _instance = None

    112     __trace_args__ = {"name": "rpc"}

    113

    114     def __init__(self, options=None, config_file=None):

    115         # If no options have been provided, create an empty dict

    116         if not options:

    117             options = {}

    118

    119         msg = validate_pre_plugin_load()

    120         if msg:

    121             LOG.critical(msg)

    122             raise Exception(msg)

    123

    124         # NOTE(jkoelker) Testing for the subclass with the __subclasshook__

    125         #                breaks tach monitoring. It has been removed

    126         #                intentionally to allow v2 plugins to be monitored

    127         #                for performance metrics.

    128         plugin_provider = cfg.CONF.core_plugin

    -> 'neutron.plugins.ml2.plugin.Ml2Plugin'

    129         LOG.info("Loading core plugin: %s", plugin_provider)

    130         # NOTE(armax): keep hold of the actual plugin object

    131         plugin = self._get_plugin_instance(CORE_PLUGINS_NAMESPACE,

    132                                            plugin_provider)

    -> <neutron.plugins.ml2.plugin.Ml2Plugin object at 0x7fda262eb9d0>

    133         directory.add_plugin(lib_const.CORE, plugin)

    -> (Pdb) p directory.get_plugins()

       {'CORE': <weakproxy at 0x7fda252560a8 to Ml2Plugin at 0x7fda262eb9d0>}

    134         msg = validate_post_plugin_load()

    135         if msg:

    136             LOG.critical(msg)

    137             raise Exception(msg)

    138

    139         # load services from the core plugin first

    140         self._load_services_from_core_plugin(plugin)

    141         self._load_service_plugins()

    -> backtrace:

 

        -> return pecan_app.v2_factory(global_config, **local_config)

        /usr/lib/python2.7/site-packages/neutron/pecan_wsgi/app.py(47)v2_factory()

        -> startup.initialize_all()

        /usr/lib/python2.7/site-packages/neutron/pecan_wsgi/startup.py(39)initialize_all()

        -> manager.init()

        /usr/lib/python2.7/site-packages/neutron/manager.py(296)init()

        -> NeutronManager.get_instance()

        /usr/lib/python2.7/site-packages/neutron/manager.py(247)get_instance()

        -> cls._create_instance()

        /usr/lib/python2.7/site-packages/oslo_concurrency/lockutils.py(271)inner()

        -> return f(*args, **kwargs)

        /usr/lib/python2.7/site-packages/neutron/manager.py(233)_create_instance()

        -> cls._instance = cls()

        /usr/lib/python2.7/site-packages/neutron/manager.py(141)__init__()

        -> self._load_service_plugins()

        /usr/lib/python2.7/site-packages/neutron/manager.py(203)_load_service_plugins()

        -> provider)

        /usr/lib/python2.7/site-packages/neutron/manager.py(165)_get_plugin_instance()

        -> plugin_class = self.load_class_for_provider(namespace, plugin_provider)

        /usr/lib/python2.7/site-packages/neutron/manager.py(159)load_class_for_provider()

        -> plugin_provider)

        /usr/lib/python2.7/site-packages/neutron/common/utils.py(330)load_class_by_alias_or_classname()

        -> class_to_load = importutils.import_class(name)

        > /usr/lib/python2.7/site-packages/oslo_utils/importutils.py(24)import_class()

        -> def import_class(import_str):

 

    -> <class 'neutron_lbaas.services.loadbalancer.plugin.LoadBalancerPluginv2'>

    -> (Pdb) p directory.get_plugins()

        {'CORE': <weakproxy at 0x7fda25235f70 to Ml2Plugin at 0x7fda262eb9d0>,

        'LOADBALANCERV2': <weakproxy at 0x7fda25235f18 to LoadBalancerPluginv2 at 0x7fda250369d0>}

 

    142         # Used by pecan WSGI

    143         self.resource_plugin_mappings = {}

    144         self.resource_controller_mappings = {}

    145         self.path_prefix_resource_mappings = defaultdict(list)

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值