neutron-linuxbridge 源码走读(一)

文件名:linuxbridge_neutron_agent.py

主要走读LinuxBridgeManager类。

 

class LinuxBridgeManager:
    def __init__(self, interface_mappings, root_helper):
        self.interface_mappings = interface_mappings
        self.root_helper = root_helper
        self.ip = ip_lib.IPWrapper(self.root_helper)
        # VXLAN related parameters:
        self.local_ip = cfg.CONF.VXLAN.local_ip
        self.vxlan_mode = lconst.VXLAN_NONE
        if cfg.CONF.VXLAN.enable_vxlan:
            self.local_int = self.get_interface_by_ip(self.local_ip)
            if self.local_int:
                self.check_vxlan_support()
            else:
                LOG.warning(_('VXLAN is enabled, a valid local_ip '
                              'must be provided'))
        # Store network mapping to segments
        self.network_map = {}

##其中interface_mappings为读取 etc/neutron/plugins/linuxbridge/linuxbridge_conf.ini文件中physical_interface_mappings的值,如physnet1:eth1,__init__方法主要给linuxbridgeManager初始化一些信息。包含上述interface_mappings,vxlan,network相关信息。


    def interface_exists_on_bridge(self, bridge, interface):
        directory = '/sys/class/net/%s/brif' % bridge
        for filename in os.listdir(directory):
            if filename == interface:
                return True
        return False

##判断网口是否存在于网桥上,可以看到判断方法是在网桥目录下的brif目录下是否存在接口文件。
    def get_bridge_name(self, network_id):
        if not network_id:
            LOG.warning(_("Invalid Network ID, will lead to incorrect bridge"
                          "name"))
        bridge_name = BRIDGE_NAME_PREFIX + network_id[0:11]
        return bridge_name
##BRIDGE_NAME_PREFIX = "brq",也就是说network对应的bridge的名称为"brq+network_id的前12位"
此处的network是neutron里面的network?
    def get_subinterface_name(self, physical_interface, vlan_id):
        if not vlan_id:
            LOG.warning(_("Invalid VLAN ID, will lead to incorrect "
                          "subinterface name"))
        subinterface_name = '%s.%s' % (physical_interface, vlan_id)
        return subinterface_name
##获取(准确来讲应该叫生成吧)子接口名称,子接口名称为物理接口+vlanId生成。


    def get_tap_device_name(self, interface_id):
        if not interface_id:
            LOG.warning(_("Invalid Interface ID, will lead to incorrect "
                          "tap device name"))
        tap_device_name = TAP_INTERFACE_PREFIX + interface_id[0:11]
        return tap_device_name
##TAP_INTERFACE_PREFIX = "tap",于虚拟机port相连的tap口的名称为,"tap+port的前12位"

    def get_vxlan_device_name(self, segmentation_id):
        if 0 <= int(segmentation_id) <= constants.MAX_VXLAN_VNI:
            return VXLAN_INTERFACE_PREFIX + str(segmentation_id)
        else:
            LOG.warning(_("Invalid Segmentation ID: %s, will lead to "
                          "incorrect vxlan device name"), segmentation_id)

##VXLAN_INTERFACE_PREFIX = "vxlan-",vxlan设备名为"vxlan-segmentation_id"


    def get_all_neutron_bridges(self):
        neutron_bridge_list = []
        bridge_list = os.listdir(BRIDGE_FS)
        for bridge in bridge_list:
            if bridge.startswith(BRIDGE_NAME_PREFIX):
                neutron_bridge_list.append(bridge)
        return neutron_bridge_list
##BRIDGE_FS = "/sys/devices/virtual/net/",查询"/sys/devices/virtual/net/"目录下的"brq"设备



    def get_interfaces_on_bridge(self, bridge_name):
        if ip_lib.device_exists(bridge_name, root_helper=self.root_helper):
            bridge_interface_path = BRIDGE_INTERFACES_FS.replace(
                BRIDGE_NAME_PLACEHOLDER, bridge_name)
            return os.listdir(bridge_interface_path)
        else:
            return []

