手记

如何用EOS和OpenShift部署一个BaaS系统


现在区块链技术很是热门,很多大公司都纷纷开辟了自己关于区块链的相关业务部门,比如数字金融,产品溯源等,那么什么是区块链技术呢?区块链是分布式数据存储、点对点传输、共识机制、加密算法等计算机技术的新型应用模式。在区块链技术的迭代过程中,大家公认比特币是区块链1.0版本的应用,以太坊是2.0版本,现在在链圈炒的最为火热的就是自称是区块链3.0版本的EOS。它以BFT+DPos的公式机制和石墨烯网络为基础,号称公网上线以后能够达到3000-4000左右的TPS(比特币为6-7笔,以太坊是30-40笔,但是笔者亲测EOS的3.0版本是1000tps左右,可能是受环境和主机性能所限吧)。越来越多的人开始关注区块链技术,越来越多的企业级应用开始搬上区块链,现在我就带领大家搭建一套最简单的BaaS系统(block chain as a service:区块链即服务)。
当人们一说到某某即服务的时候总是想到PaaS、SaaS或者IaaS,没错!这次搭建的BaaS也和前面几个类似采用PaaS的设计架构,即用容器的方式搭建BaaS平台。提到容器,就不得不想到现在最流行的Docker+Kubernetes的方式,Docker提供了单机条件下的容器运行环境,Kubernetes提供了一套高可用的容器编排管理系统。本次使用的就是在代码级对Kubernetes做了升级和改造的,由Red Hat公司推出的OpenShift,它使用Kuberenetes作为其内核,对它网络功能做了补充,增加了Web界面,又对原有的Ingress做了优化和升级,所以我们选择它作为本次BaaS的基础平台。
BaaS平台部署图如下图所示:



1、首先开始制作EOS镜像
1)下面是EOS代码获取方法:

git clone https://github.com/EOSIO/eos --recursive


然后进入代码目录执行:

cd eos
./eosio_build.sh


2)在代码的EOS目录下会生成一个build目录,把EOS的二进制文件NodeOS、keosd、cleos等复制到要制作镜像的子目录下,下面是Dockerfile文件的内容,其中基础镜像选用了CentOS,这个系统的版本好要和刚才编译EOS的一致,否则可能会出现NodeOS无法运行的错误。

From docker.io/centos
ADD cleos /cleos
ADD nodeos /nodeos
ADD genesis.json /genesis.json
ADD config.ini /config.ini
ADD nodeosd.sh /opt/eosio/bin/
USER 0
CMD /opt/eosio/bin/nodeosd.sh


3)除了有必要的二进制文件外,我们还要为NodeOS启动配置必要的启动脚本和两个配置文件。
下面是启动脚本:

#!/bin/sh
mkdir -p /opt/eosio/config-dir
mkdir -p /opt/eosio/data-dir
mkdir -p /opt/eosio/bin
cp /config.ini /opt/eosio/config-dir
cp /genesis.json /opt/eosio/config-dir
cp /cleos /opt/eosio/bin
cp /nodeos /opt/eosio/bin
cp -r /contracts /opt/eosio/bin
CONFIG_DIR="--config-dir=/opt/eosio/config-dir"
DATA_DIR="--data-dir=/opt/eosio/data-dir"
#replace agent name
sed -i 's/AGENT-NAME/'$HOSTNAME'/g' /opt/eosio/config-dir/config.ini
#replace producer name
sed -i 's/EOSIO/'$BP'/g' /opt/eosio/config-dir/config.ini
#replace p2p-peer-address
sed -i 's/EOSIO/'$BP'/g' /opt/eosio/config-dir/config.ini
#replace p2p-server-address
sed -i 's/EOSIO/'$BP'/g' /opt/eosio/config-dir/config.ini
sed -i 's/P2P_PEER_ADDRESS/'${BP_LIST}'/g' /opt/eosio/config-dir/config.ini
sed -i 's/P2P_SERVER_ADDRESS/'${HOSTNAME}'/g' /opt/eosio/config-dir/config.ini
#replace keys
sed -i 's/PRIVATE-KEY/'${PRIVATE_KEY}'/g' /opt/eosio/config-dir/config.ini
sed -i 's/PUBLIC-KEY/'${PUBLIC_KEY}'/g' /opt/eosio/config-dir/config.ini
exec /opt/eosio/bin/nodeos -e $CONFIG_DIR $DATA_DIR $@


