saltstack
声明:
该部分是整理于官方文档。
一:安装
由于我的操作系统是红帽系列,所以如下安装是针对红帽系安装,yum安装的好处自动帮我们解决依赖关系。
安装epel:
redhat5:
1 rpm -Uvh http://mirror.pnl.gov/epel/5/i386/epel-release-5-4.noarch.rpm
redhat6:
1 rpm -Uvh http://ftp.linux.ncsu.edu/pub/epel/6/i386/epel-release-6-8.noarch.rpm
在yum源中,minion和master端安装包是不同,是为了方便在不通机器上安装不通的角色。一个master多个minion端。
在master端机器上:
1 yum install salt-master
在minion端运行:
1 yum install salt-minion
master设置开机启动:
1 chkconfig salt-master on
启动master端服务:
1 service salt-master start
同样在client端设置开机启动和启动服务
1 chkconfig salt-minion on 2 service salt-minion start
二: 配置saltstack
一般情况下,master端不需要配置什么,master端的默认配置满足大部分需求,minion端需要做一些本地配置就可以。
默认情况下salt配置文件在:/etc/salt
根据节点的功能的不同,命名规则也不同。
master端:
/etc/salt/master
minion端:
/etc/salt/minion
a:master端配置:
默认情况下,master端监听4505和4506端口。4506端口是minion端返回任务执行结果的端口。默认绑定IP地址:0.0.0.0 你也可以进行设置相应的IP地址,比如:
1 - #interface: 0.0.0.0 2 + interface: 10.0.0.1
更新完配置文件之后,需要重启master。
b:minion端配置:
虽然minion端有很多配置选项,但是配置minion端还是什么简单的,默认情况下,minion端连接的是域名为:‘salt’的master。如果minion端已经给对‘salt’做了正确的解析,即解析到正确的maste IP 就可以。就无需做任何的配置。
如果域名‘salt’不能做正确的解析,可以用如下指令来指定master端。
1 - #master: salt 2 + master: 10.0.0.1
做完如上配置之后,重启minion端。
三:key管理(节点认证)
Salt是使用AES加密master和minion之间的通信。以保证minion和master之间的通信不被篡改。而minion和master之间的互相认证通过是彼此发送的"key"。
在发送命令给minion之前,minion端的key必须被master接收。master端执行命令:salt-key 来列出master已经接收的key和未被接收的key。
1 [root@master ~]# salt-key -L 2 Unaccepted Keys: 3 alpha 4 bravo 5 charlie 6 delta 7 Accepted Keys:
这个例子显示salt master已经知道有4个minions。但是他们key都没有被接受。接受所有的minions的key并被master接收和控制,使用salt-key命令:
1 [root@master ~]# salt-key -A 2 [root@master ~]# salt-key -L 3 Unaccepted Keys: 4 Accepted Keys: 5 alpha 6 bravo 7 charlie 8 delta
命令:salt-key当然也可以接收指定单个minion 的key。可以使用参数:-a keyname 来接收单个minionkey值。
1 [root@master ~]# salt-key -a keyname
四:发送命令(远程执行)
master发送一个简单的test.ping模块的指令给minion端。
1 [root@master ~]# salt ’alpha’ test.ping 2 alpha: 3 True
master和所有minions的之间也可以用相同的测试方法。
1 [root@master ~]# salt ’*’ test.ping 2 alpha: 3 True 4 bravo: 5 True 6 charlie: 7 True 8 delta: 9 True
每一个minion端都必须给master端应答返回一个True.
五:匹配minion id
minion id :是minion端提供一个唯一自定义的值对于master来说。默认情况下,minion id是有该minion端的主机名。但是主机名有时候会发生变化。
每个minion端 都需要一个唯一自定义的id。通常在客户端第一次启动的时候会使用主机名来代替这个id。这个id值可以用过配置设置来进行更改。
注意:
如果minion id是保证minion's的公或者私钥,如果他发生变化,master端必须重新接受一个新的key值如果minion端的hosts发生变化。
如果和minion端的客户端和master端如果minion id发生变化的话,minion端的客户端会自动停止。需要清理本地的缓存id和master端需要删除该节点并且重新接收改minion端的key。
六:主机匹配
默认匹配salt是使用shell方式匹配minion id,这个同样在top file 生效。
注意:你必须要单引号来包裹匹配的salt节点,当salt对该匹配进行调用的时候。
匹配所minions端:
1 salt ’*’ test.ping
匹配所有minions端含有example.net 域名或者包含example
1 salt ’*.example.net’ test.ping 2 salt ’*.example.*’ test.ping
匹配所有webN minions 包含example.net域名(web1.example.net, web2.example.net . . .webN.example.net)
1 salt ’web?.example.net’ test.ping#?表示单个字符数字?
匹配web1到web5minions
1 salt ’web[1-5]’ test.ping
匹配字母区间:web-x ,web-y,web-z minion端:
1 salt ’web-[x-z]’ test.ping
正则表达式匹配:
minions端的匹配可以用使用正则表达来进行匹配:
比如:包含web1-prob 和web1-devel的minions端:
1 salt -E ’web1-(prod|devel)’ test.ping
如果在sls文件中进行匹配的话,你必须要在第一个选项内写上匹配类型:
1 base: 2 ’web1-(prod|devel)’: 3 - match: pcre 4 - webserver
Lists匹配模式:
在大多数情况下,你可以写一个list 标识minions id。
1 salt -L ’web1,web2,web3’ test.ping
saltstack数据系统:grains和pillars。
Grains
salt自身带有一个接口,收集系统底层信息。这个接口叫做grains。Grains 是当minions一启动的时候就会收集的一些静态信息。这个接口同样在salt的模块和组件上生效,以便于在salt的minions上根据正确的操作系统执行正确的命令。
minion启动的时候收集这些静态资源是十分重要的,这些信息必须是静态的。这就意味着这些信息,在grains中不可变的。并且这些收集的数据性质是静态的。也包括一些网络信息。比如:ip
匹配所有操作系统为:Centos minions客户端:
1 salt -G ’os:CentOS’ test.ping
匹配所有的minions是64位CPU并返回他核数。
1 salt -G ’cpuarch:x86_64’ grains.item num_cpus
-G模式是以 key:value形式进行target minions。
可以使用多级字典进行target minon 多级字典用:进行分隔。
1 #salt -G 'interfaces:eth0:19.168.1.1.1'
salt -G ’os:CentOS’ test.ping
总的构想:
通过saltstack的LocalClient的run_job异步执行任务方法,返回任务jid,并存于数据库中,前端通过ajax异步请求数据库,查看后台任务是否执行完成。
一:salts opts dictionary
有些客户端需要访问salts opts dictionary(一个字典包含master和minion的配置文件)
通常字典的信息匹配通过环境变量如果做了设置的话,如果没有做设置,会读取默认配置文件进行读取。
语法结构:
1 salt.config.client_config(path,env_var='SALT_CLIENT_CONFIG';default=None)
1 # cat salt_pots.py 2 import salt.config 3 master_ops=salt.config.client_config('/etc/salt/master') 4 print(master_ops)
内容:
如上是获取saltmaster的配置信息,同样可以获取minion的配置信息。需要运行在minon端。唯一改变只是配置文件。
1 [root@localhost python]# cat python.py 2 import salt.config 3 master_ops=salt.config.client_config('/etc/salt/minion') 4 print(master_ops)
以上获取master或者minion端的配置信息。
二:Salt's Client Interfaces salt客户端api。通过调用salt API 直接让minion端执行命令。
首先我们看下类:LocalClient源码:下面是主要方法,详细的介绍请参考官网:https://docs.saltstack.com/en/latest/ref/clients/index.html
1 class LocalClient(object): 2 ''' 3 The interface used by the :command:`salt` CLI tool on the Salt Master 4 5 ``LocalClient`` is used to send a command to Salt minions to execute 6 :ref:`execution modules <all-salt.modules>` and return the results to the 7 Salt Master. 8 9 Importing and using ``LocalClient`` must be done on the same machine as the 10 Salt Master and it must be done using the same user that the Salt Master is 11 running as. (Unless :conf_master:`external_auth` is configured and 12 authentication credentials are included in the execution). 13 该类主要是saltmaster端向minion端发送命令,接口。 14 .. code-block:: python 15 例子: 16 import salt.client 17 18 local = salt.client.LocalClient() 19 local.cmd('*', 'test.fib', [10]) 20 ''' 21 def __init__(self, 22 c_path=os.path.join(syspaths.CONFIG_DIR, 'master'), 23 mopts=None, skip_perm_errors=False): 24 if mopts: 25 self.opts = mopts 26 else: 27 if os.path.isdir(c_path): 28 log.warning( 29 '{0} expects a file path not a directory path({1}) to ' 30 'it\'s \'c_path\' keyword argument'.format( 31 self.__class__.__name__, c_path 32 ) 33 ) 34 self.opts = salt.config.client_config(c_path) 35 self.serial = salt.payload.Serial(self.opts) 36 self.salt_user = salt.utils.get_specific_user() 37 self.skip_perm_errors = skip_perm_errors 38 self.key = self.__read_master_key() 39 self.event = salt.utils.event.get_event( 40 'master', 41 self.opts['sock_dir'], 42 self.opts['transport'], 43 opts=self.opts, 44 listen=not self.opts.get('__worker', False)) 45 self.functions = salt.loader.minion_mods(self.opts) 46 self.returners = salt.loader.returners(self.opts, self.functions) 47 48 def run_job( 49 self, 50 tgt, 51 fun, 52 arg=(), 53 expr_form='glob', 54 ret='', 55 timeout=None, 56 jid='', 57 kwarg=None, 58 **kwargs): 59 ''' 60 Asynchronously send a command to connected minions 61 异步向客户端发送命令,并返回我们想要的信息比如jid。 62 Prep the job directory and publish a command to any targeted minions. 63 64 :return: A dictionary of (validated) ``pub_data`` or an empty 65 dictionary on failure. The ``pub_data`` contains the job ID and a 66 list of all minions that are expected to return data. 67 68 .. code-block:: python 69 返回数据的格式。 70 >>> local.run_job('*', 'test.sleep', [300]) 71 {'jid': '20131219215650131543', 'minions': ['jerry']} 72 ''' 73 arg = salt.utils.args.condition_input(arg, kwarg) 74 75 try: 76 pub_data = self.pub( 77 tgt, 78 fun, 79 arg, 80 expr_form, 81 ret, 82 jid=jid, 83 timeout=self._get_timeout(timeout), 84 **kwargs) 85 except SaltClientError: 86 # Re-raise error with specific message 87 raise SaltClientError( 88 'The salt master could not be contacted. Is master running?' 89 ) 90 except Exception as general_exception: 91 # Convert to generic client error and pass along mesasge 92 raise SaltClientError(general_exception) 93 94 return self._check_pub_data(pub_data)
salt源码做的真心不错,他在介绍类和方法的同事给咱们提供相应的例子,如上,我们可以调用相应的方法在salt-minion端进行执行。主要接口的执行是在master端(如果你提供了相应的master数据,可以不在master端执行,如上的一内容的master-ops的字典形式内容),因为我们在创建对象的时候,进行如下配置的读取:
1 def __init__(self, 2 c_path=os.path.join(syspaths.CONFIG_DIR, 'master'), 3 mopts=None, skip_perm_errors=False): 4 if mopts: 5 self.opts = mopts 6 else: 7 if os.path.isdir(c_path): 8 log.warning( 9 '{0} expects a file path not a directory path({1}) to ' 10 'it\'s \'c_path\' keyword argument'.format( 11 self.__class__.__name__, c_path 12 ) 13 ) 14 self.opts = salt.config.client_config(c_path) 15 self.serial = salt.payload.Serial(self.opts) 16 self.salt_user = salt.utils.get_specific_user() 17 self.skip_perm_errors = skip_perm_errors 18 self.key = self.__read_master_key() 19 self.event = salt.utils.event.get_event( 20 'master', 21 self.opts['sock_dir'], 22 self.opts['transport'], 23 opts=self.opts, 24 listen=not self.opts.get('__worker', False)) 25 self.functions = salt.loader.minion_mods(self.opts) 26 self.returners = salt.loader.returners(self.opts, self.functions)
我们实现一个简单的模块命令执行:
1 >>> import salt.client 2 >>> cli=salt.clinet.LocalClient() 3 >>> ret=cli.cmd('*','test.ping') 4 >>> print ret 5 {'salt_minion': True}
返回命令的执行结果。注意:cmd执行不异步执行,他会等所有的minion端执行完之后,才把所有的结果返回回来。这个也可以设置超时时间。这个超时间不是执行任务超时时间,官方解释如下:
:param timeout: Seconds to wait after the last minion returns but before all minions return. 等待最后一个minion端返回结果的超时时间在所有数据返回之前。
也可以直接给模块传入参数,
1 >>> cli.cmd('*','cmd.run',['whoami']) 2 {'salt_minion': 'root'}
我们也可以执行多个模块命令,传入多个参数,其中需要注意的是:如果有的模块不需要参数,但是需要传入空的列表,否则报错。
1 >>> cli.cmd('*',['cmd.run','test.ping'],[['whoami'],[]]) 2 {'salt_minion': {'test.ping': True, 'cmd.run': 'root'}}
需要注意的是:参数是一个列表套列表。而模块是一个列表中,以字符形式传入函数种。
该类还提供一个cmd_async,命令以异步的方式进行执行。如下:
1 def cmd_async( 2 self, 3 tgt, 4 fun, 5 arg=(), 6 expr_form='glob', 7 ret='', 8 jid='', 9 kwarg=None, 10 **kwargs): 11 ''' 12 Asynchronously send a command to connected minions 13 14 The function signature is the same as :py:meth:`cmd` with the 15 following exceptions. 16 17 :returns: A job ID or 0 on failure. 18 19 .. code-block:: python 20 21 >>> local.cmd_async('*', 'test.sleep', [300]) 22 '20131219215921857715' 23 ''' 24 arg = salt.utils.args.condition_input(arg, kwarg) 25 pub_data = self.run_job(tgt, 26 fun, 27 arg, 28 expr_form, 29 ret, 30 jid=jid, 31 **kwargs) 32 try: 33 return pub_data['jid'] 34 except KeyError: 35 return 0
但是如果在函数返回的时候,如果pub_data中即返回的数据中不包含key值为'jid'的将不会返回jid值。直接返回0.jid这个值比较重要,一会我们会说为什么要这个值。
1 >>> cli.cmd_async('*',['cmd.run','test.ping'],[['whoami'],[]]) 2 '20161211061247439244'
然后我们根据master的配置文件中,找到:cachedir: /var/cache/salt/master从该配置文件读取我们这个任务的执行情况:
可以查找对应的任务返回情况。/var/cache/salt/master是minion返回任务执行结果的目录,一般缓存24小时:
1 # Set the number of hours to keep old job information in the job cache: 2 #keep_jobs: 24
这个是默认值,我们可以进行修改。
然后我接下来研究LocalClient()的run_job的方法:
1 def run_job( 2 self, 3 tgt, 4 fun, 5 arg=(), 6 expr_form='glob', 7 ret='', 8 timeout=None, 9 jid='', 10 kwarg=None, 11 **kwargs): 12 ''' 13 Asynchronously send a command to connected minions 14 15 Prep the job directory and publish a command to any targeted minions. 16 17 :return: A dictionary of (validated) ``pub_data`` or an empty 18 dictionary on failure. The ``pub_data`` contains the job ID and a 19 list of all minions that are expected to return data. 20 给master端返回一个字典,里面包含一个jid和其他一些信息。 21 .. code-block:: python 22 23 >>> local.run_job('*', 'test.sleep', [300]) 24 {'jid': '20131219215650131543', 'minions': ['jerry']} 25 ''' 26 arg = salt.utils.args.condition_input(arg, kwarg) 27 28 try: 29 pub_data = self.pub( 30 tgt, 31 fun, 32 arg, 33 expr_form, 34 ret, 35 jid=jid, 36 timeout=self._get_timeout(timeout), 37 **kwargs) 38 except SaltClientError: 39 # Re-raise error with specific message 40 raise SaltClientError( 41 'The salt master could not be contacted. Is master running?' 42 ) 43 except Exception as general_exception: 44 # Convert to generic client error and pass along mesasge 45 raise SaltClientError(general_exception) 46 47 return self._check_pub_data(pub_data)
我看下如果我现在一个minion端挂掉,看看返回的结果是什么?
如果执行结果没有返回的时候,返回如下结果:
1 >>> import salt.client 2 >>> cli=salt.client.LocalClient() 3 >>> ret=cli.run_job('*','test.ping') 4 >>> print ret 5 {'jid': '20161211062720119556', 'minions': ['salt_minion']}
同样我可以去master端缓存的目录查看下:
查看相应的目录的,并没有执行结果返回:
我们把minion端启动起来:
1 >>> import salt.client 2 >>> cli=salt.client.LocalClient() 3 >>> 4 >>> ret=cli.run_job('*','test.ping') 5 >>> print ret 6 {'jid': '20161211063245061096', 'minions': ['salt_minion']}
有结果的返回的:
重点:
为什么我们一直强调这个jid,在saltstack中这个jid做为一个任务的唯一标识,所以当我们给大量的minion端推送数据的时候,需要关注以下问题:
1、任务执行需要异步非阻塞执行。
2、如果异步之后,我们怎么去master设置的job缓存中去找我们想要的任务内容呢?
3、所以这个时候jid显得格外重要,所以我们上面篇幅讨论那么多,尤其原因就是在找异步、jid的方法。
既然方法找到了,那下一步是将我们minion端返回的执行结果捕获,在saltstack默认情况下,是将job任务缓存到我们的本地:/var/cache/salt/master中jobs。
但是这个方案我们不想使用,那么接下来就是将执行结果返回给到数据库。
但是返回数据方式有如下2种形式:
(一):Master Job Cache - Master-Side Returner
如上图(一):表示master发送的命令给minion端,然后minion端将结果返回给db、redis、等。
这种方式,只需要在master端进行简单配置即可,minion端不需要进行什么配置。
(二):External Job Cache - Minion-Side Returner
如图(2),是master端,将命令发送给minion端,然后由minion端将数据分别写入redis、mysql等。
这种方式,需要在minion端安装相应的模块(python-mysqldb),还需要配置相应的db数据库信息(账号、密码等),配置较为麻烦,最主要是不安全。
综上所述:我们采用一的模式:Master Job Cache - Master-Side Returner
master配置文件需要添加如下:
1 mysql.host: 'salt' 2 mysql.user: 'salt' 3 mysql.pass: 'salt' 4 mysql.db: 'salt' 5 mysql.port: 3306
需要注意的是:salt需要能被解析在master端
修改master端job缓存:修改配置文件:/etc/salt/master
1 master_job_cache: mysql
如果在传输过程不需要证书认证的话需要往:/etc/salt/master 加入如下配置:
1 mysql.ssl_ca: None 2 mysql.ssl_cert: None 3 mysql.ssl_key: None
修改完如上配置重启salt-master:
1 /etc/init.d/salt-master restart
创建数据库:
1 yum install -y mysql-server.x86_64
创建相应的数据库:
1 CREATE DATABASE `salt` 2 DEFAULT CHARACTER SET utf8 3 DEFAULT COLLATE utf8_general_ci; 4 5 USE `salt`;
创建表结构:
1 -- 2 -- Table structure for table `jids` 3 -- 4 5 DROP TABLE IF EXISTS `jids`; 6 CREATE TABLE `jids` ( 7 `jid` varchar(255) NOT NULL, 8 `load` mediumtext NOT NULL, 9 UNIQUE KEY `jid` (`jid`) 10 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 11 CREATE INDEX jid ON jids(jid) USING BTREE; 12 13 -- 14 -- Table structure for table `salt_returns` 15 -- 16 17 DROP TABLE IF EXISTS `salt_returns`; 18 CREATE TABLE `salt_returns` ( 19 `fun` varchar(50) NOT NULL, 20 `jid` varchar(255) NOT NULL, 21 `return` mediumtext NOT NULL, 22 `id` varchar(255) NOT NULL, 23 `success` varchar(10) NOT NULL, 24 `full_ret` mediumtext NOT NULL, 25 `alter_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 26 KEY `id` (`id`), 27 KEY `jid` (`jid`), 28 KEY `fun` (`fun`) 29 ) ENGINE=InnoDB DEFAULT CHARSET=utf8; 30 31 -- 32 -- Table structure for table `salt_events` 33 -- 34 35 DROP TABLE IF EXISTS `salt_events`; 36 CREATE TABLE `salt_events` ( 37 `id` BIGINT NOT NULL AUTO_INCREMENT, 38 `tag` varchar(255) NOT NULL, 39 `data` mediumtext NOT NULL, 40 `alter_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, 41 `master_id` varchar(255) NOT NULL, 42 PRIMARY KEY (`id`), 43 KEY `tag` (`tag`) 44 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
给salt用户授权访问:
1 grant all on salt.* to salt@'%' identified by 'salt';
注意:%不包括localhost。
由于master和mysql数据在同一台服务器:所以需要授权给localhost。
1 grant all on salt.* to salt@'localhost' identified by 'salt';
另外master端需要安装连接mysql的API。注意是:MySQL-python.x86_64 。注意名字!!在python2版本中。
1 yum install -y MySQL-python.x86_64
然后我们做个测试:
1 >>> import salt.client 2 >>> cli=salt.client.LocalClient() 3 >>> ret=cli.run_job('*','test.ping') 4 >>> ret=cli.run_job('*','cmd.run',['w']) 5 >>> print ret 6 {'jid': '20161211175654153286', 'minions': ['salt_minion']}
然后我查看数据库:
1 mysql> select full_ret from salt_returns where jid='20161211175654153286'; 2 +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 3 | full_ret | 4 +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 5 | {"fun_args": ["w"], "jid": "20161211175654153286", "return": " 17:56:54 up 14:24, 2 users, load average: 0.20, 0.13, 0.11\nUSER TTY FROM LOGIN@ IDLE JCPU PCPU WHAT\nroot tty1 - Thu01 2:06m 0.03s 0.03s -bash\nroot pts/0 192.168.217.1 15:50 2:05m 0.01s 0.01s -bash", "retcode": 0, "success": true, "cmd": "_return", "_stamp": "2016-12-12T01:56:54.256049", "fun": "cmd.run", "id": "salt_minion"} | 6 +-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ 7 1 row in set (0.00 sec)
cheers!!有结果了!搞定!!