##查询指定网桥brif目录下的所有接口设备。
    def get_tap_devices_count(self, bridge_name):
            bridge_interface_path = BRIDGE_INTERFACES_FS.replace(
                BRIDGE_NAME_PLACEHOLDER, bridge_name)
            try:
                if_list = os.listdir(bridge_interface_path)
                return len([interface for interface in if_list if
                            interface.startswith(TAP_INTERFACE_PREFIX)])
            except OSError:
                return 0
##获取网桥下所有tap设备
    def get_interface_by_ip(self, ip):
        for device in self.ip.get_devices():
            if device.addr.list(to=ip):
                return device.name

    def get_bridge_for_tap_device(self, tap_device_name):
        bridges = self.get_all_neutron_bridges()
        for bridge in bridges:
            interfaces = self.get_interfaces_on_bridge(bridge)
            if tap_device_name in interfaces:
                return bridge

        return None

    def is_device_on_bridge(self, device_name):
        if not device_name:
            return False
        else:
            bridge_port_path = BRIDGE_PORT_FS_FOR_DEVICE.replace(
                DEVICE_NAME_PLACEHOLDER, device_name)
            return os.path.exists(bridge_port_path)

    def ensure_vlan_bridge(self, network_id, physical_interface, vlan_id):
        """Create a vlan and bridge unless they already exist."""
        interface = self.ensure_vlan(physical_interface, vlan_id)
        bridge_name = self.get_bridge_name(network_id)
        ips, gateway = self.get_interface_details(interface)
        if self.ensure_bridge(bridge_name, interface, ips, gateway):
            return interface
##核心:
##1、 ip link add link <physical_interface> name <physical_interface.vlanId> type vlan id <vlanId>
##2、brctl addbr <bridge_name>
##3、brctl setfd <bridge_name> 
##4、brctl stp <bridge_name> off
##5、ip link set <bridge_name> up