genesis.json文件内容:

# cat genesis.json
{
 "initial_timestamp": "2018-03-02T12:00:00.000",
 "initial_key": "EOSxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
 "initial_configuration": {
   "max_block_net_usage": 1048576,
   "target_block_net_usage_pct": 1000,
   "max_transaction_net_usage": 524288,
   "base_per_transaction_net_usage": 12,
   "net_usage_leeway": 500,
   "context_free_discount_net_usage_num": 20,
   "context_free_discount_net_usage_den": 100,
   "max_block_cpu_usage": 100000,
   "target_block_cpu_usage_pct": 500,
   "max_transaction_cpu_usage": 50000,
   "min_transaction_cpu_usage": 100,
   "max_transaction_lifetime": 3600,
   "deferred_trx_expiration_window": 600,
   "max_transaction_delay": 3888000,
   "max_inline_action_size": 4096,
   "max_inline_action_depth": 4,
   "max_authority_depth": 6,
   "max_generated_transaction_count": 16
 },
 "initial_chain_id": "0000000000000000000000000000000000000000000000000000000000000000"
}
# cat config.ini
# File to read Genesis State from (eosio::chain_plugin)
genesis-json = "genesis.json"
# override the initial timestamp in the Genesis State file (eosio::chain_plugin)
# genesis-timestamp =
# the location of the block log (absolute path or relative to application data dir) (eosio::chain_plugin)
block-log-dir = "blocks"
# Pairs of [BLOCK_NUM,BLOCK_ID] that should be enforced as checkpoints. (eosio::chain_plugin)
# checkpoint =
# Override default WASM runtime (eosio::chain_plugin)
# wasm-runtime =
# Maximum size MB of database shared memory file (eosio::chain_plugin)
shared-memory-size-mb = 1024
# Track only transactions whose scopes involve the listed accounts. Default is to track all transactions. (eosio::history_plugin)
# filter_on_accounts =
# The local IP and port to listen for incoming http connections; set blank to disable. (eosio::http_plugin)
http-server-address = 0.0.0.0:8888
# The local IP and port to listen for incoming https connections; leave blank to disable. (eosio::http_plugin)
# https-server-address =
# Filename with the certificate chain to present on https connections. PEM format. Required for https. (eosio::http_plugin)
# https-certificate-chain-file =
# Filename with https private key in PEM format. Required for https (eosio::http_plugin)
# https-private-key-file =
# Specify the Access-Control-Allow-Origin to be returned on each request. (eosio::http_plugin)
# access-control-allow-origin =
# Specify the Access-Control-Allow-Headers to be returned on each request. (eosio::http_plugin)
# access-control-allow-headers =
# Specify if Access-Control-Allow-Credentials: true should be returned on each request. (eosio::http_plugin)
access-control-allow-credentials = false
# The actual host:port used to listen for incoming p2p connections. (eosio::net_plugin)
p2p-listen-endpoint = 0.0.0.0:9876
# An externally accessible host:port for identifying this node. Defaults to p2p-listen-endpoint. (eosio::net_plugin)
p2p-peer-address = P2P_PEER_ADDRESS:9876
# The public endpoint of a peer node to connect to. Use multiple p2p-peer-address options as needed to compose a network. (eosio::net_plugin)
p2p-server-address = P2P_SERVER_ADDRESS
# The name supplied to identify this node amongst the peers. (eosio::net_plugin)
agent-name = "AGENT-NAME"
# Can be 'any' or 'producers' or 'specified' or 'none'. If 'specified', peer-key must be specified at least once. If only 'producers', peer-key is not required. 'producers' and 'specified' may be combined. (eosio::net_plugin)
allowed-connection = any
# Optional public key of peer allowed to connect.  May be used multiple times. (eosio::net_plugin)
# peer-key =
# Tuple of [PublicKey, WIF private key] (may specify multiple times) (eosio::net_plugin)
# peer-private-key =
# Maximum number of clients from which connections are accepted, use 0 for no limit (eosio::net_plugin)
max-clients = 25
# number of seconds to wait before cleaning up dead connections (eosio::net_plugin)
connection-cleanup-period = 30
# True to require exact match of peer network version. (eosio::net_plugin)
network-version-match = 0
# number of blocks to retrieve in a chunk from any individual peer during synchronization (eosio::net_plugin)
sync-fetch-span = 100
# maximum sizes of transaction or block messages that are sent without first sending a notice (eosio::net_plugin)
max-implicit-request = 1500
# Enable block production, even if the chain is stale. (eosio::producer_plugin)
enable-stale-production = false
# Limits the maximum time (in milliseconds) that is allowed a pushed transaction's code to execute before being considered invalid (eosio::producer_plugin)
max-transaction-time = 30
# Percent of producers (0-100) that must be participating in order to produce blocks (eosio::producer_plugin)
required-participation = 33
# ID of producer controlled by this node (e.g. inita; may specify multiple times) (eosio::producer_plugin)
producer-name = EOSIO
# Tuple of [public key, WIF private key] (may specify multiple times) (eosio::producer_plugin)
private-key = ["PUBLIC-KEY","PRIVATE-KEY"]
# Lag in number of blocks from the head block when selecting the reference block for transactions (-1 means Last Irreversible Block) (eosio::txn_test_gen_plugin)
txn-reference-block-lag = 0
# The path of the wallet files (absolute path or relative to application data dir) (eosio::wallet_plugin)
wallet-dir = "."
# Timeout for unlocked wallet in seconds (default 900 (15 minutes)). Wallets will automatically lock after specified number of seconds of inactivity. Activity is defined as any wallet command e.g. list-wallets. (eosio::wallet_plugin)
unlock-timeout = 900
# eosio key that will be imported automatically when a wallet is created. (eosio::wallet_plugin)
# eosio-key =
# Plugin(s) to enable, may be specified multiple times
# plugin =
plugin = eosio::producer_plugin
plugin = eosio::chain_api_plugin
plugin = eosio::wallet_api_plugin
plugin = eosio::http_plugin
plugin = eosio::history_api_plugin
plugin = eosio::history_plugin}}}
4)运行下面命令生成image并推送到私有的Registry。
{{{docker build . -t registryhost:5000/eos/eos
docker push registryhost:5000/eos/eos


2、EOS节点部署
1)当镜像推送到Registry后,我们就可以进行Yaml文件的编写并且配置了,下面是Yaml文件的内容:

at nodeos.yaml 
apiVersion: v1
kind: Template
metadata:
 name: nodeos
 annotations:
   openshift.io/display-name: nodeos
   description: nodeos of eos.
   iconClass: icon-wildfly
   tags: nodeos
objects:
- apiVersion: v1
 kind: Service
 metadata:
   annotations:
     description: Exposes and load balances the application pods
   name: ${BP}
 spec:
   ports:
   - name: http-8888-tcp
     port: 8888
     protocol: TCP
     targetPort: 8888
   - name: admin-9876-tcp
     port: 9876
     protocol: TCP
     targetPort: 9876
   selector:
     name: ${BP}
   type: ClusterIP
- apiVersion: v1
 kind: Route
 metadata:
   name: nodeos-http
 spec:
   host: nodeos
   to:
     kind: Service
     name: ${BP}
   port:
     targetPort: http-8888-tcp
- apiVersion: v1
 kind: DeploymentConfig
 metadata:
   annotations:
     description: Defines how to deploy the nodeos
   name: ${BP}
 spec:
   replicas: 1
   selector:
     name: ${BP}
   strategy:
     type: Rolling
   template:
     metadata:
       labels:
         name: ${BP}
     spec:
       containers:
       - image: "192.168.20.2:5000/eosio/eos"
         env:
         - name: BP
           value: ${BP}
         - name: PUBLIC_KEY
           value: ${PUBLIC_KEY}
         - name: PRIVATE_KEY
           value: ${PRIVATE_KEY}
         - name: BP_LIST
           value: ${BP_LIST}
         imagePullPolicy: Always
         name: nodeos-bp
         ports:
         - containerPort: 8888
           protocol: "TCP"
         - containerPort: 9876
           protocol: "TCP"    
         resources:
           requests:
             memory: 4096Mi
             cpu: 2000m
       dnsPolicy: ClusterFirst
       restartPolicy: Always
       securityContext:
       - runAsUser: 0
parameters:
- name: BP
 description: Block Producer
 displayName: Block Producer
 value: eosio
- name: PUBLIC_KEY
 description: PUBLIC_KEY.
 displayName: PUBLIC_KEY
 value: EOS6MRyAjQq8ud7hVNYcfnVPJqcVpscN5So8BhtHuGYqET5GDW5CV
- name: PRIVATE_KEY
 description: PRIVATE_KEY.
 displayName: PRIVATE_KEY
 value: 5KQwrPbwdL6PhXujxW37FSSQZ1JiwsST4cqQzDeyXtP79zkvFD3
- name: BP_LIST
 description: A IP list of Block Producer
 displayName: A IP list of Block Producer
 value: bp's hostname and port


2)有了Yaml文件,我们就可以把我们的Yaml文件推送到OpenShift中当成模版,然后通过模版进行EOS节点的实例化,这里面有一个问题需要注意,所有节点的公私钥不可能一样,需要通过模版的参数在实例化的时候传入容器里,然后启动NodeOS的时候,调用NodeOS启动脚本把传入的变量(每一个传入变量都会生成一个环境变量)替换到config.ini文件中,然后启动EOS的主进程NodeOS。当然,这里还有一个问题也需要注意,那就是4.1的EOS配置文件里需要配置两个参数,一个是自己自身的IP(p2p-server-address),和对端IP(p2p-peer-address)第一个地址可以用这个Pod的hostname代替,第二个可以通过指定之前节点的hostname来填充。
3)使用oc create -f eos.yaml 进行模版的部署,然后通过Web界面,选择刚才的模版,输入变量的内容,最后点击生成,如下图所示:



