最近都在狂写脚本,好像变成 半个运维
一样…
刚好有需求写些部署工具,于是了解到了fabric这个模块。下面简单带过一下
什么是Fabric
引用fabric主页的介绍
Fabric is a high level Python (2.7, 3.4+) library designed to execute shell commands remotely over SSH, yielding useful Python objects in return
意思就是Fabric是基于SSH的远程执行命令,并返回可调用的python对象的框架
Fabric2.x
的版本与1.x
相比,除了支持python3
之外,还做了很多改动。网上很多博客都写的是1.x
的版本,参考时要注意。
这里主要分析2.x
的版本
安装什么的就不废话了,下面来用一下
参照他的示例:
1
2
3
4
5
6
7
from fabric import Connection
'web1.example.com').run('uname -s') result = Connection(
"Ran {.command!r} on {.host}, got this stdout:\n{.stdout}" msg =
print(msg.format(result))
Ran "uname -s" on web1.example.com, got this stdout:
Linux
一目了然
下面在公司服务器上测试一下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# 服务器ip: 10.10.22.13
# 用户: root
from fabric import Connection
'10.10.22.13', user='root').run('uname -s') result = Connection(
"Ran {.command!r} on {.host}, got this stdout:\n{.stdout}" msg =
print(msg.format(result))
Traceback (most recent call last):
File "/Users/yang/workspace/PycharmProjects/FP-project/inventory_allocate/test/fabric_test.py", line 10, in <module>
result = Connection("10.10.22.13", user='root').run("uname -s")
File "<decorator-gen-3>", line 2, in run
File "/Users/yang/anaconda3/lib/python3.6/site-packages/fabric/connection.py", line 29, in opens
self.open()
File "/Users/yang/anaconda3/lib/python3.6/site-packages/fabric/connection.py", line 501, in open
self.client.connect(**kwargs)
File "/Users/yang/anaconda3/lib/python3.6/site-packages/paramiko/client.py", line 424, in connect
passphrase,
File "/Users/yang/anaconda3/lib/python3.6/site-packages/paramiko/client.py", line 715, in _auth
raise SSHException('No authentication methods available')
paramiko.ssh_exception.SSHException: No authentication methods available
呵呵,我他妈就知道,代码里一行ssh的参数毛都没见到,这么容易连上就有鬼了(微笑
没的说,填坑要紧
源码分析
分析报错
1
2
3
4
5
6
7
8
9
10
11
12
13
Traceback (most recent call last):
File "/Users/yang/workspace/PycharmProjects/FP-project/inventory_allocate/test/fabric_test.py", line 10, in <module>
result = Connection("10.10.22.13", user='root').run("uname -s")
File "<decorator-gen-3>", line 2, in run
File "/Users/yang/anaconda3/lib/python3.6/site-packages/fabric/connection.py", line 29, in opens
self.open()
File "/Users/yang/anaconda3/lib/python3.6/site-packages/fabric/connection.py", line 501, in open
self.client.connect(**kwargs)
File "/Users/yang/anaconda3/lib/python3.6/site-packages/paramiko/client.py", line 424, in connect
passphrase,
File "/Users/yang/anaconda3/lib/python3.6/site-packages/paramiko/client.py", line 715, in _auth
raise SSHException('No authentication methods available')
paramiko.ssh_exception.SSHException: No authentication methods available
我们可以看到调用堆栈上的错误回溯,定位到line
501
,在实例化Connection
对象后调用client.connect(**kwargs)
时抽了…
直接摸过去,从Connection
类出发往他祖宗上刨,下面先给出继承关系和Connection
的关键部分,然后逐块拆分说明:
1 |
class Connection(Context): |
重要的成员变量
1 |
host = None # 主机名或IP地址: www.host.com, 66.66.66.66 |
构造函数参数
1 |
host |
这些就是我们在实例化Connection
对象时可以控制的一些部分,比较重要的有config
和connection_kwargs
构造函数主体
config
1 |
super(Connection, self).__init__(config=config) |
config
成员变量是一个Config
对象,它是调用父类Context.__init__()
方法来初始化的。Context.__init__()
定义如下:
1
2
3
4
5
6
7
8
9
10
class Context(DataProxy):
def __init__(self, config=None):
config = config if config is not None else Config()
self._set(_config=config)
command_prefixes = list()
self._set(command_prefixes=command_prefixes)
command_cwds = list()
self._set(command_cwds=command_cwds)
具体过程是Context.__init__()
初始化时调用_set()
绑定了Config
成员对象_config
:
1
2
3
4
5
def _set(self, *args, **kwargs):
if args:
object.__setattr__(self, *args)
for key, value in six.iteritems(kwargs):
object.__setattr__(self, key, value)
再通过加了@property
的config()
函数,使得connection
对象能直接用self.config
来引用_config
:
1
2
3
4
5
6
7
def config(self):
return self._config
def config(self, value):
self._set(_config=value)
host, user, port
1 |
shorthand = self.derive_shorthand(host) |
这段是处理host
参数的。host
可以有下面集中传入形式:
1
2
3
4
user@host:port # 例如: root@10.10.10.10:6666
user@host # 例如: root@10.10.10.10
host:port # 例如: 10.10.10.10:6666
host # 例如: 10.10.10.10
前三种会调用self.derive_shorthand(host)
分别解析出self.host
,self.user
和self.port
,最后一种需单独传入user
,port
。
如果用前三种传入方式的话,记得不要再重复传入user
或port
了,会抛出异常
源码是真他妈长啊,注释就占了一两百行 (微笑
ok,初始化的参数我们先到这里,因为它给的示例也就只有个host
而已,一会再讲config_kwags
和深入下config
。我们现在先去报错的地方:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
kwargs = dict(
self.connect_kwargs,
username=self.user,
hostname=self.host,
port=self.port,
)
if self.gateway:
kwargs["sock"] = self.open_gateway()
if self.connect_timeout:
kwargs["timeout"] = self.connect_timeout
# Strip out empty defaults for less noisy debugging
if "key_filename" in kwargs and not kwargs["key_filename"]:
del kwargs["key_filename"]
# Actually connect!
self.client.connect(**kwargs) # 就是你了
看到最后一行,传参时将字典kwargs
传了过去,kwargs
里除了usr
,host
,port
之外,还有一个connect_kwargs
。我们看看client.connect()
的定义:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
def connect(
self,
hostname,
port=SSH_PORT,
username=None,
password=None, # 你
pkey=None, # 你
key_filename=None, # 还有你
timeout=None,
allow_agent=True,
look_for_keys=True,
compress=False,
sock=None,
gss_auth=False,
gss_kex=False,
gss_deleg_creds=True,
gss_host=None,
banner_timeout=None,
auth_timeout=None,
gss_trust_dns=True,
passphrase=None,
)
看到6, 7, 8
行没?that’s it.
接下来我们改写一下一开始的示例:
-
使用
password
:1
2
3
4
5
6from fabric import Connection
# my_password为10.10.22.13的root用户密码
'10.10.22.13', user='root', connect_kwargs={'password': '${my_password}'}) conn = Connection(
"uname -s") conn.run(
Linux -
使用
key_filename
:1
2
3
4
5
6
7from fabric import Connection
# 使用key_filename参数前提需将你的ssh公钥(.pub后缀)添加到远程服务器的.ssh/authorized_keys file里
# id_rsa为私钥
'10.10.22.13', user='root', connect_kwargs={'key_filename': '${path to local .ssh dir}/${your id_rsa file}'}) conn = Connection(
"uname -s") conn.run(
Linux
朋友们,让我们举杯庆祝一下吧
connect_kwargs
我们趁此机会窥视一下connect_keargs
的相关部分
1
self.connect_kwargs = self.resolve_connect_kwargs(connect_kwargs)
我们看到,当connect_kwargs
为None
时,会通过config
成员变量动态增加属性connect_kwargs
属性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def resolve_connect_kwargs(self, connect_kwargs):
if connect_kwargs is None:
connect_kwargs = self.config.connect_kwargs # 调用__getattr__()动态增加属性
elif "key_filename" in self.config.connect_kwargs:
kwarg_val = connect_kwargs.get("key_filename", [])
conf_val = self.config.connect_kwargs["key_filename"]
connect_kwargs["key_filename"] = conf_val + kwarg_val
if "identityfile" in self.ssh_config:
connect_kwargs.setdefault("key_filename", [])
connect_kwargs["key_filename"].extend(
self.ssh_config["identityfile"]
)
return connect_kwargs
在__getattr__
方法里又调用了类方法_get()
将connect_kwargs
传到key
:
1
2
3
4
5
6
7
8
9
10
11
12
13
def __getattr__(self, key):
try:
return self._get(key) # 调用_get()
except KeyError:
if key in self._proxies:
return getattr(self._config, key)
err = "No attribute or config key found for {!r}".format(key)
attrs = [x for x in dir(self.__class__) if not x.startswith('_')]
err += "\n\nValid keys: {!r}".format(
sorted(list(self._config.keys()))
)
err += "\n\nValid real attributes: {!r}".format(attrs)
raise AttributeError(err)
调用_get()
后返回一个DataProxy
对象value
:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def _get(self, key):
if key in (
'__setstate__',
):
raise AttributeError(key)
value = self._config[key]
if isinstance(value, dict):
keypath = (key,)
if hasattr(self, '_keypath'):
keypath = self._keypath + keypath
root = getattr(self, '_root', self)
value = DataProxy.from_data(
data=value,
root=root,
keypath=keypath,
)
return value
是不是晕了?呵呵没关系,我他妈也是。就让他随风而去吧
fab命令
安装完fabric
后会连同fab
工具一起装到python/bin
下,在终端输入fab
-h
查看命令参数
简单来说,fab
干这么件事,直接执行当前目录下的fabfile.py
脚本里的函数,下面介绍具体如何写fabfile.py
首先按照国际惯例,导入包:
1
2
from fabric import Connection
from invoke import task
实例化COnnection
对象:
1
2
# ${}里的内容自行填充
conn = Connection("${remote host}", user='${remote user}', connect_kwargs={'password': "${remote user's password}"})
定义一个功能函数:
1
2
3
def execute(c):
conn.run("uname -s")
这个@task
装饰器是必须加的,保证fab
能直接执行,参数c
不用管它,但不能定义成与你实例化的conn
同名,原因后面会说
保存为fabfile.py
:
1
2
3
4
5
6
7
8
from fabric import Connection
from invoke import task
conn = Connection("${remote host}", user='${remote user}', connect_kwargs={'password': "${remote user's password}"})
def execute(c):
conn.run("uname -s")
在终端执行(服务器系统为Linux):
1
2
fab execute
Linux # Linux下的输出
说回参数c
,c
在这里实际上是本地连接,是localhost
的一个Connection
对象。我们可以试一下:
1
2
3
4
5
6
from fabric import Connection
from invoke import task
def execute(c):
c.run("uname -s")
在终端执行(我的系统为Mac):
1
2
fab execute
Darwin # Mac下的输出
一目了然
到这里大家应该大致明白fab
是干啥的了。这样的玩法就多了,下面举几个例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from fabric import Connection
from invoke import task
conn = Connection("${remote host}", user='${remote user}', connect_kwargs={'password': "${remote user's password}"})
def uname_local(c):
c.run("uname -s")
# 列出某路径下的文件
def ls_remote(c, dir_path):
with conn.cd(dir_path):
conn.run("ls -la")
# 还能在函数里实例化Connection对象
def uname_rmt(c, host, user, password):
con = Connection(host, user=user, connect_kwargs={'password': password})
con.run("uname -s")
执行:
1
2
3
4
5
6
7
8
9
fab uname_local
Darwin
fab ls_remote /home
trendy
td_root
fab uname_rmt 10.10.22.13 root ******* # 这个密码就不放出来了
Linux
具体的Connection
还封装了哪些命令,就需要大家在使用中探索了
fab 1.x 与 fab 2.x
fab 1.x
与fab 2.x
最大的不同就是fab
2.x
没有了env
模块,所有的操作都基于Connection
对象完成,对于fab
命令的调用,将暴露方法封装到invoke
模块,使其独立出来。这是我觉得最大的不同。只是现在还没有很多的博客写到fab
2.x
的一些特性,官方文档也很简单,还是需要大家花时间阅读下源码才能清楚里面的逻辑
有时间的话会再对fabric
做个详细剖析