12-04 4 views
公司一直是使用SaltStack做为集中管理工具的,一直是通过命令行的方式去管理。这种方式需要运维人员要对salt的语法比较熟悉。
做成平台化是一个比较好的解决方案,可以做好命令过滤、操作审计、也不需要人员熟悉salt的语法。
1 2 3 4 5 |
[root@master ~]# yum install python-pip gcc make python-devel libffi-devel openssl openss-devel -y # 如果不是使用默认的python,可以不需要安装python-pip [root@master ~]# pip install pyOpenSSL # 如果是自己编译安装的pip请替换成自己安装的pip命令 [root@storage ~]# salt-call --local tls.create_self_signed_cert local: Created Private Key: "/etc/pki/tls/certs/localhost.key." Created Certificate: "/etc/pki/tls/certs/localhost.crt." |
注:如果没有salt-call命令,请安装salt-minion
1 2 3 4 5 6 |
[root@master ~]# cat /etc/salt//master.d/api.conf rest_cherrypy: host: 10.9.54.23 port: 8000 ssl_crt: /etc/pki/tls/certs/localhost.crt ssl_key: /etc/pki/tls/certs/localhost.key |
rest_cherrypy: salt-api是一个基于cherrypy的rest接口
host: 监听主机IP,如果不设定默认监听本机的所有IP
port: 监控端口
salt-api提供了PAM的方式进行认证和权限划分,在master的/etc/salt/master.d目录下创建一个以.conf结尾的文件,命名最好是比较明确的,如auth.conf,如下
1 2 3 4 5 6 7 8 |
[root@master~]# vim /etc/salt/master.d/auth.conf external_auth: pam: # 采用Linux系统用户进行验证 saltapi: - .* #设置用户的权限,允许该用户操作哪些主机,*代表全部 - '@wheel' # 允许访问所有的wheel模块 - '@runner' # 允许访问所有的runner模块 - '@jobs' # 允许访问所有的job runner或wheel模块 |
点此查看runners模块的完整列表
点此查看wheel模块的完整列表
1 2 |
[root@master ~]# useradd -M -s /sbin/nologin saltapi [root@master ~]# echo "password" | /usr/bin/passwd saltapi --stdin |
-M: 不要自动建立用户的登入目录
-s: 指定用户登录的shell,默认是/bin/bash
/sbin/nologin: 是指此用户无法使用bash或其他shell登陆系统,并不是指这个用户无法使用系统资源
1 2 3 4 5 6 7 8 9 10 11 12 |
[root@localhost~]# curl -sSk https://10.9.54.23:8000/login -H 'Accept: application/x-yaml' -d username='saltapi' -d password='saltapi' -d eauth='pam' return: - eauth: pam expire: 1548200627.920629 perms: - .* - '@wheel' - '@runner' - '@jobs' start: 1548157427.920628 token: 1e2574f238b46f704a5623ba33c6c7eb3b36900d user: saltapi |
利用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
In [1]: import requests In [2]: from requests.adapters import HTTPAdapter In [3]: from requests.packages.urllib3.exceptions import InsecureRequestWarning In [4]: requests.packages.urllib3.disable_warnings(InsecureRequestWarning) # 解决证书不被信任时也不弹warning信息 In [5]: session = requests.Session() # 保持cookie In [6]: resp=session.post('https://10.9.54.23:8000/login', json={'username':'saltapi','password':'saltapi','eauth':'pam'},verify=False) # 获取token In [7]: resp.json() Out[7]: {'return': [{'perms': ['.*', '@wheel', '@runner', '@jobs'], 'start': 1548164383.69108, 'token': '3298aaca8c21abbe5a09820887d0fd10441ba28f', # token 'expire': 1548207583.691081, 'user': 'saltapi', 'eauth': 'pam'}]} In [8]: session.post('https://10.9.54.23:8000', json=[{'client':'local','tgt':'k8s-master.*','fun':'test.ping'}],verify=False).json() # 使用cookie请求 Out[8]: {'return': [{'k8s-master.itnotebooks.com': True}]} |
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 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 |
import json import copy import requests from crypt import crypt from requests.adapters import HTTPAdapter from requests.packages.urllib3.exceptions import InsecureRequestWarning requests.packages.urllib3.disable_warnings(InsecureRequestWarning) class SaltAPI(object): def __init__(self, url=None, user=None, password=None): self.__url = url self.__user = user self.__password = password self.__headers = {'Accept': 'application/json', 'Content-type': 'application/json'} self.__data = {'client': 'local', 'tgt': None, 'fun': None, 'arg': None} self.__token = None def get_token(self): """ 用户登陆和获取token :return: """ params = {'eauth': 'pam', 'username': self.__user, 'password': self.__password} content = self.postRequest(params, self.__headers, prefix='login') try: self.__token = content['return'][0]['token'] self.__headers['X-Auth-Token'] = self.__token except Exception as e: print(e) return content def get_grains(self, target=None): """ 获取系统基础信息 :return: """ data = copy.deepcopy(self.__data) if target: data['tgt'] = target else: data['tgt'] = '*' data['fun'] = 'grains.items' content = self.postRequest(data, self.__headers) try: return content['return'][0] except Exception as e: print(e) return content def get_auth_keys(self): """ 获取所有已认证的主机 :return: """ data = copy.deepcopy(self.__data) data['client'] = 'wheel' data['fun'] = 'key.list_all' content = self.postRequest(data, self.__headers) try: return content['return'][0]['data']['return']['minions'] except Exception as e: print(e) return content def get_minion_status(self): """ 获取所有主机的连接状态 :return: """ data = copy.deepcopy(self.__data) data['client'] = 'runner' data['fun'] = 'manage.status' data.pop('tgt') data.pop('arg') content = self.postRequest(data, self.__headers) try: return content['return'][0] except Exception as e: print(e) return content def delete_key(self, minion=None): ''' 删除指定主机的认证信息 ''' if not minion: return {'success': False, 'msg': 'minion-id is none'} data = copy.deepcopy(self.__data) data['client'] = 'wheel' data['fun'] = 'key.delete' data['match'] = minion content = self.postRequest(data, self.__headers) try: return {'success': content['return'][0]['data']['success']} except Exception as e: print(e) return content def minion_alive(self, minion=None): ''' Minion主机存活检测 ''' data = copy.deepcopy(self.__data) if minion: data['tgt'] = minion else: data['tgt'] = '*' data['fun'] = 'test.ping' data.pop('arg') content = self.postRequest(data, self.__headers) try: if content['return'][0]: return content['return'][0] else: return {minion: False} except Exception as e: print(e) return content def passwd(self, target=None, user=None, password=None): """ 修改密码 :param target: 目标客户端 :param user: 目标客户端的系统用户名 :param password: 新的密码,必须大于等于12位 :return: """ if not target: return {'success': False, 'msg': 'target is none.'} if not user: return {'success': False, 'msg': 'user is none.'} if not password: return {'success': False, 'msg': 'password is none.'} if len(password) < 12: return {'success': False, 'msg': 'password must be greater than or equal to 12 bits.'} if password.isalpha() or password.isdigit() or password.islower() or password.isupper(): return { 'success': False, 'msg': 'password must be have lowercase, uppercase and digit.'} password = crypt(password, 'cmdb') content = self.cmd(target=target, arg='usermod -p "{}" {}'.format(password, user)) return {'success': True, 'msg': 'Changing password for user {}.' 'all authentication tokens updated successfully.'.format(user) } def get_users(self, target=None): """ 获取系统用户 :param target: 目标客户端 :return: """ if not target: return {'success': False, 'msg': 'target is none.'} content = self.cmd(target=target, arg="grep /bin/bash /etc/passwd|awk -F ':' '{print $1}'") return content def cmd(self, target=None, fun='cmd.run', arg=None, async=False): """ 远程执行任务 :param target: 目标客户端,为空return False :param fun: 模块 :param arg: 参数,可为空 :param async: 异步执行,默认非异步 :return: """ data = copy.deepcopy(self.__data) if not target: return {'success': False, 'msg': 'target is none'} if not arg: data.pop('arg') if async: data['client'] = 'local_async' data['tgt'] = target data['fun'] = fun data['arg'] = arg content = self.postRequest(data, self.__headers) try: return content['return'][0] except Exception as e: print(e) return content def jobs(self, fun=None, jid=None): """ 任务 :param fun: active,detail :param jid: Job ID :return: """ data = {'client': 'runner'} if fun == 'active': data['fun'] = 'jobs.active' elif fun == 'detail': if not jid: return {'success': False, 'msg': 'job id is none'} data['fun'] = 'jobs.lookup_jid' data['jid'] = jid else: return {'success': False, 'msg': 'fun is active or detail'} content = self.postRequest(data, self.__headers) try: return content['return'][0] except Exception as e: print(e) return content def postRequest(self, data, headers, prefix=None): if prefix: url = '{}/{}'.format(self.__url, prefix) else: url = self.__url try: s = requests.Session() s.mount('https://', HTTPAdapter(max_retries=10)) ret = s.post(url, data=json.dumps(data), headers=headers, verify=False, timeout=(30, 60)) if ret.status_code == 401: print('401 Unauthorized') return {'return': [{'success': False, 'msg': '401 Unauthorized'}]} elif ret.status_code == 200: return ret.json() except Exception as e: print(e) return {'return': [{'success': False, 'msg': e}]} |