3、既然环境已经设置完成,现在我们来初始化整个EOS的环境。
1)首先通过cleos wallet create创建一个默认的钱包,如果-n可以指定钱包的名字,这个钱包默认的存储路径是/root/eosio-wallet/目录下,看下面的输出:

# cleos wallet create 
"/usr/local/bin/keosd" launched
Creating wallet: default
Save password to use in the future to unlock this wallet.
Without password imported keys will not be retrievable.
"PW5J2SGNDmhZFiVnbZY6V669XLEksGs13KnhCnkuqQZScj5ieNV5j"


其中下面的是这个钱包的密码,需要记住,因为现在4.1版本的钱包900秒后就会自动上锁,需要这个密码来解锁。
2)创建一对公私钥,创建账户,并部署合约。
创建私钥公钥:

# cleos create key 
"/usr/local/bin/keosd" launched Private key: 5Kgh744izqDCNyuRZcBu4nMjgJbBwWBdqQZuQi53qPB9L69Cj9X Public key: EOS6CYWY1q4AAsVV3rzcT1GGQmZcg7yDnA6jZx2KUjHvmZWPv8DQg


创建代币账户:

#cleos -u http://nodeos:80 create account eosio eosio.token EOS6CYWY1q4AAsVV3rzcT1GGQmZcg7yDnA6jZx2KUjHvmZWPv8DQg EOS6CYWY1q4AAsVV3rzcT1GGQmZcg7yDnA6jZx2KUjHvmZWPv8DQg


