Python模块学习 - fabric

简介

fabric是一个Python的库,同时它也是一个命令行工具。使用fabric提供的命令行工具,可以很方便地执行应用部署和系统管理等操作。

fabric依赖于paramiko进行ssh交互,fabric的设计思路是通过几个API接口来完成所有的部署,因此fabric对系统管理操作进行了简单的封装,比如执行命令,上传文件,并行操作和异常处理等。

#安装
# fabric3支持python3
pip3 install fabric3

由于fabric比较特殊它还是一个命令行工具,可以通过help进行命令的了解

pyvip@Vip:~/utils$ fab --help
Usage: fab [options] <command>[:arg1,arg2=val2,host=foo,hosts='h1;h2',...] ...

Options:
  -h, --help            show this help message and exit
  -d NAME, --display=NAME
                        print detailed info about command NAME
  -F FORMAT, --list-format=FORMAT
                        formats --list, choices: short, normal, nested
  -I, --initial-password-prompt
                        Force password prompt up-front
  --initial-sudo-password-prompt
                        Force sudo password prompt up-front
  -l, --list            print list of possible commands and exit
  --set=KEY=VALUE,...   comma separated KEY=VALUE pairs to set Fab env vars
……

错误的提示

# fab -help
Traceback (most recent call last):
  File "/usr/local/python3/bin/fab", line 11, in <module>
    load_entry_point('Fabric==1.14.0', 'console_scripts', 'fab')()
  File "/usr/local/python3/lib/python3.6/site-packages/pkg_resources/__init__.py", line 480, in load_entry_point
    return get_distribution(dist).load_entry_point(group, name)
  File "/usr/local/python3/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2693, in load_entry_point
    return ep.load()
  File "/usr/local/python3/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2324, in load
    return self.resolve()
  File "/usr/local/python3/lib/python3.6/site-packages/pkg_resources/__init__.py", line 2330, in resolve
    module = __import__(self.module_name, fromlist=['__name__'], level=0)
  File "/usr/local/python3/lib/python3.6/site-packages/fabric/main.py", line 13, in <module>
    from operator import isMappingType
ImportError: cannot import name 'isMappingType'

# 说明你使用的python版本可fabric版本不同,python3 安装时使用的是fabric3

  

入门使用

fabric的典型使用方式就是,创建一个Python文件,该文件包含一到多个函数,然后使用fab命令调用这些函数。这些函数在fabric中成为task,下面是一个例子

from fabric.api import *
from fabric.contrib.console import confirm
from fabric.utils import abort
from fabric.colors import *

env.hosts = ['192.168.5.128']
env.port = 22
env.user = 'root'
env.password = 'mysql123'


def hostname():
    run('hostname')


def ls(path='.'):
    run('ls {0}'.format(path))


def tail(path='/etc/pas', line=10):
    run('tail -n {0} {1}'.format(line, path))


def hello():
    with settings(hide('everything'),warn_only=True):   # 关闭显示
        result = run('anetstat -lntup|grep -w 25')
        print(result)                                   # 命令执行的结果
        print(result.return_code)                       # 返回码,0表示正确执行,1表示错误
        print(result.failed)

  

PS:fab命令执行时,默认引用一个名为fabfile.py的文件,我们也可以通过-f来进行指定(文件名不能为abc.py,会冲突).

这里使用了三个fabric的封装:

  1. run:用于执行远程命令的封装
  2. sudo:以sudo权限执行远程命令
  3. env:保存用户配置的字典(保存了相关的配置,比如登录用户名env.user,密码env.password,端口env.port等,如果没有指定用户名那么默认使用当前用户,端口使用22)
 1、获取任务列表
pyvip@Vip:~/utils$ fab -f fab_utils.py --list
Available commands:

    hello
    hostname
    ls
    tail

pyvip@Vip:~/utils$ fab -f fab_utils.py --list
Available commands:

    hello
    hostname
    ls
    tail
# 2、执行hostname函数
pyvip@Vip:~/utils$ fab -f fab_utils.py hostname
[192.168.5.128] Executing task 'hostname'
[192.168.5.128] run: hostname
[192.168.5.128] out: china
[192.168.5.128] out: 


