有关SaltStack的State错误的执行在其他主机上

背景问题

我们的运维平台是基于 SaltStack 封装的任务系统.
在任务系统上将需要执行多个操作转为不同的 state
SaltStack 负责进行调度执行, Minion 执行完了之后, 会将数据结果返回给 master
而 master 则将结果保存在 mongodb 里面, 看图

edbf79382966e44f0cf30fced067c217.png

然而, 在我们使用的时候出现个奇怪的问题

3cdf9b3845bc75db3b5aa060195e3437.png

第一个主机上的 state 执行是成功
但是莫名其妙会在第二个个主机上执行 state
幸好没执行成功, 不然尽给我瞎整.

分析排查

基本测试

先看看这个主机在 master 上是否能正常连接.
执行命令salt "VM_0_7_centos" test.ping
结果显示无法连接

再看看 salt-minon 的 key 是否还存在
执行命令salt-key -L | grep "VM_0_7_centos"
结果显示, 这台主机 salt-minion 的 key 并不存在

这就很尴尬了, 为什么一台不存在的主机会被state.sls执行到.
关键还不是必现的问题, 不好排查.

查看源码

这个问题奇怪的很, 而且不是必现.
网上也找不到什么有用的信息.

好在 SaltStack 是一个开源的项目.
有什么不清楚的, 我们可以直接看源码来排查.

其实对 SaltStack 不是很了解, 代码太长, 我们直接找到问题的核心代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
#
# 我们使用的是salt -S '10.0.0.1' state.sls的方式执行的
# 所以问题肯定出在查找minions的时候
# 直接看_check_ipcidr_minions方法就行了
#
# 从上层调用的方法来看, greedy 参数默认是True
# 所以每次执行必定是`贪婪模式`
#
def _check_ipcidr_minions(self, expr, greedy):
'''
Return the minions found by looking via ipcidr
'''
cache_enabled = self.opts.get('minion_data_cache', False)

if greedy:
# 如果是贪婪模式, 从/etc/salt/pki/minions/里面获取minions
minions = self._pki_minions()
elif cache_enabled:
# 如果非贪婪模式, 且打开了缓存, 则从/var/cache/salt/master/minions/获取minions
minions = self.cache.list('minions')
else:
return {'minions': [],
'missing': []}

# 从函数整体来看
# minions是最终返回的列表, 称为返回列表
# cminions是用来遍历的列表, 称为遍历列表
# 因为不能一边遍历一个对象一边修改一个对象
if cache_enabled:
if greedy:
cminions = self.cache.list('minions')
else:
cminions = minions
if cminions is None:
return {'minions': minions,
'missing': []}

tgt = expr
try:
# Target is an address?
tgt = ipaddress.ip_address(tgt)
except Exception:
try:
# Target is a network?
tgt = ipaddress.ip_network(tgt)
except Exception:
log.error('Invalid IP/CIDR target: %s', tgt)
return {'minions': [],
'missing': []}
proto = 'ipv{0}'.format(tgt.version)

# 使用set对list类型去重
minions = set(minions)

# 遍历的列表是从`/var/cache/salt/master/minions/`获取
# 返回的列表如果是贪婪模式
# 从`/etc/salt/pki/minions/`获取
# 返回的列表如果非贪婪模式
# 从`/var/cache/salt/master/minions/`获取
# 所以在贪婪模式下, 如果遍历列表和返回列表不一致
# 则最终会导致多出很多不是我们想要的minion
#
for id_ in cminions:
mdata = self.cache.fetch('minions/{0}'.format(id_), 'data')
if mdata is None:
if not greedy:
minions.remove(id_)
continue
grains = mdata.get('grains')
if grains is None or proto not in grains:
match = False
elif isinstance(tgt, (ipaddress.IPv4Address, ipaddress.IPv6Address)):
match = six.text_type(tgt) in grains[proto]
else:
match = salt.utils.network.in_subnet(tgt, grains[proto])

if not match and id_ in minions:
minions.remove(id_)

return {'minions': list(minions),
'missing': []}

最终发现
/var/cache/salt/master/minions/目录里没有相应 minion 的 cache 信息
/etc/salt/pki/minions/上却有相应的 minion 密钥

复盘思考

问题的思考

  1. 因为主机下线后没有清掉相应的 key 文件导致的
  2. 使用了 IP 匹配模式来查找 minion
  3. IP 匹配模式默认使用贪婪模式

很明显, 不了解的原理导致的问题, 反省, 反省, 反省.

设计的思考

我们原本理解的直接指定 IP 地址效率会更高(主机之间通过 IP 地址通过)
而且主机名会变, 但 IP 地址一般来说不会变, 所以我们使用 IP 地址来执行
然而从代码上来看并不是这样的

在 salt-master 寻找 minion 时通过 minion 的 IP 地址的来过滤相应的 minion
这个实现的过滤方式还有点奇怪

一般来说, 我们的实现思路应该是这样的

1
2
3
4
5
6
7
def find(list, key):
result = []
for i in list:
if key == i:
result.append(i)

return result

而 saltstack 上实现的方法则是

1
2
3
4
5
6
def find(list1, list2, key):
for i in list1:
if key == i:
list2.remove(key)

return list2

这个设计可能是为了满足贪婪模式, 但这个贪婪模式不清楚是为什么.
从使用者的角度来看, 感觉这是个欠妥的设计.

因为使用时已经明确的指定了对某一个 IP 执行某个动作
但贪婪模式却尽可能的给我匹配更多的 minion

在认知和实现上有较大的差距, 虽然这个可能是使用者不当造成的.
但在设计时应该尽量避免这种有明显不同的地方.