部署代币合约:

#cleos -u http://nodeos:80  set contract eosio.token  eos/build/contracts/eosio.token/
Reading WAST/WASM from /eos_code/dawn-v4.1.0/eos/build/contracts/eosio.token/eosio.token.wasm...
Using already assembled WASM...
Publishing contract...
executed transaction: 9ff87dffe3eb2eeac29a4afd8eec8faa563f6be43cf5142e71cd61d0c4b66244  8072 bytes  976 us
#         eosio <= eosio::setcode               {"account":"eosio.token","vmtype":0,"vmversion":0,"code":"0061736d01000000017f1560037f7e7f0060057f7e...
#         eosio <= eosio::setabi                {"account":"eosio.token","abi":"0e656f73696f3a3a6162692f312e30010c6163636f756e745f6e616d65046e616d65...
warning: transaction executed locally, but may not be confirmed by the network yet


创建代币:

#cleos -u http://nodeos:80  push action eosio.token create '["eosio","1000000000.0000 SYS",0,0,0]' -p eosio.token
executed transaction: 8d7e472fa7309abfa051eed367c552e3e3e8f283530dc3375761d7da2cb298e9  120 bytes  453 us
#   eosio.token <= eosio.token::create          {"issuer":"eosio","maximum_supply":"1000000000.0000 SYS"}
>> warning: transaction executed locally, but may not be confirmed by the network yet