Done.
Disconnecting from 192.168.5.128... done.
# 3、多个参数的情况
pyvip@Vip:~/utils$ fab -f fab_utils.py ls:/
[192.168.5.128] Executing task 'ls'
[192.168.5.128] run: ls /
[192.168.5.128] out: bin  boot  data  dev  etc  home  lib  lib64  lost+found  media	misc  mnt  net	opt  proc  root  sbin  selinux	srv  sys  tmp  usr  var
[192.168.5.128] out: 


Done.
Disconnecting from 192.168.5.128... done.

需要注意的是:

  • 一次可以多个task,按照顺序执行: fab -f fab_util.py hostname ls
  • 给task传递参数使用task:参数,多个参数按照位置进行传递(和Python相同,对于关键字的参数可以,在命令行中指定:fab ls:path=/home)

fabric的命令行参数

fab命令作为fabric程序的入口提供了,丰富的参数调用.

# -l:查看task列表

# -f:指定fab的入口文件,默认是fabfile.py
# -g:指定网管设备,比如堡垒机环境下,填写堡垒机的IP
# -H:在命令行指定目标服务器,用逗号分隔多个服务器
# -P:以并行方式运行任务,默认为串行
# -R:以角色区分不同的服务
# -t:连接超时的时间,以秒为单位
# -w:命令执行失败时的警告,默认是终止任务
# -- Fabric参数,其他包含fabric脚本的中的参数的快捷操作,比如--user,--port,或者直接跟要执行的Linux命令

  如下例子,不写一行代码获取所有主机的ip地址

pyvip@Vip:~/utils$ fab -H 192.168.5.128 --port 22 --user='root' --password='mysql123' -- 'ip a '
[192.168.5.128] Executing task '<remainder>'
[192.168.5.128] run: ip a 
[192.168.5.128] out: 1: lo: <LOOPBACK,UP,LOWER_UP> mtu 16436 qdisc noqueue state UNKNOWN 
[192.168.5.128] out:     link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
[192.168.5.128] out:     inet 127.0.0.1/8 scope host lo
[192.168.5.128] out:     inet6 ::1/128 scope host 
[192.168.5.128] out:        valid_lft forever preferred_lft forever
[192.168.5.128] out: 2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UP qlen 1000
[192.168.5.128] out:     link/ether 00:0c:29:96:0a:a0 brd ff:ff:ff:ff:ff:ff
[192.168.5.128] out:     inet 192.168.5.128/24 brd 192.168.5.255 scope global eth0
[192.168.5.128] out:     inet6 fe80::20c:29ff:fe96:aa0/64 scope link 
[192.168.5.128] out:        valid_lft forever preferred_lft forever
[192.168.5.128] out: 3: pan0: <BROADCAST,MULTICAST> mtu 1500 qdisc noop state DOWN 
[192.168.5.128] out:     link/ether 7a:4d:51:6c:c2:cd brd ff:ff:ff:ff:ff:ff

 

常用的对象和方法介绍 

介绍fabric中的env对象,以及其他的比如执行命令模块,上传文件等。

fabric中的env

 env是一个全局唯一的字典,保存了Fabric所有的配置,在Fabric的实现中,他是一个_AttributeDict()对象,之所以封装成_AttributeDict()对象,是覆盖了__getattr__和__setattr__,使我们可以使用“对象.属性=值”的方式,操作字典。

 我们可以通过源码的方式,查看env的配置参数,或者使用如下方式查看:

import json
from fabric.api import env

print(json.dumps(env, indent=3))


def hell(name='world'):
    print('hello %s' % name)


-----------------------------------------------
结果