未完待续....
    def ensure_vxlan_bridge(self, network_id, segmentation_id):
        """Create a vxlan and bridge unless they already exist."""
        interface = self.ensure_vxlan(segmentation_id)
        if not interface:
            LOG.error(_("Failed creating vxlan interface for "
                        "%(segmentation_id)s"),
                      {segmentation_id: segmentation_id})
            return
        bridge_name = self.get_bridge_name(network_id)
        self.ensure_bridge(bridge_name, interface)
        return interface

    def get_interface_details(self, interface):
        device = self.ip.device(interface)
        ips = device.addr.list(scope='global')

        # Update default gateway if necessary
        gateway = device.route.get_gateway(scope='global')
        return ips, gateway

    def ensure_flat_bridge(self, network_id, physical_interface):
        """Create a non-vlan bridge unless it already exists."""
        bridge_name = self.get_bridge_name(network_id)
        ips, gateway = self.get_interface_details(physical_interface)
        if self.ensure_bridge(bridge_name, physical_interface, ips, gateway):
            return physical_interface

    def ensure_local_bridge(self, network_id):
        """Create a local bridge unless it already exists."""
        bridge_name = self.get_bridge_name(network_id)
        return self.ensure_bridge(bridge_name)

    def ensure_vlan(self, physical_interface, vlan_id):
        """Create a vlan unless it already exists."""
        interface = self.get_subinterface_name(physical_interface, vlan_id)
        if not ip_lib.device_exists(interface, root_helper=self.root_helper):
            LOG.debug(_("Creating subinterface %(interface)s for "
                        "VLAN %(vlan_id)s on interface "
                        "%(physical_interface)s"),
                      {'interface': interface, 'vlan_id': vlan_id,
                       'physical_interface': physical_interface})
            if utils.execute(['ip', 'link', 'add', 'link',
                              physical_interface,
                              'name', interface, 'type', 'vlan', 'id',
                              vlan_id], root_helper=self.root_helper):
                return
            if utils.execute(['ip', 'link', 'set',
                              interface, 'up'], root_helper=self.root_helper):
                return
            LOG.debug(_("Done creating subinterface %s"), interface)
        return interface

    def ensure_vxlan(self, segmentation_id):
        """Create a vxlan unless it already exists."""
        interface = self.get_vxlan_device_name(segmentation_id)
        if not ip_lib.device_exists(interface, root_helper=self.root_helper):
            LOG.debug(_("Creating vxlan interface %(interface)s for "
                        "VNI %(segmentation_id)s"),
                      {'interface': interface,
                       'segmentation_id': segmentation_id})
            args = {'dev': self.local_int}
            if self.vxlan_mode == lconst.VXLAN_MCAST:
                args['group'] = cfg.CONF.VXLAN.vxlan_group
            if cfg.CONF.VXLAN.ttl:
                args['ttl'] = cfg.CONF.VXLAN.ttl
            if cfg.CONF.VXLAN.tos:
                args['tos'] = cfg.CONF.VXLAN.tos
            if cfg.CONF.VXLAN.l2_population:
                args['proxy'] = True
            int_vxlan = self.ip.add_vxlan(interface, segmentation_id, **args)
            int_vxlan.link.set_up()
            LOG.debug(_("Done creating vxlan interface %s"), interface)
        return interface

    def update_interface_ip_details(self, destination, source, ips,
                                    gateway):
        if ips or gateway:
            dst_device = self.ip.device(destination)
            src_device = self.ip.device(source)

        # Append IP's to bridge if necessary
        if ips:
            for ip in ips:
                dst_device.addr.add(ip_version=ip['ip_version'],
                                    cidr=ip['cidr'],
                                    broadcast=ip['broadcast'])

        if gateway:
            # Ensure that the gateway can be updated by changing the metric
            metric = 100
            if 'metric' in gateway:
                metric = gateway['metric'] - 1
            dst_device.route.add_gateway(gateway=gateway['gateway'],
                                         metric=metric)
            src_device.route.delete_gateway(gateway=gateway['gateway'])

        # Remove IP's from interface
        if ips:
            for ip in ips:
                src_device.addr.delete(ip_version=ip['ip_version'],
                                       cidr=ip['cidr'])

    def _bridge_exists_and_ensure_up(self, bridge_name):
        """Check if the bridge exists and make sure it is up."""
        br = ip_lib.IPDevice(bridge_name, self.root_helper)
        try:
            # If the device doesn't exist this will throw a RuntimeError
            br.link.set_up()
        except RuntimeError:
            return False
        return True

    def ensure_bridge(self, bridge_name, interface=None, ips=None,
                      gateway=None):
        """Create a bridge unless it already exists."""
        # _bridge_exists_and_ensure_up instead of device_exists is used here
        # because there are cases where the bridge exists but it's not UP,
        # for example:
        # 1) A greenthread was executing this function and had not yet executed
        # "ip link set bridge_name up" before eventlet switched to this
        # thread running the same function
        # 2) The Nova VIF driver was running concurrently and had just created
        #    the bridge, but had not yet put it UP
        if not self._bridge_exists_and_ensure_up(bridge_name):
            LOG.debug(_("Starting bridge %(bridge_name)s for subinterface "
                        "%(interface)s"),
                      {'bridge_name': bridge_name, 'interface': interface})
            if utils.execute(['brctl', 'addbr', bridge_name],
                             root_helper=self.root_helper):
                return
            if utils.execute(['brctl', 'setfd', bridge_name,
                              str(0)], root_helper=self.root_helper):
                return
            if utils.execute(['brctl', 'stp', bridge_name,
                              'off'], root_helper=self.root_helper):
                return
            if utils.execute(['ip', 'link', 'set', bridge_name,
                              'up'], root_helper=self.root_helper):
                return
            LOG.debug(_("Done starting bridge %(bridge_name)s for "
                        "subinterface %(interface)s"),
                      {'bridge_name': bridge_name, 'interface': interface})

        if not interface:
            return bridge_name

        # Update IP info if necessary
        self.update_interface_ip_details(bridge_name, interface, ips, gateway)

        # Check if the interface is part of the bridge
        if not self.interface_exists_on_bridge(bridge_name, interface):
            try:
                # Check if the interface is not enslaved in another bridge
                if self.is_device_on_bridge(interface):
                    bridge = self.get_bridge_for_tap_device(interface)
                    utils.execute(['brctl', 'delif', bridge, interface],
                                  root_helper=self.root_helper)

                utils.execute(['brctl', 'addif', bridge_name, interface],
                              root_helper=self.root_helper)
            except Exception as e:
                LOG.error(_("Unable to add %(interface)s to %(bridge_name)s! "
                            "Exception: %(e)s"),
                          {'interface': interface, 'bridge_name': bridge_name,
                           'e': e})
                return
        return bridge_name

    def ensure_physical_in_bridge(self, network_id,
                                  network_type,
                                  physical_network,
                                  segmentation_id):
        if network_type == p_const.TYPE_VXLAN:
            if self.vxlan_mode == lconst.VXLAN_NONE:
                LOG.error(_("Unable to add vxlan interface for network %s"),
                          network_id)
                return
            return self.ensure_vxlan_bridge(network_id, segmentation_id)

        physical_interface = self.interface_mappings.get(physical_network)
        if not physical_interface:
            LOG.error(_("No mapping for physical network %s"),
                      physical_network)
            return
        if network_type == p_const.TYPE_FLAT:
            return self.ensure_flat_bridge(network_id, physical_interface)
        elif network_type == p_const.TYPE_VLAN:
            return self.ensure_vlan_bridge(network_id, physical_interface,
                                           segmentation_id)
        else:
            LOG.error(_("Unknown network_type %(network_type)s for network "
                        "%(network_id)s."), {network_type: network_type,
                                             network_id: network_id})

    def add_tap_interface(self, network_id, network_type, physical_network,
                          segmentation_id, tap_device_name):
        """Add tap interface.

        If a VIF has been plugged into a network, this function will
        add the corresponding tap device to the relevant bridge.
        """
        if not ip_lib.device_exists(tap_device_name,
                                    root_helper=self.root_helper):
            LOG.debug(_("Tap device: %s does not exist on "
                        "this host, skipped"), tap_device_name)
            return False

        bridge_name = self.get_bridge_name(network_id)
        if network_type == p_const.TYPE_LOCAL:
            self.ensure_local_bridge(network_id)
        elif not self.ensure_physical_in_bridge(network_id,
                                                network_type,
                                                physical_network,
                                                segmentation_id):
            return False

        # Check if device needs to be added to bridge
        tap_device_in_bridge = self.get_bridge_for_tap_device(tap_device_name)
        if not tap_device_in_bridge:
            data = {'tap_device_name': tap_device_name,
                    'bridge_name': bridge_name}
            msg = _("Adding device %(tap_device_name)s to bridge "
                    "%(bridge_name)s") % data
            LOG.debug(msg)
            if utils.execute(['brctl', 'addif', bridge_name, tap_device_name],
                             root_helper=self.root_helper):
                return False
        else:
            data = {'tap_device_name': tap_device_name,
                    'bridge_name': bridge_name}
            msg = _("%(tap_device_name)s already exists on bridge "
                    "%(bridge_name)s") % data
            LOG.debug(msg)
        return True

    def add_interface(self, network_id, network_type, physical_network,
                      segmentation_id, port_id):
        self.network_map[network_id] = NetworkSegment(network_type,
                                                      physical_network,
                                                      segmentation_id)
        tap_device_name = self.get_tap_device_name(port_id)
        return self.add_tap_interface(network_id, network_type,
                                      physical_network, segmentation_id,
                                      tap_device_name)

    def delete_vlan_bridge(self, bridge_name):
        if ip_lib.device_exists(bridge_name, root_helper=self.root_helper):
            interfaces_on_bridge = self.get_interfaces_on_bridge(bridge_name)
            for interface in interfaces_on_bridge:
                self.remove_interface(bridge_name, interface)

                if interface.startswith(VXLAN_INTERFACE_PREFIX):
                    self.delete_vxlan(interface)
                    continue

                for physical_interface in self.interface_mappings.itervalues():
                    if (interface.startswith(physical_interface)):
                        ips, gateway = self.get_interface_details(bridge_name)
                        if ips:
                            # This is a flat network or a VLAN interface that
                            # was setup outside of neutron => return IP's from
                            # bridge to interface
                            self.update_interface_ip_details(interface,
                                                             bridge_name,
                                                             ips, gateway)
                        elif physical_interface != interface:
                            self.delete_vlan(interface)

            LOG.debug(_("Deleting bridge %s"), bridge_name)
            if utils.execute(['ip', 'link', 'set', bridge_name, 'down'],
                             root_helper=self.root_helper):
                return
            if utils.execute(['brctl', 'delbr', bridge_name],
                             root_helper=self.root_helper):
                return
            LOG.debug(_("Done deleting bridge %s"), bridge_name)

        else:
            LOG.error(_("Cannot delete bridge %s, does not exist"),
                      bridge_name)

    def remove_empty_bridges(self):
        for network_id in self.network_map.keys():
            bridge_name = self.get_bridge_name(network_id)
            if not self.get_tap_devices_count(bridge_name):
                self.delete_vlan_bridge(bridge_name)
                del self.network_map[network_id]

    def remove_interface(self, bridge_name, interface_name):
        if ip_lib.device_exists(bridge_name, root_helper=self.root_helper):
            if not self.is_device_on_bridge(interface_name):
                return True
            LOG.debug(_("Removing device %(interface_name)s from bridge "
                        "%(bridge_name)s"),
                      {'interface_name': interface_name,
                       'bridge_name': bridge_name})
            if utils.execute(['brctl', 'delif', bridge_name, interface_name],
                             root_helper=self.root_helper):
                return False
            LOG.debug(_("Done removing device %(interface_name)s from bridge "
                        "%(bridge_name)s"),
                      {'interface_name': interface_name,
                       'bridge_name': bridge_name})
            return True
        else:
            LOG.debug(_("Cannot remove device %(interface_name)s bridge "
                        "%(bridge_name)s does not exist"),
                      {'interface_name': interface_name,
                       'bridge_name': bridge_name})
            return False

    def delete_vlan(self, interface):
        if ip_lib.device_exists(interface, root_helper=self.root_helper):
            LOG.debug(_("Deleting subinterface %s for vlan"), interface)
            if utils.execute(['ip', 'link', 'set', interface, 'down'],
                             root_helper=self.root_helper):
                return
            if utils.execute(['ip', 'link', 'delete', interface],
                             root_helper=self.root_helper):
                return
            LOG.debug(_("Done deleting subinterface %s"), interface)

    def delete_vxlan(self, interface):
        if ip_lib.device_exists(interface, root_helper=self.root_helper):
            LOG.debug(_("Deleting vxlan interface %s for vlan"),
                      interface)
            int_vxlan = self.ip.device(interface)
            int_vxlan.link.set_down()
            int_vxlan.link.delete()
            LOG.debug(_("Done deleting vxlan interface %s"), interface)

    def update_devices(self, registered_devices):
        devices = self.get_tap_devices()
        if devices == registered_devices:
            return
        added = devices - registered_devices
        removed = registered_devices - devices
        return {'current': devices,
                'added': added,
                'removed': removed}

    def get_tap_devices(self):
        devices = set()
        for device in os.listdir(BRIDGE_FS):
            if device.startswith(TAP_INTERFACE_PREFIX):
                devices.add(device)
        return devices

    def vxlan_ucast_supported(self):
        if not cfg.CONF.VXLAN.l2_population:
            return False
        if not ip_lib.iproute_arg_supported(
                ['bridge', 'fdb'], 'append', self.root_helper):
            LOG.warning(_('Option "%(option)s" must be supported by command '
                          '"%(command)s" to enable %(mode)s mode') %
                        {'option': 'append',
                         'command': 'bridge fdb',
                         'mode': 'VXLAN UCAST'})
            return False
        for segmentation_id in range(1, constants.MAX_VXLAN_VNI + 1):
            if not ip_lib.device_exists(
                    self.get_vxlan_device_name(segmentation_id),
                    root_helper=self.root_helper):
                break
        else:
            LOG.error(_('No valid Segmentation ID to perform UCAST test.'))
            return False

        test_iface = self.ensure_vxlan(segmentation_id)
        try:
            utils.execute(
                cmd=['bridge', 'fdb', 'append', constants.FLOODING_ENTRY[0],
                     'dev', test_iface, 'dst', '1.1.1.1'],
                root_helper=self.root_helper)
            return True
        except RuntimeError:
            return False
        finally:
            self.delete_vxlan(test_iface)

    def vxlan_mcast_supported(self):
        if not cfg.CONF.VXLAN.vxlan_group:
            LOG.warning(_('VXLAN muticast group must be provided in '
                          'vxlan_group option to enable VXLAN MCAST mode'))
            return False
        if not ip_lib.iproute_arg_supported(
                ['ip', 'link', 'add', 'type', 'vxlan'],
                'proxy', self.root_helper):
            LOG.warning(_('Option "%(option)s" must be supported by command '
                          '"%(command)s" to enable %(mode)s mode') %
                        {'option': 'proxy',
                         'command': 'ip link add type vxlan',
                         'mode': 'VXLAN MCAST'})

            return False
        return True

    def vxlan_module_supported(self):
        try:
            utils.execute(cmd=['modinfo', 'vxlan'])
            return True
        except RuntimeError:
            return False

    def check_vxlan_support(self):
        self.vxlan_mode = lconst.VXLAN_NONE
        if not self.vxlan_module_supported():
            LOG.error(_('Linux kernel vxlan module and iproute2 3.8 or above '
                        'are required to enable VXLAN.'))
            raise exceptions.VxlanNetworkUnsupported()

        if self.vxlan_ucast_supported():
            self.vxlan_mode = lconst.VXLAN_UCAST
        elif self.vxlan_mcast_supported():
            self.vxlan_mode = lconst.VXLAN_MCAST
        else:
            raise exceptions.VxlanNetworkUnsupported()
        LOG.debug(_('Using %s VXLAN mode'), self.vxlan_mode)

    def fdb_ip_entry_exists(self, mac, ip, interface):
        entries = utils.execute(['ip', 'neigh', 'show', 'to', ip,
                                 'dev', interface],
                                root_helper=self.root_helper)
        return mac in entries

    def fdb_bridge_entry_exists(self, mac, interface, agent_ip=None):
        entries = utils.execute(['bridge', 'fdb', 'show', 'dev', interface],
                                root_helper=self.root_helper)
        if not agent_ip:
            return mac in entries

        return (agent_ip in entries and mac in entries)

    def add_fdb_ip_entry(self, mac, ip, interface):
        utils.execute(['ip', 'neigh', 'replace', ip, 'lladdr', mac,
                       'dev', interface, 'nud', 'permanent'],
                      root_helper=self.root_helper,
                      check_exit_code=False)

    def remove_fdb_ip_entry(self, mac, ip, interface):
        utils.execute(['ip', 'neigh', 'del', ip, 'lladdr', mac,
                       'dev', interface],
                      root_helper=self.root_helper,
                      check_exit_code=False)

    def add_fdb_bridge_entry(self, mac, agent_ip, interface, operation="add"):
        utils.execute(['bridge', 'fdb', operation, mac, 'dev', interface,
                       'dst', agent_ip],
                      root_helper=self.root_helper,
                      check_exit_code=False)

    def remove_fdb_bridge_entry(self, mac, agent_ip, interface):
        utils.execute(['bridge', 'fdb', 'del', mac, 'dev', interface,
                       'dst', agent_ip],
                      root_helper=self.root_helper,
                      check_exit_code=False)

    def add_fdb_entries(self, agent_ip, ports, interface):
        for mac, ip in ports:
            if mac != constants.FLOODING_ENTRY[0]:
                self.add_fdb_ip_entry(mac, ip, interface)
                self.add_fdb_bridge_entry(mac, agent_ip, interface)
            elif self.vxlan_mode == lconst.VXLAN_UCAST:
                if self.fdb_bridge_entry_exists(mac, interface):
                    self.add_fdb_bridge_entry(mac, agent_ip, interface,
                                              "append")
                else:
                    self.add_fdb_bridge_entry(mac, agent_ip, interface)

    def remove_fdb_entries(self, agent_ip, ports, interface):
        for mac, ip in ports:
            if mac != constants.FLOODING_ENTRY[0]:
                self.remove_fdb_ip_entry(mac, ip, interface)
                self.remove_fdb_bridge_entry(mac, agent_ip, interface)
            elif self.vxlan_mode == lconst.VXLAN_UCAST:
                self.remove_fdb_bridge_entry(mac, agent_ip, interface)

 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值