发布代币:

#cleos -u http://nodeos:80 push action eosio.token issue '["eosio","1000000000.0000 SYS","issue"]' -p eosio
cleos -u http://nodeos:80 push action eosio.token issue '["eosio","1000000000.0000 SYS","issue"]' -p eosio
executed transaction: bdf099816554de5849f030a1d26cc40ade29b74c87aff4180796c7c26507d677  128 bytes  343 us
#   eosio.token <= eosio.token::issue           {"to":"eosio","quantity":"1000000000.0000 SYS","memo":"issue"}
warning: transaction executed locally, but may not be confirmed by the network yet


查询代币余额:

#cleos -u http://nodeos:80 get currency balance eosio.token eosio
1000000000.0000 SYS


创建账号:

#cleos -u http://nodeos:80  create account eosio alex EOS6CYWY1q4AAsVV3rzcT1GGQmZcg7yDnA6jZx2KUjHvmZWPv8DQg EOS6CYWY1q4AAsVV3rzcT1GGQmZcg7yDnA6jZx2KUjHvmZWPv8DQg


EOS转账 :

#cleos -u http://nodeos:80  push action eosio.token transfer '["eosio", "alex", "10000.0000 SYS", ""]' -p eosio


3)创建BP,所谓BP就是EOS上的出块节点(block producer),到6月2日 EOS主网上线,全网应该一共21个BP节点,每个节点都要经过EOS的投票选举产生,现在我们的环境里还没有一个被选举的BP节点,所以需要通过抵押EOS的方式进行投票,当投票超过150M个EOS的时候,得票最多的前21个节点就是BP节点了,当然我们的BaaS受部署规模限制,不一样定要到21个节点,所以我们选举4个BP节点,执行如下命令:

部署系统合约
# cleos -u http://nodeos:80 set contract eosio eos/build/contracts/eosio.system
Reading WAST/WASM from ./build/contracts/eosio.system/eosio.system.wasm...
Using already assembled WASM...
Publishing contract...
executed transaction: cd1975d3628c1306f1fb58e49d5474d61c829bad42bffb4aeca7488bb041bd4d  40488 bytes  3969 us
#         eosio <= eosio::setcode               {"account":"eosio","vmtype":0,"vmversion":0,"code":"0061736d0100000001ba022f60027f7e0060067f7e7e7f7f...
#         eosio <= eosio::setabi                {"account":"eosio","abi":"0e656f73696f3a3a6162692f312e30050c6163636f756e745f6e616d65046e616d650f7065...
>> warning: transaction executed locally, but may not be confirmed by the network yet


创建账户bp1作为出块节点:

cleos -u http://nodeos:80 system newaccount eosio bp1 EOS7FNpU6P3yhT7bkf3fs7aNUxb3yNbMXPbn4nsYgh3ZkEhtchEAH EOS7FNpU6P3yhT7bkf3fs7aNUxb3yNbMXPbn4nsYgh3ZkEhtchEAH --stake-net '500.00 SYS' --stake-cpu '500.00 SYS'
1697946ms thread-0   main.cpp:419                  create_action        ] result: {"binargs":"0000000000ea3055000000000000423d00200000"} arg: {"code":"eosio","action":"buyrambytes","args":{"payer":"eosio","receiver":"bp1","bytes":8192}}
1697948ms thread-0   main.cpp:419                  create_action        ] result: {"binargs":"0000000000ea3055000000000000423d404b4c00000000000453595300000000404b4c0000000000045359530000000000"} arg: {"code":"eosio","action":"delegatebw","args":{"from":"eosio","receiver":"bp1","stake_net_quantity":"500.0000 SYS","stake_cpu_quantity":"500.0000 SYS","transfer":false}}
executed transaction: bbdeb7aa13953746e93c47b53729ce36ca2d89f16d87b872ebbacd71c2f4fc6b  336 bytes  1831 us
#         eosio <= eosio::newaccount            {"creator":"eosio","name":"bp1","owner":{"threshold":1,"keys":[{"key":"EOS7FNpU6P3yhT7bkf3fs7aNUxb3y...
>>
#         eosio <= eosio::buyrambytes           {"payer":"eosio","receiver":"bp1","bytes":8192}
>>
#         eosio <= eosio::delegatebw            {"from":"eosio","receiver":"bp1","stake_net_quantity":"500.0000 SYS","stake_cpu_quantity":"500.0000 ...
>> warning: transaction executed locally, but may not be confirmed by the network yet


