├── NewConnect.py ├── NewInit.sh ├── NewLogin.py ├── README.md └── image ├── 1.jpg ├── 2.jpg └── 3.jpg /NewConnect.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding: utf-8 3 | #author: xwjr.com 4 | 5 | import sys 6 | 7 | reload(sys) 8 | sys.setdefaultencoding('utf8') 9 | 10 | import os 11 | import re 12 | import time 13 | import datetime 14 | import textwrap 15 | import getpass 16 | import readline 17 | import django 18 | import paramiko 19 | import errno 20 | import pyte 21 | import operator 22 | import struct, fcntl, signal, socket, select 23 | from io import open as copen 24 | import uuid 25 | 26 | os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' 27 | if not django.get_version().startswith('1.6'): 28 | setup = django.setup() 29 | from django.contrib.sessions.models import Session 30 | from jumpserver.api import ServerError, User, Asset, PermRole, AssetGroup, get_object, mkdir, get_asset_info 31 | from jumpserver.api import GetSSHKeyTime, GetPasswordTime, logger, Log, TtyLog, get_role_key, CRYPTOR, bash, get_tmp_dir 32 | from jperm.perm_api import gen_resource, get_group_asset_perm, get_group_user_perm, user_have_perm, PermRole 33 | from jumpserver.settings import LOG_DIR, NAV_SORT_BY 34 | from jperm.ansible_api import MyRunner 35 | # from jlog.log_api import escapeString 36 | from jlog.models import ExecLog, FileLog 37 | from jlog.views import TermLogRecorder 38 | 39 | from NewLogin import gettoken,senddata 40 | import random 41 | 42 | def Yanzhengma(): 43 | VerificationCode = str(random.randint(100000, 999999)) 44 | TYPE = "touser" 45 | touser = str(sys.argv[1]) 46 | 47 | TIME = str(sys.argv[2]) 48 | content = VerificationCode 49 | subject = "验证码:" 50 | 51 | corpid = '*****************' 52 | corpsecret = '**********************************' 53 | accesstoken = gettoken(corpid,corpsecret) 54 | #发送验证码 55 | Status = senddata(accesstoken,TYPE,touser,subject,content) 56 | logger.debug(Status) 57 | logger.debug("New user login:" + "\t" + USER) 58 | Times = 4 59 | Status = "no" 60 | while (Times >= 2) and (Status == "no"): 61 | try: 62 | VerificationCodeInput = "" 63 | print '\033[1;33;40m' 64 | VerificationCodeInput = str(raw_input("请输入验证码:\033[1;37;40m")) 65 | print '\033[0m' 66 | except Exception,e: 67 | print Exception,":",e 68 | if (VerificationCodeInput == VerificationCode): 69 | #print "欢迎登陆希望金融小分队跳板机!" 70 | Status = "ok" 71 | else: 72 | print '\033[1;31;40m' 73 | print "请输入正确的验证码!!!" 74 | print '\033[0m' 75 | Times -= 1 76 | if (Status == "no"): 77 | sys.exit() 78 | 79 | 80 | Yanzhengma() 81 | 82 | 83 | login_user = get_object(User, username=getpass.getuser()) 84 | try: 85 | remote_ip = os.environ.get('SSH_CLIENT').split()[0] 86 | except (IndexError, AttributeError): 87 | remote_ip = os.popen("who -m | awk '{ print $NF }'").read().strip('()\n') 88 | 89 | try: 90 | import termios 91 | import tty 92 | except ImportError: 93 | print '\033[1;31m仅支持类Unix系统 Only unix like supported.\033[0m' 94 | time.sleep(3) 95 | sys.exit() 96 | 97 | 98 | def color_print(msg, color='red', exits=False): 99 | """ 100 | Print colorful string. 101 | 颜色打印字符或者退出 102 | """ 103 | color_msg = {'blue': '\033[1;36m%s\033[0m', 104 | 'green': '\033[1;32m%s\033[0m', 105 | 'yellow': '\033[1;33m%s\033[0m', 106 | 'red': '\033[1;31m%s\033[0m', 107 | 'title': '\033[30;42m%s\033[0m', 108 | 'info': '\033[32m%s\033[0m'} 109 | msg = color_msg.get(color, 'red') % msg 110 | print msg 111 | if exits: 112 | time.sleep(2) 113 | sys.exit() 114 | return msg 115 | 116 | 117 | def write_log(f, msg): 118 | msg = re.sub(r'[\r\n]', '\r\n', msg) 119 | f.write(msg) 120 | f.flush() 121 | 122 | 123 | class Tty(object): 124 | """ 125 | A virtual tty class 126 | 一个虚拟终端类,实现连接ssh和记录日志,基类 127 | """ 128 | def __init__(self, user, asset, role, login_type='ssh'): 129 | self.username = user.username 130 | self.asset_name = asset.hostname 131 | self.ip = None 132 | self.port = 22 133 | self.ssh = None 134 | self.channel = None 135 | self.asset = asset 136 | self.user = user 137 | self.role = role 138 | self.remote_ip = '' 139 | self.login_type = login_type 140 | self.vim_flag = False 141 | self.vim_end_pattern = re.compile(r'\x1b\[\?1049', re.X) 142 | self.vim_data = '' 143 | self.stream = None 144 | self.screen = None 145 | self.__init_screen_stream() 146 | 147 | def __init_screen_stream(self): 148 | """ 149 | 初始化虚拟屏幕和字符流 150 | """ 151 | self.stream = pyte.ByteStream() 152 | self.screen = pyte.Screen(80, 24) 153 | self.stream.attach(self.screen) 154 | 155 | @staticmethod 156 | def is_output(strings): 157 | newline_char = ['\n', '\r', '\r\n'] 158 | for char in newline_char: 159 | if char in strings: 160 | return True 161 | return False 162 | 163 | @staticmethod 164 | def command_parser(command): 165 | """ 166 | 处理命令中如果有ps1或者mysql的特殊情况,极端情况下会有ps1和mysql 167 | :param command:要处理的字符传 168 | :return:返回去除PS1或者mysql字符串的结果 169 | """ 170 | result = None 171 | match = re.compile('\[?.*@.*\]?[\$#]\s').split(command) 172 | if match: 173 | # 只需要最后的一个PS1后面的字符串 174 | result = match[-1].strip() 175 | else: 176 | # PS1没找到,查找mysql 177 | match = re.split('mysql>\s', command) 178 | if match: 179 | # 只需要最后一个mysql后面的字符串 180 | result = match[-1].strip() 181 | return result 182 | 183 | def deal_command(self, data): 184 | """ 185 | 处理截获的命令 186 | :param data: 要处理的命令 187 | :return:返回最后的处理结果 188 | """ 189 | command = '' 190 | try: 191 | self.stream.feed(data) 192 | # 从虚拟屏幕中获取处理后的数据 193 | for line in reversed(self.screen.buffer): 194 | line_data = "".join(map(operator.attrgetter("data"), line)).strip() 195 | if len(line_data) > 0: 196 | parser_result = self.command_parser(line_data) 197 | if parser_result is not None: 198 | # 2个条件写一起会有错误的数据 199 | if len(parser_result) > 0: 200 | command = parser_result 201 | else: 202 | command = line_data 203 | break 204 | except Exception: 205 | pass 206 | # 虚拟屏幕清空 207 | self.screen.reset() 208 | return command 209 | 210 | def get_log(self): 211 | """ 212 | Logging user command and output. 213 | 记录用户的日志 214 | """ 215 | banned_list = ['/', '\0', '*', '?'] 216 | tty_log_dir = os.path.join(LOG_DIR, 'tty') 217 | date_today = datetime.datetime.now() 218 | date_start = date_today.strftime('%Y%m%d') 219 | time_start = date_today.strftime('%H%M%S') 220 | today_connect_log_dir = os.path.join(tty_log_dir, date_start) 221 | filename = '%s_%s_%s' % (self.username, self.asset_name, time_start) 222 | for banned_char in banned_list: 223 | filename = filename.replace(banned_char, '_') 224 | log_file_path = os.path.join(today_connect_log_dir, filename) 225 | try: 226 | mkdir(os.path.dirname(today_connect_log_dir), mode=777) 227 | mkdir(today_connect_log_dir, mode=777) 228 | except OSError as e: 229 | logger.debug('创建目录 %s 失败,请修改%s目录权限 With error msg: %s' % (today_connect_log_dir, tty_log_dir, e)) 230 | raise ServerError('创建目录 %s 失败,请修改%s目录权限' % (today_connect_log_dir, tty_log_dir)) 231 | try: 232 | log_file_f = open(log_file_path + '.log', 'a') 233 | log_time_f = open(log_file_path + '.time', 'a') 234 | except IOError as e: 235 | logger.debug('创建tty日志文件失败, 请修改目录%s权限 With error msg: %s' % (today_connect_log_dir, e)) 236 | raise ServerError('创建tty日志文件失败, 请修改目录%s权限' % today_connect_log_dir) 237 | 238 | if self.login_type == 'ssh': # 如果是ssh连接过来,记录connect.py的pid,web terminal记录为日志的id 239 | pid = os.getpid() 240 | self.remote_ip = remote_ip # 获取远端IP 241 | else: 242 | pid = 0 243 | 244 | log = Log(user=self.username, host=self.asset_name, remote_ip=self.remote_ip, login_type=self.login_type, 245 | log_path=log_file_path, start_time=date_today, pid=pid) 246 | log.save() 247 | if self.login_type == 'web': 248 | log.pid = log.id # 设置log id为websocket的id, 然后kill时干掉websocket 249 | log.save() 250 | 251 | log_file_f.write('Start at %s\r\n' % datetime.datetime.now()) 252 | return log_file_f, log_time_f, log 253 | 254 | def get_connect_info(self): 255 | """ 256 | 获取需要登陆的主机的信息和映射用户的账号密码 257 | """ 258 | asset_info = get_asset_info(self.asset) 259 | role_key = get_role_key(self.user, self.role) # 获取角色的key,因为ansible需要权限是600,所以统一生成用户_角色key 260 | role_pass = CRYPTOR.decrypt(self.role.password) 261 | connect_info = {'user': self.user, 'asset': self.asset, 'ip': asset_info.get('ip'), 262 | 'port': int(asset_info.get('port')), 'role_name': self.role.name, 263 | 'role_pass': role_pass, 'role_key': role_key} 264 | logger.debug(connect_info) 265 | return connect_info 266 | 267 | def get_connection(self): 268 | """ 269 | 获取连接成功后的ssh 270 | """ 271 | connect_info = self.get_connect_info() 272 | 273 | # 发起ssh连接请求 Make a ssh connection 274 | ssh = paramiko.SSHClient() 275 | # ssh.load_system_host_keys() 276 | ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 277 | try: 278 | role_key = connect_info.get('role_key') 279 | if role_key and os.path.isfile(role_key): 280 | try: 281 | ssh.connect(connect_info.get('ip'), 282 | port=connect_info.get('port'), 283 | username=connect_info.get('role_name'), 284 | password=connect_info.get('role_pass'), 285 | key_filename=role_key, 286 | look_for_keys=False) 287 | return ssh 288 | except (paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException): 289 | logger.warning(u'使用ssh key %s 失败, 尝试只使用密码' % role_key) 290 | pass 291 | 292 | ssh.connect(connect_info.get('ip'), 293 | port=connect_info.get('port'), 294 | username=connect_info.get('role_name'), 295 | password=connect_info.get('role_pass'), 296 | allow_agent=False, 297 | look_for_keys=False) 298 | 299 | except (paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException): 300 | raise ServerError('认证失败 Authentication Error.') 301 | except socket.error: 302 | raise ServerError('端口可能不对 Connect SSH Socket Port Error, Please Correct it.') 303 | else: 304 | self.ssh = ssh 305 | return ssh 306 | 307 | 308 | class SshTty(Tty): 309 | """ 310 | A virtual tty class 311 | 一个虚拟终端类,实现连接ssh和记录日志 312 | """ 313 | 314 | @staticmethod 315 | def get_win_size(): 316 | """ 317 | This function use to get the size of the windows! 318 | 获得terminal窗口大小 319 | """ 320 | if 'TIOCGWINSZ' in dir(termios): 321 | TIOCGWINSZ = termios.TIOCGWINSZ 322 | else: 323 | TIOCGWINSZ = 1074295912L 324 | s = struct.pack('HHHH', 0, 0, 0, 0) 325 | x = fcntl.ioctl(sys.stdout.fileno(), TIOCGWINSZ, s) 326 | return struct.unpack('HHHH', x)[0:2] 327 | 328 | def set_win_size(self, sig, data): 329 | """ 330 | This function use to set the window size of the terminal! 331 | 设置terminal窗口大小 332 | """ 333 | try: 334 | win_size = self.get_win_size() 335 | self.channel.resize_pty(height=win_size[0], width=win_size[1]) 336 | except Exception: 337 | pass 338 | 339 | def posix_shell(self): 340 | """ 341 | Use paramiko channel connect server interactive. 342 | 使用paramiko模块的channel,连接后端,进入交互式 343 | """ 344 | log_file_f, log_time_f, log = self.get_log() 345 | termlog = TermLogRecorder(User.objects.get(id=self.user.id)) 346 | termlog.setid(log.id) 347 | old_tty = termios.tcgetattr(sys.stdin) 348 | pre_timestamp = time.time() 349 | data = '' 350 | input_mode = False 351 | try: 352 | tty.setraw(sys.stdin.fileno()) 353 | tty.setcbreak(sys.stdin.fileno()) 354 | self.channel.settimeout(0.0) 355 | 356 | while True: 357 | try: 358 | r, w, e = select.select([self.channel, sys.stdin], [], []) 359 | flag = fcntl.fcntl(sys.stdin, fcntl.F_GETFL, 0) 360 | fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flag|os.O_NONBLOCK) 361 | except Exception: 362 | pass 363 | 364 | if self.channel in r: 365 | try: 366 | x = self.channel.recv(10240) 367 | if len(x) == 0: 368 | break 369 | 370 | index = 0 371 | len_x = len(x) 372 | while index < len_x: 373 | try: 374 | n = os.write(sys.stdout.fileno(), x[index:]) 375 | sys.stdout.flush() 376 | index += n 377 | except OSError as msg: 378 | if msg.errno == errno.EAGAIN: 379 | continue 380 | now_timestamp = time.time() 381 | termlog.write(x) 382 | termlog.recoder = False 383 | log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(x))) 384 | log_time_f.flush() 385 | log_file_f.write(x) 386 | log_file_f.flush() 387 | pre_timestamp = now_timestamp 388 | log_file_f.flush() 389 | 390 | self.vim_data += x 391 | if input_mode: 392 | data += x 393 | 394 | except socket.timeout: 395 | pass 396 | 397 | if sys.stdin in r: 398 | try: 399 | x = os.read(sys.stdin.fileno(), 4096) 400 | except OSError: 401 | pass 402 | termlog.recoder = True 403 | input_mode = True 404 | if self.is_output(str(x)): 405 | # 如果len(str(x)) > 1 说明是复制输入的 406 | if len(str(x)) > 1: 407 | data = x 408 | match = self.vim_end_pattern.findall(self.vim_data) 409 | if match: 410 | if self.vim_flag or len(match) == 2: 411 | self.vim_flag = False 412 | else: 413 | self.vim_flag = True 414 | elif not self.vim_flag: 415 | self.vim_flag = False 416 | data = self.deal_command(data)[0:200] 417 | if data is not None: 418 | TtyLog(log=log, datetime=datetime.datetime.now(), cmd=data).save() 419 | data = '' 420 | self.vim_data = '' 421 | input_mode = False 422 | 423 | if len(x) == 0: 424 | break 425 | self.channel.send(x) 426 | 427 | finally: 428 | termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) 429 | log_file_f.write('End time is %s' % datetime.datetime.now()) 430 | log_file_f.close() 431 | log_time_f.close() 432 | termlog.save() 433 | log.filename = termlog.filename 434 | log.is_finished = True 435 | log.end_time = datetime.datetime.now() 436 | log.save() 437 | 438 | def connect(self): 439 | """ 440 | Connect server. 441 | 连接服务器 442 | """ 443 | # 发起ssh连接请求 Make a ssh connection 444 | ssh = self.get_connection() 445 | 446 | transport = ssh.get_transport() 447 | transport.set_keepalive(30) 448 | transport.use_compression(True) 449 | 450 | # 获取连接的隧道并设置窗口大小 Make a channel and set windows size 451 | global channel 452 | win_size = self.get_win_size() 453 | # self.channel = channel = ssh.invoke_shell(height=win_size[0], width=win_size[1], term='xterm') 454 | self.channel = channel = transport.open_session() 455 | channel.get_pty(term='xterm', height=win_size[0], width=win_size[1]) 456 | channel.invoke_shell() 457 | try: 458 | signal.signal(signal.SIGWINCH, self.set_win_size) 459 | except: 460 | pass 461 | 462 | self.posix_shell() 463 | 464 | # Shutdown channel socket 465 | channel.close() 466 | ssh.close() 467 | 468 | 469 | class Nav(object): 470 | """ 471 | 导航提示类 472 | """ 473 | def __init__(self, user): 474 | self.user = user 475 | self.user_perm = get_group_user_perm(self.user) 476 | if NAV_SORT_BY == 'ip': 477 | self.perm_assets = sorted(self.user_perm.get('asset', []).keys(), 478 | key=lambda x: [int(num) for num in x.ip.split('.') if num.isdigit()]) 479 | elif NAV_SORT_BY == 'hostname': 480 | self.perm_assets = self.natural_sort_hostname(self.user_perm.get('asset', []).keys()) 481 | else: 482 | self.perm_assets = tuple(self.user_perm.get('asset', [])) 483 | self.search_result = self.perm_assets 484 | self.perm_asset_groups = self.user_perm.get('asset_group', []) 485 | 486 | def natural_sort_hostname(self, list): 487 | convert = lambda text: int(text) if text.isdigit() else text.lower() 488 | alphanum_key = lambda x: [ convert(c) for c in re.split('([0-9]+)', x.hostname) ] 489 | return sorted(list, key = alphanum_key) 490 | 491 | @staticmethod 492 | def print_nav(): 493 | """ 494 | Print prompt 495 | 打印提示导航 496 | """ 497 | msg = """\n\033[1;32m### 欢迎使用希望金融小分队跳板机系统 ### \033[0m 498 | 499 | 1) 输入 \033[32mID\033[0m 直接登录 或 输入\033[32m部分 IP,主机名,备注\033[0m 进行搜索登录(如果唯一). 500 | 2) 输入 \033[32m/\033[0m + \033[32mIP, 主机名 or 备注 \033[0m搜索. 如: /ip 501 | 3) 输入 \033[32mP/p\033[0m 显示您有权限的主机. 502 | 4) 输入 \033[32mG/g\033[0m 显示您有权限的主机组. 503 | 5) 输入 \033[32mG/g\033[0m\033[0m + \033[32m组ID\033[0m 显示该组下主机. 如: g1 504 | 6) 输入 \033[32mE/e\033[0m 批量执行命令. 505 | 7) 输入 \033[32mU/u\033[0m 批量上传文件. 506 | 8) 输入 \033[32mD/d\033[0m 批量下载文件. 507 | 9) 输入 \033[32mH/h\033[0m 帮助. 508 | 0) 输入 \033[32mQ/q\033[0m 退出. 509 | """ 510 | print textwrap.dedent(msg) 511 | 512 | def get_asset_group_member(self, str_r): 513 | gid_pattern = re.compile(r'^g\d+$') 514 | 515 | if gid_pattern.match(str_r): 516 | gid = int(str_r.lstrip('g')) 517 | # 获取资产组包含的资产 518 | asset_group = get_object(AssetGroup, id=gid) 519 | if asset_group and asset_group in self.perm_asset_groups: 520 | self.search_result = list(asset_group.asset_set.all()) 521 | else: 522 | color_print('没有该资产组或没有权限') 523 | return 524 | 525 | def search(self, str_r=''): 526 | # 搜索结果保存 527 | if str_r: 528 | try: 529 | id_ = int(str_r) 530 | if id_ < len(self.search_result): 531 | self.search_result = [self.search_result[id_]] 532 | return 533 | else: 534 | raise ValueError 535 | 536 | except (ValueError, TypeError): 537 | # 匹配 ip, hostname, 备注 538 | str_r = str_r.lower() 539 | self.search_result = [asset for asset in self.perm_assets if str_r == str(asset.ip).lower()] or \ 540 | [asset for asset in self.perm_assets if str_r in str(asset.ip).lower() \ 541 | or str_r in str(asset.hostname).lower() \ 542 | or str_r in str(asset.comment).lower()] 543 | else: 544 | # 如果没有输入就展现所有 545 | self.search_result = self.perm_assets 546 | 547 | @staticmethod 548 | def truncate_str(str_, length=30): 549 | str_ = str_.decode('utf-8') 550 | if len(str_) > length: 551 | return str_[:14] + '..' + str_[-14:] 552 | else: 553 | return str_ 554 | 555 | @staticmethod 556 | def get_max_asset_property_length(assets, property_='hostname'): 557 | try: 558 | return max([len(getattr(asset, property_)) for asset in assets]) 559 | except ValueError: 560 | return 30 561 | 562 | def print_search_result(self): 563 | hostname_max_length = self.get_max_asset_property_length(self.search_result) 564 | line = '[%-3s] %-16s %-5s %-' + str(hostname_max_length) + 's %-10s %s' 565 | color_print(line % ('ID', 'IP', 'Port', 'Hostname', 'SysUser', 'Comment'), 'title') 566 | if hasattr(self.search_result, '__iter__'): 567 | for index, asset in enumerate(self.search_result): 568 | # 获取该资产信息 569 | asset_info = get_asset_info(asset) 570 | # 获取该资产包含的角色 571 | role = [str(role.name) for role in self.user_perm.get('asset').get(asset).get('role')] 572 | print line % (index, asset.ip, asset_info.get('port'), 573 | self.truncate_str(asset.hostname), str(role).replace("'", ''), asset.comment) 574 | print 575 | 576 | def try_connect(self): 577 | try: 578 | asset = self.search_result[0] 579 | roles = list(self.user_perm.get('asset').get(asset).get('role')) 580 | if len(roles) == 1: 581 | role = roles[0] 582 | elif len(roles) > 1: 583 | print "\033[32m[ID] 系统用户\033[0m" 584 | for index, role in enumerate(roles): 585 | print "[%-2s] %s" % (index, role.name) 586 | print 587 | print "授权系统用户超过1个,请输入ID, q退出" 588 | try: 589 | role_index = raw_input("\033[1;32mID>:\033[0m ").strip() 590 | if role_index == 'q': 591 | return 592 | else: 593 | role = roles[int(role_index)] 594 | except IndexError: 595 | color_print('请输入正确ID', 'red') 596 | return 597 | else: 598 | color_print('没有映射用户', 'red') 599 | return 600 | 601 | print('Connecting %s ...' % asset.hostname) 602 | ssh_tty = SshTty(login_user, asset, role) 603 | ssh_tty.connect() 604 | except (KeyError, ValueError): 605 | color_print('请输入正确ID', 'red') 606 | except ServerError, e: 607 | color_print(e, 'red') 608 | 609 | def print_asset_group(self): 610 | """ 611 | 打印用户授权的资产组 612 | """ 613 | user_asset_group_all = get_group_user_perm(self.user).get('asset_group', []) 614 | color_print('[%-3s] %-20s %s' % ('ID', '组名', '备注'), 'title') 615 | for asset_group in user_asset_group_all: 616 | print '[%-3s] %-15s %s' % (asset_group.id, asset_group.name, asset_group.comment) 617 | print 618 | 619 | def exec_cmd(self): 620 | """ 621 | 批量执行命令 622 | """ 623 | while True: 624 | roles = self.user_perm.get('role').keys() 625 | if len(roles) > 1: # 授权角色数大于1 626 | color_print('[%-2s] %-15s' % ('ID', '系统用户'), 'info') 627 | role_check = dict(zip(range(len(roles)), roles)) 628 | 629 | for i, r in role_check.items(): 630 | print '[%-2s] %-15s' % (i, r.name) 631 | print 632 | print "请输入运行命令所关联系统用户的ID, q退出" 633 | 634 | try: 635 | role_id = int(raw_input("\033[1;32mRole>:\033[0m ").strip()) 636 | if role_id == 'q': 637 | break 638 | except (IndexError, ValueError): 639 | color_print('错误输入') 640 | else: 641 | role = role_check[int(role_id)] 642 | elif len(roles) == 1: # 授权角色数为1 643 | role = roles[0] 644 | else: 645 | color_print('当前用户未被授予角色,无法执行任何操作,如有疑问请联系管理员。') 646 | return 647 | assets = list(self.user_perm.get('role', {}).get(role).get('asset')) # 获取该用户,角色授权主机 648 | print "授权包含该系统用户的所有主机" 649 | for asset in assets: 650 | print ' %s' % asset.hostname 651 | print 652 | print "请输入主机名或ansible支持的pattern, 多个主机:分隔, q退出" 653 | pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip() 654 | if pattern == 'q': 655 | break 656 | else: 657 | res = gen_resource({'user': self.user, 'asset': assets, 'role': role}, perm=self.user_perm) 658 | runner = MyRunner(res) 659 | asset_name_str = '' 660 | print "匹配主机:" 661 | for inv in runner.inventory.get_hosts(pattern=pattern): 662 | print ' %s' % inv.name 663 | asset_name_str += '%s ' % inv.name 664 | print 665 | 666 | while True: 667 | print "请输入执行的命令, 按q退出" 668 | command = raw_input("\033[1;32mCmds>:\033[0m ").strip() 669 | if command == 'q': 670 | break 671 | elif not command: 672 | color_print('命令不能为空...') 673 | continue 674 | runner.run('shell', command, pattern=pattern) 675 | ExecLog(host=asset_name_str, user=self.user.username, cmd=command, remote_ip=remote_ip, 676 | result=runner.results).save() 677 | for k, v in runner.results.items(): 678 | if k == 'ok': 679 | for host, output in v.items(): 680 | color_print("%s => %s" % (host, 'Ok'), 'green') 681 | print output 682 | print 683 | else: 684 | for host, output in v.items(): 685 | color_print("%s => %s" % (host, k), 'red') 686 | color_print(output, 'red') 687 | print 688 | print "~o~ Task finished ~o~" 689 | print 690 | 691 | def upload(self): 692 | while True: 693 | try: 694 | print "进入批量上传模式" 695 | print "请输入主机名或ansible支持的pattern, 多个主机:分隔 q退出" 696 | pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip() 697 | if pattern == 'q': 698 | break 699 | else: 700 | assets = self.user_perm.get('asset').keys() 701 | res = gen_resource({'user': self.user, 'asset': assets}, perm=self.user_perm) 702 | runner = MyRunner(res) 703 | asset_name_str = '' 704 | print "匹配主机:" 705 | for inv in runner.inventory.get_hosts(pattern=pattern): 706 | print inv.name 707 | asset_name_str += '%s ' % inv.name 708 | 709 | if not asset_name_str: 710 | color_print('没有匹配主机') 711 | continue 712 | tmp_dir = get_tmp_dir() 713 | logger.debug('Upload tmp dir: %s' % tmp_dir) 714 | os.chdir(tmp_dir) 715 | bash('rz') 716 | filename_str = ' '.join(os.listdir(tmp_dir)) 717 | if not filename_str: 718 | color_print("上传文件为空") 719 | continue 720 | logger.debug('上传文件: %s' % filename_str) 721 | 722 | runner = MyRunner(res) 723 | runner.run('copy', module_args='src=%s dest=%s directory_mode' 724 | % (tmp_dir, '/tmp'), pattern=pattern) 725 | ret = runner.results 726 | FileLog(user=self.user.name, host=asset_name_str, filename=filename_str, 727 | remote_ip=remote_ip, type='upload', result=ret).save() 728 | logger.debug('Upload file: %s' % ret) 729 | if ret.get('failed'): 730 | error = '上传目录: %s \n上传失败: [ %s ] \n上传成功 [ %s ]' % (tmp_dir, 731 | ', '.join(ret.get('failed').keys()), 732 | ', '.join(ret.get('ok').keys())) 733 | color_print(error) 734 | else: 735 | msg = '上传目录: %s \n传送成功 [ %s ]' % (tmp_dir, ', '.join(ret.get('ok').keys())) 736 | color_print(msg, 'green') 737 | print 738 | 739 | except IndexError: 740 | pass 741 | 742 | def download(self): 743 | while True: 744 | try: 745 | print "进入批量下载模式" 746 | print "请输入主机名或ansible支持的pattern, 多个主机:分隔,q退出" 747 | pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip() 748 | if pattern == 'q': 749 | break 750 | else: 751 | assets = self.user_perm.get('asset').keys() 752 | res = gen_resource({'user': self.user, 'asset': assets}, perm=self.user_perm) 753 | runner = MyRunner(res) 754 | asset_name_str = '' 755 | print "匹配主机:\n" 756 | for inv in runner.inventory.get_hosts(pattern=pattern): 757 | asset_name_str += '%s ' % inv.name 758 | print ' %s' % inv.name 759 | if not asset_name_str: 760 | color_print('没有匹配主机') 761 | continue 762 | print 763 | while True: 764 | tmp_dir = get_tmp_dir() 765 | logger.debug('Download tmp dir: %s' % tmp_dir) 766 | print "请输入文件路径(不支持目录)" 767 | file_path = raw_input("\033[1;32mPath>:\033[0m ").strip() 768 | if file_path == 'q': 769 | break 770 | 771 | if not file_path: 772 | color_print("文件路径为空") 773 | continue 774 | 775 | runner.run('fetch', module_args='src=%s dest=%s' % (file_path, tmp_dir), pattern=pattern) 776 | ret = runner.results 777 | FileLog(user=self.user.name, host=asset_name_str, filename=file_path, type='download', 778 | remote_ip=remote_ip, result=ret).save() 779 | logger.debug('Download file result: %s' % ret) 780 | os.chdir('/tmp') 781 | tmp_dir_name = os.path.basename(tmp_dir) 782 | if not os.listdir(tmp_dir): 783 | color_print('下载全部失败') 784 | continue 785 | bash('tar czf %s.tar.gz %s && sz %s.tar.gz' % (tmp_dir, tmp_dir_name, tmp_dir)) 786 | 787 | if ret.get('failed'): 788 | error = '文件名称: %s \n下载失败: [ %s ] \n下载成功 [ %s ]' % \ 789 | ('%s.tar.gz' % tmp_dir_name, ', '.join(ret.get('failed').keys()), ', '.join(ret.get('ok').keys())) 790 | color_print(error) 791 | else: 792 | msg = '文件名称: %s \n下载成功 [ %s ]' % ('%s.tar.gz' % tmp_dir_name, ', '.join(ret.get('ok').keys())) 793 | color_print(msg, 'green') 794 | print 795 | except IndexError: 796 | pass 797 | 798 | 799 | def main(): 800 | """ 801 | he he 802 | 主程序 803 | """ 804 | if not login_user: # 判断用户是否存在 805 | color_print('没有该用户,或许你是以root运行的 No that user.', exits=True) 806 | 807 | if not login_user.is_active: 808 | color_print('您的用户已禁用,请联系管理员.', exits=True) 809 | 810 | gid_pattern = re.compile(r'^g\d+$') 811 | nav = Nav(login_user) 812 | nav.print_nav() 813 | 814 | try: 815 | while True: 816 | try: 817 | option = raw_input("\033[1;32mOpt or ID>:\033[0m ").strip() 818 | except EOFError: 819 | nav.print_nav() 820 | continue 821 | except KeyboardInterrupt: 822 | sys.exit(0) 823 | if option in ['P', 'p', '\n', '']: 824 | nav.search() 825 | nav.print_search_result() 826 | continue 827 | if option.startswith('/'): 828 | nav.search(option.lstrip('/')) 829 | nav.print_search_result() 830 | elif gid_pattern.match(option): 831 | nav.get_asset_group_member(str_r=option) 832 | nav.print_search_result() 833 | elif option in ['G', 'g']: 834 | nav.print_asset_group() 835 | continue 836 | elif option in ['E', 'e']: 837 | nav.exec_cmd() 838 | continue 839 | elif option in ['U', 'u']: 840 | nav.upload() 841 | elif option in ['D', 'd']: 842 | nav.download() 843 | elif option in ['H', 'h']: 844 | nav.print_nav() 845 | elif option in ['Q', 'q', 'exit']: 846 | sys.exit() 847 | else: 848 | nav.search(option) 849 | if len(nav.search_result) == 1: 850 | print('Only match Host: %s ' % nav.search_result[0].hostname) 851 | nav.try_connect() 852 | else: 853 | nav.print_search_result() 854 | 855 | except IndexError, e: 856 | color_print(e) 857 | time.sleep(5) 858 | 859 | if __name__ == '__main__': 860 | main() 861 | -------------------------------------------------------------------------------- /NewInit.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # 3 | #author: xwjr.com 4 | 5 | 6 | trap '' SIGINT 7 | base_dir=$(dirname $0) 8 | 9 | export LANG='zh_CN.UTF-8' 10 | 11 | TIME=`date +%H:%M` 12 | python /app/jumpserver/NewLogin.py $USER $TIME >> /dev/null 2>&1 & 13 | 14 | python $base_dir/NewConnect.py $USER $TIME 15 | 16 | 17 | exit 18 | -------------------------------------------------------------------------------- /NewLogin.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | #_*_coding:utf-8 _*_ 3 | #author: xwjr.com 4 | 5 | import cgi 6 | import urllib,urllib2 7 | import json 8 | import sys 9 | import simplejson 10 | 11 | 12 | def gettoken(corpid,corpsecret): 13 | gettoken_url = 'https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=' + corpid + '&corpsecret=' + corpsecret 14 | #print gettoken_url 15 | try: 16 | token_file = urllib2.urlopen(gettoken_url) 17 | except urllib2.HTTPError as e: 18 | print e.code 19 | print e.read().decode("utf8") 20 | sys.exit() 21 | token_data = token_file.read().decode('utf-8') 22 | token_json = json.loads(token_data) 23 | token_json.keys() 24 | token = token_json['access_token'] 25 | return token 26 | 27 | 28 | 29 | def senddata(access_token,TYPE,toinfo,subject,content): 30 | #print TYPE 31 | send_url = 'https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=' + access_token 32 | send_values = { 33 | TYPE:toinfo, #企业号中的用户帐号,在zabbix用户Media中配置,如果配置不正常,将按部门发送。 34 | "msgtype":"text", #消息类型。 35 | "agentid":"17", #企业号中的应用id。 36 | "text":{ 37 | "content":subject + '\n' + content 38 | }, 39 | "safe":"0" 40 | } 41 | # send_data = json.dumps(send_values, ensure_ascii=False).encode('utf-8') 42 | send_data = simplejson.dumps(send_values, ensure_ascii=False).encode('utf-8') 43 | send_request = urllib2.Request(send_url, send_data) 44 | response = json.loads(urllib2.urlopen(send_request).read()) 45 | #print str(response) 46 | return (response) 47 | 48 | 49 | if __name__ == '__main__': 50 | 51 | totag = "14" 52 | USER = str(sys.argv[1]) 53 | TIME = str(sys.argv[2]) 54 | content = "登陆用户:" + USER + '\n' + "时间:" + TIME 55 | subject = "希望金融小分队跳板机登陆提醒" 56 | TYPE = "totag" 57 | corpid = '****************' 58 | corpsecret = '**********************' 59 | accesstoken = gettoken(corpid,corpsecret) 60 | senddata(accesstoken,TYPE,totag,subject,content) 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jumpserver-VerificationCode 2 | 3 | ### 介绍 4 | 本项目为[jumpserver](https://github.com/jumpserver/jumpserver)添加了企业微信验证码功能 5 | ### 使用方法 6 | 1. 3个脚本放置与jumpserver根目录 7 | 2. 修改 **/etc/passwd** 中的**init.sh**修改为**NewInit.sh** 8 | ### 效果图 9 | ![image](https://raw.githubusercontent.com/XWJR-Ops/Jumpserver-VerificationCode/master/image/1.jpg) 10 | ![image](https://raw.githubusercontent.com/XWJR-Ops/Jumpserver-VerificationCode/master/image/2.jpg) 11 | ![image](https://raw.githubusercontent.com/XWJR-Ops/Jumpserver-VerificationCode/master/image/3.jpg) -------------------------------------------------------------------------------- /image/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XWJR-Ops/Jumpserver-VerificationCode/11001260fd62f223a40de1d9bf0d6b1f7bb8065e/image/1.jpg -------------------------------------------------------------------------------- /image/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XWJR-Ops/Jumpserver-VerificationCode/11001260fd62f223a40de1d9bf0d6b1f7bb8065e/image/2.jpg -------------------------------------------------------------------------------- /image/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/XWJR-Ops/Jumpserver-VerificationCode/11001260fd62f223a40de1d9bf0d6b1f7bb8065e/image/3.jpg --------------------------------------------------------------------------------