pyvip@Vip:~/utils$ fab -f fab_utils.py -l
{
   "show": null,
   "": true,
   "sudo_user": null,
   "default_port": "22",
   "key_filename": null,
   "path": "",
   "hosts": [
      "192.168.5.128"
   ],
   "host_string": null,
   "ok_ret_codes": [
      0
   ],
   "always_use_pty": true,
   "fabfile": "fab_utils.py",
   "echo_stdin": true,
   "again_prompt": "Sorry, try again.",
   "command": null,
   "forward_agent": false,
   "command_prefixes": [],
   "cwd": "",
   "connection_attempts": 1,
   "linewise": false,
   "gateway": null,
   "use_exceptions_for": {
      "network": false
……

 

常用的env配置如下:

  • env.hosts:定义目标服务器列表
  • env.exclude_hosts:排除特定的服务器
  • env.user SSH:到远程服务器的用户名
  • env.port:远程服务器的端口号
  • env.key_filename:私钥文件的位置
  • env.password SSH:到远程服务器的密码

针对不同主机不同密码的情况,可以使用如下的方式:

env.hosts = [
    'root@192.168.10.201:22',
    'root@192.168.10.202:22',
    'root@192.168.10.203:22'
]
env.passwords = {
    'root@192.168.10.201:22':'123456201',
    'root@192.168.10.202:22':'123456202',
    'root@192.168.10.203:22':'123456203'

  

fabric提供的命令

run():在远程服务器上执行Linux命令,还有一个重要的参数pty,如果我们执行命令以后需要有一个常驻的服务进程,那么就需要设置pty=False,避免因为Fabric退出导致进程的退出

run('service mysqld start',pty=False)

 PS:执行完毕会返回输出的信息,我们可以定义变量接受,同时这个返回信息有一个方法return_code,当返回的是正确执行的结果时code为0,否则不为0

def hello():

    with settings(hide('everything'),warn_only=True):   # 关闭显示
        result = run('anetstat -lntup|grep -w 25')
        print(result)                                   # 命令执行的结果
        print(result.return_code)                       # 返回码,0表示正确执行,1表示错误

结果

[192.168.5.128] Executing task 'hello'
/bin/bash: anetstat: command not found
1
True

Done.
Disconnecting from 192.168.5.128... done.

sudo():与run类似,使用管理员权限在远程服务器上执行shell命令,还有一个重要的参数pty,如果我们执行命令以后需要有一个常驻的服务进程,那么就需要设置pty=False,避免因为Fabric退出导致进程的退出。

local():用以执行本地命令,返回要执行的命令,local是对Python的Subprocess模块的封装,更负载的功能可以直接使用Subprocess模块,包含capture参数,默认为False,表示subprocess输出的信息进行显示,如果不想显示,那么指定capture=True即可

 

ef test():
    result = local('make test',capture=True)
    print(result)
    print(result.failed)
    print(result.succeeded)
 
# 返回执行的命令
# 如果执行失败那么 result.failed 为True
# 如果执行成功那么 result.succeeded 为True

get():从远程服务器上获取文件,通过remote_path参数声明从何处下载,通过local_path表示下载到何处。remote_path支持通配符。

get(remote_path='/etc/passwd',local_path='/tmp/passwd')

put():将本地的文件上传到远程服务器,参数与get相似,此外,还可以通过mode参数执行远程文件的权限配置。

get(remote_path='/etc/passwd',local_path='/tmp/passwd')

  

reboot():重启远程服务器,可以通过wait参数设置等待几秒钟重启

	
reboot(wait=30)

propmt():用以在Fabric执行任务的过程中与管理员进行交互,类似于python的input

key = prompt('Please specify process nice level:',key='nice',validate=int)
# 会返回采集到的key

  

fabric的上下文管理器

env中存储的是全局配置,有时候我们并不希望修改全局配置参数,只希望临时修改部分配置,例如:修改当前工作目录,修改日志输出级别等。

在fabric中我们可以通过上下文管理器临时修改参数配置,而不会影响全局配置。当程序进入上下文管理器的作用域时,临时修改就会起作用;当程序离开上下文管理器时,临时修改就会消失。

cd():切换远程目录

def change(dir='/tmp'):
    with cd(dir):
        run('pwd')     # /tmp
    run('pwd')         # /root

lcd():切换本地目录

path():配置远程服务器PATH环境变量,只对当前会话有效,不会影响远程服务器的其他操作,path的修改支持多种模式

  • append:默认行为,将给定的路径添加到PATH后面。
  • prepend:将给定的路径添加到PATH的前面。
  • replace:替换当前环境的PATH变量。
def addpath():
    with path('/tmp','prepend'):
        run("echo $PATH")
    run("echo $PATH")

prefix():前缀,它接受一个命令作为参数,表示在其内部执行的代码块,都要先执行prefix的命令参数。

def testprefix():
    with cd('/tmp'):
        with prefix('echo 123'):
            run('echo 456')
            run('echo 789')
 
# 转换为Linux命令为:
cd /tmp && echo '123' && echo '456'
cd /tmp && echo '123' && echo '789' 

shell_env():设置shell脚本的环境变量 

def setenv():
    with shell_env(HTTP_PROXY='1.1.1.1'):
        run('echo $HTTP_PROXY')
    run('echo $HTTP_PROXY')
 
# 等同于shell中的export
export HTTP_PROXY='1.1.1.1'

settings():通用配置,用于临时覆盖env变量

def who():
    with settings(user='dev'):    # 临时修改用户名为dev
        run('who')
    run('who')

 

remote_tunnel():通过SSH的端口转发建立的链接

with remote_tunnel(3306):
    run('mysql -uroot -p password')

  

hide():用于隐藏指定类型的输出信息,hide定义的可选类型有7种

  • status:状态信息,如服务器断开链接,用户使用ctrl+C等,如果Fabric顺利执行,不会有状态信息
  • aborts:终止信息,一般将fabric当作库使用的时候需要关闭
  • warnings:警告信息,如grep的字符串不在文件中
  • running:fabric运行过程中的数据
  • stdout:执行shell命令的标准输出
  • stderr:执行shell命令的错误输出
  • user:用户输出,类似于Python中的print函数

为了方便使用,fabric对以上其中类型做了进一步的封装

  • output:包含stdout,stderr
  • everything:包含stdout,stderr,warnings,running,user
  • commands:包含stdout,running

show():与hide相反,表示显示指定类型的输出

def hello():
    with settings(show('everything'),warn_only=True):   # 显示所有
        result = run('netstat -lntup|grep')
        print('1='+result)                                   # 命令执行的结果
        print('2='+str(result.return_code))                  # 返回码,0表示正确执行,1表示错误
        print('3='+str(result.failed))

结果

pyvip@Vip:~/utils$ fab -f fab_utils.py hello
[192.168.5.128] Executing task 'hello'
[192.168.5.128] run: netstat -lntup|grep
[192.168.5.128] out: 用法: grep [选项]... PATTERN [FILE]...
[192.168.5.128] out: 试用‘grep --help’来获得更多信息。
[192.168.5.128] out: 


Warning: run() received nonzero return code 2 while executing 'netstat -lntup|grep'!

NoneType


1=用法: grep [选项]... PATTERN [FILE]...
试用‘grep --help’来获得更多信息。
2=2
3=True

Done.

 

quiet():隐藏全部输出,仅在执行错误的时候发出告警信息,功能等同于 with settings(hide('everything'),warn_only=True) .

# 比如创建目录的时候,如果目录存在,默认情况下Fabric会报错退出,我们是允许这种错误的,所以针对这种错误,我们进行如下设置,使fabric只打出告警信息而不会中断执行。
with settings(warn_only=True)

  

装饰器

Fabric提供的命令一般都是执行某一个具体的操作,提供的上下文管理器一般都是用于临时修改配置参数,而fabric提供的装饰器,既不是执行具体的操作,也不是修改参数,而是控制如何执行这些操作,在那些服务器上执行这些操作,fabric的装饰器与人物执行紧密相关。下面从几个方面来进行说明

  • hosts:定制执行task的服务器列表
  • roles:定义执行task的role列表
  • parallel:并行执行task
  • serial:串行执行task
  • task:定义一个task
  • runs_once:该task只执行一次

fabric的task

 

task就是fabric需要在远程服务器上执行的函数,在fabric中有3中方法定义一个task

  1. 默认情况下,fabfile中每一个函数都是一个task。
  2. 继承自fabric的task类,这种方式比较难用,不推荐。
  3. 使用fabric的task装饰器,这是使用fabric最快速的方式,也是推荐的用法。
from fabric.api import *
 
env.user='root'
env.password='mysql123'
 
@task
def hello():
    run('echo hello')
 
def world():
    run('echo world')

  PS:默认情况下,fabfile中的所有函数对象都是一个task,但是如果我们使用了task装饰器,显示的定义了一个task,那么,其他没有通过task装饰器装饰的函数将不会被认为是一个task。

fabric的host

为了方便我们的使用,fabric提供了非常灵活的方式指定对哪些远程服务器执行操作,根据我们前面的知识,我们知道有两种方式:通过env.hosts来执行,或者在fab执行命令的时候使用-H参数,除此之外,还有以下需要注意的地方

  1. 指定host时,可以同时指定用户名和端口号: username@hostname:port
  2. 通过命令行指定要多哪些hosts执行人物:fab mytask:hosts="host1;host2"
  3. 通过hosts装饰器指定要对哪些hosts执行当前task
  4. 通过env.reject_unkown_hosts控制未知host的行为,默认True,类似于SSH的StrictHostKeyChecking的选项设置为no,不进行公钥确认。
from fabric.api import *
 
env.hosts = [
    'root@192.168.10.201:22',
    'root@192.168.10.202:22',
    'root@192.168.10.203:22'
]
env.passwords = {
    'root@192.168.10.201:22':'123456201',
    'root@192.168.10.202:22':'123456202',
     'root@192.168.10.203:22':'123456203'
}
 
@hosts('root@192.168.10.201:22')
@task
def hello():
    run('ifconfig br0')
 
 
# 命令行的方式:
fab hello:hosts="root@192.168.10.201;root@192.168.10.202"

  

fabric的role

role是对服务器进行分类的手段,通过role可以定义服务器的角色,以便对不同的服务器执行不同的操作,Role逻辑上将服务器进行了分类,分类以后,我们可以对某一类服务器指定一个role名即可。进行task任务时,对role进行控制。

# role在env.roledefs中进行定义
env.roledefs = {
    'web':['root@192.168.10.201','192.168.10.202']    # role名称为:web
    'db':['root@192.168.10.203',]                     # role名称为:db
}
  当我们定义好role以后,我们就可以通过roles装饰器来指定在哪些role上运行task。


from fabric.api import *
 
env.roledefs = {
    'web':['root@192.168.10.201:22','root@192.168.10.202:22',],
    'db':['root@192.168.10.203:22',]
}
env.passwords = {
    'root@192.168.10.201:22':'123456201',
    'root@192.168.10.202:22':'123456202',
    'root@192.168.10.203:22':'123456203'
}
 
@roles('db')       # 只对role为db的主机进行操作
@task
def hello():
    run('ifconfig br0')

 注意:hosts装饰器可以和roles装饰器一起使用(全集),看起来容易造成混乱,不建议混搭。

 

fabric的执行模型

 

fabric执行任务的步骤如下:

  1. 创建任务列表,这些任务就是fab命令行参数指定的任务,fab会保持这些任务的顺序
  2. 对于每个任务,构造需要执行该任务的服务器列表,服务器列表可以通过命令行参数指定,或者env.hosts指定,或者通过hosts和roles装饰器指定
  3. 遍历任务列表,对于每一台服务器分别执行任务,可以将任务列表和服务器列表看作是两个for循环,任务列表是外层循环,服务器列表是内存循环,fabric默认是串行执行的可以通过装饰器或者命令行参数确定任务执行的方式
  4. 对于没有指定服务器的任务默认为本地任务,仅执行一次

PS:关于并行模式:

  1. 通过命令行参数-P(--parallel)通知Fabric并行执行task
  2. 通过env.parallel设置设否需要并行执行
  3. 通过parallel装饰器通知Fabric并行执行task,它接受一个pool_size作为参数(默认为0),表示可以有几个任务并行执行

其他装饰器

前面介绍了task,hosts,roles和parallel装饰器,此外还有两个装饰器比较常用

  • runs_once:只执行一次,防止task被多次调用。例如,对目录打包进行上传,上传动作对不同的服务器可能会执行多次,但是打包的动作只需要执行一次即可。
  • serial:强制当前task穿行执行。使用该参数时优先级最高,即便是制定了并发执行的参数

常用的功能函数

fabric中还有其他的一些好用的函数

封装task

 fabric提供了一个execute函数,用来对task进行封装。它最大的好处就是可以将一个大的任务拆解为很多小任务,每个小任务互相独立,互不干扰

from fabric.api import *
 
env.roledefs = {
    'web':['root@192.168.10.201:22','root@192.168.10.202:22',],
    'db':['root@192.168.10.203:22',]
}
env.passwords = {
    'root@192.168.10.201:22':'123456201',
    'root@192.168.10.202:22':'123456202',
    'root@192.168.10.203:22':'123456203'
}
 
@roles('db')
def hello():
    run('echo hello')
 
@roles('web')
def world():
    run('echo world')
 
@task
def helloworld():
    execute(hello)    
    execute(world)

 # 函数helloworld作为入口,分别调用两个task,对不同的主机进行操作

 

utils函数

 

包含一些辅助行的功能函数,这些函数位于fabric.utils下,常用的函数如下:

  1. abort:终止函数执行,打印错误信息到stderr,并且以退出码1退出。
  2. warn:输出警告信息,但是不会终止函数的执行
  3. puts:打印输出,类似于Python中的print函数
def helloworld():
    execute(hello)
    abort('----->abort')     # 执行到这里时,直接退出
    warn('----->warn')       # 会发出提示信息,不会退出
    puts('----->puts')       # 会打印括号中的信息
    execute(world)

  

带颜色的输出 

 fabric为了让输出日志更具有可读性,对命令行中断的颜色输出进行了封装,使用print打印带有不同颜色的文本,这些颜色包含在fabric.colors中。像warn,puts打印输出的,也可以直接渲染颜色

  • blue(text,blod=False)  蓝色
  • cyan(text,blod=False)  淡蓝色
  • green(text,blod=False)  绿色
  • magenta(text,blod=False)  紫色
  • red(text,blod=False)  红色
  • white(text,blod=False)  白色
  • yellow(text,blod=False)   黄色
def ls(path='.'):
    run('ls {0}'.format(path))

def hello():

    execute(hell)  # task任务hell
    warn(yellow('----->warn'))  # 会发出提示信息,不会退出
    puts(green('----->puts'))  # 会打印括号中的信息
    execute(ls) # task任务ls
    print(green('the text is green')) # 单纯的渲染文字: 

def hell(name='world'):
    print('hello %s' % name)

 

 

  

确认信息

有时候我们在某一步执行错误,会给用户提示,是否继续执行时,confirm就非常有用了,它包含在 fabric.contrib.console中

def testconfirm():

    result = confirm('Continue Anyway?')
    print(result)
 
 
# 会提示输入y/n
# y 时 result为True
# n 时 result为False

  

使用Fabric源码安装redis

下载一个redis的包和fabfile.py放在同级目录即可,不同目录需要修改包的位置,这里使用的是redis-4.0.9版本。

#!/usr/bin/env python3
from fabric.api import *
from fabric.contrib.console import confirm
from fabric.utils import abort
from fabric.colors import *
 
env.hosts = ['192.168.10.202',]
env.user = 'root'
env.password = '123456202'
 
@runs_once
@task
def test():
    with settings(warn_only=True):
        local('tar xf redis-4.0.9.tar.gz')
        with lcd('redis-4.0.9'):
            result = local('make test',capture=True)
            if result.failed and not confirm('Test is Faild Continue Anyway?'):
                abort('Aborting at user request.')
 
    with lcd('redis-4.0.9'):
        local("make clean")
    local('tar zcvf redis-4.0.10.tar.gz redis-4.0.9')
 
@task
def deploy():
    put('redis-4.0.10.tar.gz','/tmp/')
    with cd('/tmp'):
        run('tar xf redis-4.0.10.tar.gz')
        with cd('redis-4.0.9'):
            sudo('make install')
 
@task
def start_redis():
    with settings(warn_only=True):
        result = run('netstat -lntup | grep -w redis-server')
        if result.return_code == 0:
            print(green('redis is started!'))
        else:
            run('set -m ; /usr/local/bin/redis-server &')   # 用pty=False, fabric进程退不出来,不知道为啥,所以这里用set -m
            print(green('redis start Successful'))
 
@task
def clean_local_file():
    local('rm -rf redis-4.0.10.tar.gz')
 
@task
def clean_file():
    with cd('/tmp'):
        sudo('rm -rf redis-4.0.9')
        sudo('rm -rf redis-4.0.10.tar.gz')
 
@task
def install():
    execute(test)
    execute(deploy)
    execute(clean_file)
    execute(clean_local_file)
  execute(start_redis)

  

PS:关于set -m 的作用如下:

"set -m" turns on job control, you can run processes in a separate process group.
理解:在一个独立的进程组里面运行我们的进程。

  参考资料

http://www.cnblogs.com/dachenzi/p/8695330.html

posted @ 2018-06-02 02:46  一只小小的寄居蟹  阅读(11743)  评论(0编辑  收藏  举报