导入刚才bp1的私钥:

# cleos -u http://nodeos:80 wallet import 5Je4PsFWinBPXgWQasz7usbv8MLu5AbkcjU41JQr4Suhh34yKPu
imported private key for: EOS7FNpU6P3yhT7bkf3fs7aNUxb3yNbMXPbn4nsYgh3ZkEhtchEAH


注册bp1为bp节点:

# cleos -u http://nodeos:80 system regproducer bp1 EOS7FNpU6P3yhT7bkf3fs7aNUxb3yNbMXPbn4nsYgh3ZkEhtchEAH http://bp1:8888
2155333ms thread-0   main.cpp:419                  create_action        ] result: {"binargs":"000000000000423d000336d1c7309f8c40f9a2d7bf11b6ba3f6b1c699a67d2ad26c003e5313ff1ce1c940f687474703a2f2f6270313a383838380000"} arg: {"code":"eosio","action":"regproducer","args":{"producer":"bp1","producer_key":"EOS7FNpU6P3yhT7bkf3fs7aNUxb3yNbMXPbn4nsYgh3ZkEhtchEAH","url":"http://bp1:8888","location":0}}
executed transaction: 294568630bd6c733db853ba2aac2245ffeb199d47767b80ddadc1cbf421c6d46  152 bytes  623 us
#         eosio <= eosio::regproducer           {"producer":"bp1","producer_key":"EOS7FNpU6P3yhT7bkf3fs7aNUxb3yNbMXPbn4nsYgh3ZkEhtchEAH","url":"http...
>> warning: transaction executed locally, but may not be confirmed by the network yet


eosio抵押EOS,投票给bp1:

#cleos -u http://nodeos:80 system delegatebw bp1 bp1 '100000000.0000 SYS' '50000000.0000 SYS' --transfer
460701ms thread-0   main.cpp:1083                 operator()           ] act_payload: {"from":"eosio","receiver":"bp1","stake_net_quantity":"100000000.0000 SYS","stake_cpu_quantity":"50000000.0000 SYS","transfer":true}
2460702ms thread-0   main.cpp:419                  create_action        ] result: {"binargs":"0000000000ea3055000000000000423d0010a5d4e800000004535953000000000088526a74000000045359530000000001"} arg: {"code":"eosio","action":"delegatebw","args":{"from":"eosio","receiver":"bp1","stake_net_quantity":"100000000.0000 SYS","stake_cpu_quantity":"50000000.0000 SYS","transfer":true}}
executed transaction: 98314189243cf8a38619e9f887c5430d9285f19a6a2977eccc07d838611dd17a  144 bytes  676 us
#         eosio <= eosio::delegatebw            {"from":"eosio","receiver":"bp1","stake_net_quantity":"100000000.0000 SYS","stake_cpu_quantity":"500...
>> warning: transaction executed locally, but may not be confirmed by the network yet}


用抵押的EOS投票给bp1:

#cleos -u http://nodeos:80 system voteproducer prods bp1 bp1
2531150ms thread-0   main.cpp:419                  create_action        ] result: {"binargs":"000000000000423d000000000000000001000000000000423d"} arg: {"code":"eosio","action":"voteproducer","args":{"voter":"bp1","proxy":"","producers":["bp1"]}}
executed transaction: 15c7cd6bd19f41654abb2d4f52f23953f80fd76b2698ef2a713d67c6fa022ee7  120 bytes  905 us
#         eosio <= eosio::voteproducer          {"voter":"bp1","proxy":"","producers":["bp1"]}
>> warning: transaction executed locally, but may not be confirmed by the network yet


到这一步一个最最基本的EOS BaaS就算部署完毕了。
此时你就可以在你的终端上看到类似如下输出:

3302000ms thread-0   producer_plugin.cpp:585       block_production_loo ] Produced block 00000d279a442987... #3367 @ 2018-05-22T15:55:02.000 signed by bp1 [trxs: 0, lib: 3366, confirmed: 0]


原文出处


0人推荐
随时随地看视频
慕课网APP