From c103738302c8b6aa209ece442a29b3e365c98c20 Mon Sep 17 00:00:00 2001 From: ibuler Date: Sat, 19 Dec 2015 17:26:34 +0800 Subject: [PATCH] merge with dev --- .DS_Store | Bin 6148 -> 0 bytes .gitignore | 10 +- README.md | 78 +- connect.py | 1010 +++++++++---- docs/AddUserAsset.py | 141 -- docs/__init__.py | 1 - docs/developer_doc.txt | 36 - docs/install.py | 135 -- docs/requirements.txt | 9 - docs/zzjumpserver.sh | 13 - jasset/models.py | 113 +- jasset/urls.py | 40 +- jasset/views.py | 1324 ++++++----------- jlog/models.py | 36 +- jlog/urls.py | 12 +- jlog/views.py | 182 ++- jperm/models.py | 78 +- jperm/urls.py | 47 +- jperm/views.py | 1221 +++++++-------- jumpserver.conf | 27 +- jumpserver/api.py | 542 ++++--- jumpserver/context_processors.py | 26 +- jumpserver/settings.py | 38 +- jumpserver/templatetags/mytags.py | 520 +++---- jumpserver/urls.py | 32 +- jumpserver/views.py | 503 ++++--- juser/models.py | 53 +- juser/tests.py | 3 - juser/urls.py | 48 +- juser/views.py | 841 ++++------- manage.py | 0 service.sh | 20 +- static/.DS_Store | Bin 6148 -> 0 bytes static/css/style.css | 15 +- static/img/root.png | Bin 107108 -> 108909 bytes static/js/base.js | 63 +- static/js/dropzone/dropzone.js | 2 +- static/js/highcharts/highcharts.src.js | 6 +- static/js/jquery-ui-1.10.4.min.js | 2 +- static/js/jquery.colorbox.js | 2 +- static/js/layer/extend/layer.ext.js | 14 +- static/js/layer/skin/default/icon_ext.png | Bin 7677 -> 0 bytes static/js/layer/skin/default/textbg.png | Bin 210 -> 0 bytes static/js/layer/skin/default/xubox_ico0.png | Bin 32954 -> 0 bytes .../js/layer/skin/default/xubox_loading0.gif | Bin 5793 -> 0 bytes .../js/layer/skin/default/xubox_loading1.gif | Bin 701 -> 0 bytes .../js/layer/skin/default/xubox_loading2.gif | Bin 1787 -> 0 bytes .../js/layer/skin/default/xubox_loading3.gif | Bin 2364 -> 0 bytes static/js/layer/skin/default/xubox_title0.png | Bin 221 -> 0 bytes static/js/layer/skin/layer.css | 82 +- static/js/layer/skin/layer.ext.css | 41 +- templates/base.html | 5 +- templates/download.html | 59 +- templates/foot_script.html | 21 +- templates/footer.html | 4 +- templates/head_script.html | 1 + templates/index.html | 551 ++++--- templates/index_cu.html | 146 +- templates/jasset/dept_host_ajax.html | 3 - templates/jasset/group_add.html | 69 +- templates/jasset/group_detail.html | 203 --- templates/jasset/group_edit.html | 35 +- templates/jasset/group_list.html | 113 +- templates/jasset/host_add.html | 207 --- templates/jasset/host_add_multi.html | 68 - templates/jasset/host_detail.html | 221 --- templates/jasset/host_edit.html | 228 --- templates/jasset/host_list.html | 192 --- templates/jasset/host_list_common.html | 172 --- templates/jasset/host_list_nop.html | 177 --- templates/jasset/host_search.html | 169 --- templates/jasset/idc_add.html | 68 +- templates/jasset/idc_detail.html | 211 --- templates/jasset/idc_edit.html | 90 +- templates/jasset/idc_list.html | 94 +- templates/jasset/jasset.html | 10 - templates/jasset/jlist_ip.html | 106 -- templates/jlog/log_offline.html | 184 ++- templates/jlog/log_online.html | 245 ++- templates/jlog/log_search.html | 2 +- templates/jlog/user_history.html | 2 +- templates/jperm/dept_perm_edit.html | 179 --- templates/jperm/dept_perm_list.html | 104 -- templates/jperm/perm_add.html | 176 --- templates/jperm/perm_apply.html | 187 --- templates/jperm/perm_apply_exec.html | 31 - templates/jperm/perm_apply_info.html | 55 - templates/jperm/perm_apply_search.html | 40 - templates/jperm/perm_asset_detail.html | 61 - templates/jperm/perm_detail.html | 118 -- templates/jperm/perm_edit.html | 130 -- templates/jperm/perm_edit_bak.html | 138 -- templates/jperm/perm_list.html | 108 -- templates/jperm/perm_list_ajax.html | 132 -- templates/jperm/perm_log.html | 123 +- templates/jperm/perm_log_offline.html | 127 -- templates/jperm/perm_log_online.html | 128 -- templates/jperm/perm_user_detail.html | 240 --- templates/jperm/sudo_add.html | 226 --- templates/jperm/sudo_cmd_add.html | 148 -- templates/jperm/sudo_cmd_detail.html | 48 - templates/jperm/sudo_cmd_list.html | 140 -- templates/jperm/sudo_edit.html | 155 -- templates/jperm/sudo_list.html | 129 -- templates/juser/chg_info.html | 132 -- templates/juser/dept_add.html | 133 -- templates/juser/dept_detail.html | 116 -- templates/juser/dept_edit.html | 133 -- templates/juser/dept_list.html | 126 -- templates/juser/dept_user_ajax.html | 3 - templates/juser/group_add.html | 51 +- templates/juser/group_add_ajax.html | 4 - templates/juser/group_detail.html | 2 +- templates/juser/group_edit.html | 61 +- templates/juser/group_list.html | 82 +- templates/juser/profile.html | 26 +- templates/juser/user_add.html | 134 +- templates/juser/user_detail.html | 97 +- templates/juser/user_edit.html | 149 +- templates/juser/user_list.html | 141 +- templates/login.html | 2 +- templates/nav.html | 164 +- templates/nav_bar_header.html | 56 +- templates/nav_cat_bar.html | 6 +- templates/nav_li_profile.html | 23 +- templates/paginator.html | 80 +- templates/test.html | 74 - templates/upload.html | 103 +- version | 1 - websocket/.bin/node-tail | 17 - websocket/index.js | 126 -- websocket/npm-debug.log | 888 ----------- websocket/package.json | 12 - 133 files changed, 5006 insertions(+), 11881 deletions(-) delete mode 100644 .DS_Store mode change 100644 => 100755 connect.py delete mode 100644 docs/AddUserAsset.py delete mode 100644 docs/__init__.py delete mode 100644 docs/developer_doc.txt delete mode 100644 docs/install.py delete mode 100644 docs/requirements.txt delete mode 100644 docs/zzjumpserver.sh delete mode 100644 juser/tests.py mode change 100644 => 100755 manage.py mode change 100644 => 100755 service.sh delete mode 100644 static/.DS_Store mode change 100644 => 100755 static/js/layer/extend/layer.ext.js delete mode 100644 static/js/layer/skin/default/icon_ext.png delete mode 100644 static/js/layer/skin/default/textbg.png delete mode 100644 static/js/layer/skin/default/xubox_ico0.png delete mode 100644 static/js/layer/skin/default/xubox_loading0.gif delete mode 100644 static/js/layer/skin/default/xubox_loading1.gif delete mode 100644 static/js/layer/skin/default/xubox_loading2.gif delete mode 100644 static/js/layer/skin/default/xubox_loading3.gif delete mode 100644 static/js/layer/skin/default/xubox_title0.png mode change 100644 => 100755 static/js/layer/skin/layer.css mode change 100644 => 100755 static/js/layer/skin/layer.ext.css delete mode 100644 templates/jasset/dept_host_ajax.html delete mode 100644 templates/jasset/group_detail.html delete mode 100644 templates/jasset/host_add.html delete mode 100644 templates/jasset/host_add_multi.html delete mode 100644 templates/jasset/host_detail.html delete mode 100644 templates/jasset/host_edit.html delete mode 100644 templates/jasset/host_list.html delete mode 100644 templates/jasset/host_list_common.html delete mode 100644 templates/jasset/host_list_nop.html delete mode 100644 templates/jasset/host_search.html delete mode 100644 templates/jasset/idc_detail.html delete mode 100644 templates/jasset/jasset.html delete mode 100644 templates/jasset/jlist_ip.html delete mode 100644 templates/jperm/dept_perm_edit.html delete mode 100644 templates/jperm/dept_perm_list.html delete mode 100644 templates/jperm/perm_add.html delete mode 100644 templates/jperm/perm_apply.html delete mode 100644 templates/jperm/perm_apply_exec.html delete mode 100644 templates/jperm/perm_apply_info.html delete mode 100644 templates/jperm/perm_apply_search.html delete mode 100644 templates/jperm/perm_asset_detail.html delete mode 100644 templates/jperm/perm_detail.html delete mode 100644 templates/jperm/perm_edit.html delete mode 100644 templates/jperm/perm_edit_bak.html delete mode 100644 templates/jperm/perm_list.html delete mode 100644 templates/jperm/perm_list_ajax.html delete mode 100644 templates/jperm/perm_log_offline.html delete mode 100644 templates/jperm/perm_log_online.html delete mode 100644 templates/jperm/perm_user_detail.html delete mode 100644 templates/jperm/sudo_add.html delete mode 100644 templates/jperm/sudo_cmd_add.html delete mode 100644 templates/jperm/sudo_cmd_detail.html delete mode 100644 templates/jperm/sudo_cmd_list.html delete mode 100644 templates/jperm/sudo_edit.html delete mode 100644 templates/jperm/sudo_list.html delete mode 100644 templates/juser/chg_info.html delete mode 100644 templates/juser/dept_add.html delete mode 100644 templates/juser/dept_detail.html delete mode 100644 templates/juser/dept_edit.html delete mode 100644 templates/juser/dept_list.html delete mode 100644 templates/juser/dept_user_ajax.html delete mode 100644 templates/juser/group_add_ajax.html delete mode 100644 templates/test.html delete mode 100644 version delete mode 100644 websocket/.bin/node-tail delete mode 100644 websocket/index.js delete mode 100644 websocket/npm-debug.log delete mode 100644 websocket/package.json diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index 228971d07180d06cd79ed2aa5688f4d23b0bfcb3..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHK&1%~~5S~rkL~$-ENlGA>y#{=614YeEanfVKppsCM;!2gMHz<;kWSd|Ny0(we zzw|Xq^AdfMUOTh9DRG@#gDEp&=9}HwSxKLyT@3(;&M=Bz)0J<;oQLtFarz50DZenIENuzzyNN2ztOlHL}?yj{C6}+Rav#$zr@mV z3QCC>V9yXo!w^#FJBL)A3uHm^7Y%*_a8saqY$`sY1y!N1J~%>NzEuI zvNX@$W1MnVkhTg!R0Z#tHll5Yl7HyJFv~(PU|)P^eIc%fFM=6h1{R3{S|21Tp>MG^ zsFw~jx&%O^-v}+}Q!PO`(xPv%Hi!`vVNwxIs<0)7FzMJY?Kt0JZP28Hu*HY4XBM_X z5&G;nztqD)_y*ZB1I)lI1IwoA(*FPA{`!A5iCfG7Gw@$AAR0%(Q3r3y?yYk-M|-VA seTzy$<7$IC1&!T~wL@F+HmVkkNpcWTd;6WMq1LBIkxBvhE diff --git a/.gitignore b/.gitignore index b749de2d9..983fedd49 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ *.py[cod] .idea test.py - +.DS_Store +db.sqlite3 # C extensions *.so @@ -36,8 +37,9 @@ nosetests.xml .mr.developer.cfg .project .pydevproject -node_modules -logs -keys +*.log +logs/* +keys/* jumpserver.conf nohup.out +tmp/* diff --git a/README.md b/README.md index 2a40fba79..e733655c2 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,68 @@ #欢迎使用Jumpserver -**Jumpserver**是一款由python编写开源的跳板机(堡垒机)系统,实现了跳板机应有的功能 +**Jumpserver** 是一款由python编写开源的跳板机(堡垒机)系统,实现了跳板机应有的功能 +###截图: +首页 + +![webterminal](https://github.com/ibuler/static/raw/master/jumpserver3/index.jpeg) -> **统计管理** 统一管理用户 -> -> **授权** 授权用户登录特定主机 -> -> **审计** 审计用户操作 -> -> **web管理** 漂亮的web管理界面 +WebTerminal: -## 主要模块 -#### 用户管理 #### - 负责用户管理,添加用户,编辑用户,建立部门,建立用户组等 -#### 资产管理 #### - 负责资产管理,添加资产,编辑资产,建立IDC,建立用户组等 -#### 授权管理 #### - 负责授权用户登录某些特定主机,授权sudo,查看授权申请 -#### 日志审计 #### - 负责用户操作的审计,监控用户操作,统计用户操作记录,中断用户操作 -#### 上传下载 #### - 负责用户文件上传下载 +![webterminal](https://github.com/ibuler/static/raw/master/jumpserver3/webTerminal.gif) -[官网](http://www.jumpserver.org) +Web批量执行命令 + +![WebExecCommand](https://github.com/ibuler/static/raw/master/jumpserver3/webExec.gif) + +录像回放 + +![录像](https://github.com/ibuler/static/raw/master/jumpserver3/record.gif) + +跳转和批量命令 + +![跳转](https://github.com/ibuler/static/raw/master/jumpserver3/connect.gif) + +命令统计 + +![跳转](https://github.com/ibuler/static/raw/master/jumpserver3/command.png) + +### 文档 + +* [访问wiki](https://github.com/ibuler/jumpserver/wiki) +* [快速安装](https://github.com/ibuler/jumpserver/wiki/快速安装) +* [名词解释](https://github.com/ibuler/jumpserver/wiki/名称解释) +* [快速开始](https://github.com/ibuler/jumpserver/wiki/快速开始) + +### 特点 + +* 完全真开源,GPL授权 +* Python编写,容易再次开发 +* 实现了跳板机基本功能,认证、授权、审计 +* 集成了Ansible,批量命令等 +* 支持WebTerminal +* Bootstrap编写,界面美观 +* 自动收集硬件信息 +* 录像回放 +* 命令搜索 +* 实时监控 +* 批量上传下载 + +### 其它 + +[Jumpserver官网](http://www.jumpserver.org) [demo站点](http://demo.jumpserver.org) -[更新log](http://laoguang.blog.51cto.com/6013350/1635853) +### 团队 + +* **广宏伟** ibuler +* **王墉** halcyon +* **陈尚委** 假想控 +* **喻茂峻** 紫川秀 +* **刘正** evanescunt +* **柯连春** 遍地节操 + -[部署文档](http://laoguang.blog.51cto.com/6013350/1636273) diff --git a/connect.py b/connect.py old mode 100644 new mode 100755 index dc74a6701..89dc43b16 --- a/connect.py +++ b/connect.py @@ -5,80 +5,241 @@ import sys reload(sys) sys.setdefaultencoding('utf8') -import socket import os import re -import select import time -import paramiko -import struct -import fcntl -import signal +import datetime import textwrap import getpass -import fnmatch import readline -import datetime -from multiprocessing import Pool +import django +import paramiko +import errno +import struct, fcntl, signal, socket, select +from io import open as copen +import uuid os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' -from juser.models import User -from jlog.models import Log -from jumpserver.api import CONF, BASE_DIR, ServerError, user_perm_group_api, user_perm_group_hosts_api, get_user_host -from jumpserver.api import AssetAlias, get_connect_item +if django.get_version() != '1.6': + setup = django.setup() +from django.contrib.sessions.models import Session +from jumpserver.api import ServerError, User, Asset, PermRole, AssetGroup, get_object, mkdir, get_asset_info +from jumpserver.api import logger, Log, TtyLog, get_role_key, CRYPTOR, bash, get_tmp_dir +from jperm.perm_api import gen_resource, get_group_asset_perm, get_group_user_perm, user_have_perm, PermRole +from jumpserver.settings import LOG_DIR +from jperm.ansible_api import MyRunner +# from jlog.log_api import escapeString +from jlog.models import ExecLog, FileLog +login_user = get_object(User, username=getpass.getuser()) +remote_ip = os.popen("who -m | awk '{ print $5 }'").read().strip('()\n') try: import termios import tty except ImportError: - print '\033[1;31mOnly UnixLike supported.\033[0m' + print '\033[1;31m仅支持类Unix系统 Only unix like supported.\033[0m' time.sleep(3) sys.exit() -CONF.read(os.path.join(BASE_DIR, 'jumpserver.conf')) -LOG_DIR = os.path.join(BASE_DIR, 'logs') -SSH_KEY_DIR = os.path.join(BASE_DIR, 'keys') -SERVER_KEY_DIR = os.path.join(SSH_KEY_DIR, 'server') -LOGIN_NAME = getpass.getuser() - -def color_print(msg, color='blue'): - """Print colorful string.""" +def color_print(msg, color='red', exits=False): + """ + Print colorful string. + 颜色打印字符或者退出 + """ color_msg = {'blue': '\033[1;36m%s\033[0m', 'green': '\033[1;32m%s\033[0m', - 'red': '\033[1;31m%s\033[0m'} - - print color_msg.get(color, 'blue') % msg + 'yellow': '\033[1;33m%s\033[0m', + 'red': '\033[1;31m%s\033[0m', + 'title': '\033[30;42m%s\033[0m', + 'info': '\033[32m%s\033[0m'} + msg = color_msg.get(color, 'red') % msg + print msg + if exits: + time.sleep(2) + sys.exit() + return msg -def color_print_exit(msg, color='red'): - """Print colorful string and exit.""" - color_print(msg, color=color) - time.sleep(2) - sys.exit() +def write_log(f, msg): + msg = re.sub(r'[\r\n]', '\r\n', msg) + f.write(msg) + f.flush() -def get_win_size(): - """This function use to get the size of the windows!""" - if 'TIOCGWINSZ' in dir(termios): - TIOCGWINSZ = termios.TIOCGWINSZ - else: - TIOCGWINSZ = 1074295912L # Assume - s = struct.pack('HHHH', 0, 0, 0, 0) - x = fcntl.ioctl(sys.stdout.fileno(), TIOCGWINSZ, s) - return struct.unpack('HHHH', x)[0:2] +class Tty(object): + """ + A virtual tty class + 一个虚拟终端类,实现连接ssh和记录日志,基类 + """ + def __init__(self, user, asset, role, login_type='ssh'): + self.username = user.username + self.asset_name = asset.hostname + self.ip = None + self.port = 22 + self.ssh = None + self.channel = None + self.asset = asset + self.user = user + self.role = role + self.remote_ip = '' + self.login_type = login_type + self.vim_flag = False + self.ps1_pattern = re.compile('\[.*@.*\][\$#]') + self.vim_data = '' + @staticmethod + def is_output(strings): + newline_char = ['\n', '\r', '\r\n'] + for char in newline_char: + if char in strings: + return True + return False -def set_win_size(sig, data): - """This function use to set the window size of the terminal!""" - try: - win_size = get_win_size() - channel.resize_pty(height=win_size[0], width=win_size[1]) - except: - pass + @staticmethod + def remove_obstruct_char(cmd_str): + '''删除一些干扰的特殊符号''' + control_char = re.compile(r'\x07 | \x1b\[1P | \r ', re.X) + cmd_str = control_char.sub('',cmd_str.strip()) + patch_char = re.compile('\x08\x1b\[C') #删除方向左右一起的按键 + while patch_char.search(cmd_str): + cmd_str = patch_char.sub('', cmd_str.rstrip()) + return cmd_str + @staticmethod + def deal_backspace(match_str, result_command, pattern_str, backspace_num): + ''' + 处理删除确认键 + ''' + if backspace_num > 0: + if backspace_num > len(result_command): + result_command += pattern_str + result_command = result_command[0:-backspace_num] + else: + result_command = result_command[0:-backspace_num] + result_command += pattern_str + del_len = len(match_str)-3 + if del_len > 0: + result_command = result_command[0:-del_len] + return result_command, len(match_str) + + @staticmethod + def deal_replace_char(match_str,result_command,backspace_num): + ''' + 处理替换命令 + ''' + str_lists = re.findall(r'(?<=\x1b\[1@)\w',match_str) + tmp_str =''.join(str_lists) + result_command_list = list(result_command) + if len(tmp_str) > 1: + result_command_list[-backspace_num:-(backspace_num-len(tmp_str))] = tmp_str + elif len(tmp_str) > 0: + if result_command_list[-backspace_num] == ' ': + result_command_list.insert(-backspace_num, tmp_str) + else: + result_command_list[-backspace_num] = tmp_str + result_command = ''.join(result_command_list) + return result_command, len(match_str) + + def remove_control_char(self, result_command): + """ + 处理日志特殊字符 + """ + control_char = re.compile(r""" + \x1b[ #%()*+\-.\/]. | + \r | #匹配 回车符(CR) + (?:\x1b\[|\x9b) [ -?]* [@-~] | #匹配 控制顺序描述符(CSI)... Cmd + (?:\x1b\]|\x9d) .*? (?:\x1b\\|[\a\x9c]) | \x07 | #匹配 操作系统指令(OSC)...终止符或振铃符(ST|BEL) + (?:\x1b[P^_]|[\x90\x9e\x9f]) .*? (?:\x1b\\|\x9c) | #匹配 设备控制串或私讯或应用程序命令(DCS|PM|APC)...终止符(ST) + \x1b. #匹配 转义过后的字符 + [\x80-\x9f] | (?:\x1b\]0.*) | \[.*@.*\][\$#] | (.*mysql>.*) #匹配 所有控制字符 + """, re.X) + result_command = control_char.sub('', result_command.strip()) + + if not self.vim_flag: + if result_command.startswith('vi') or result_command.startswith('fg'): + self.vim_flag = True + return result_command.decode('utf8',"ignore") + else: + return '' + def deal_command(self, str_r): + """ + 处理命令中特殊字符 + """ + str_r = self.remove_obstruct_char(str_r) + + result_command = '' # 最后的结果 + backspace_num = 0 # 光标移动的个数 + reach_backspace_flag = False # 没有检测到光标键则为true + pattern_str = '' + while str_r: + tmp = re.match(r'\s*\w+\s*', str_r) + if tmp: + str_r = str_r[len(str(tmp.group(0))):] + if reach_backspace_flag: + pattern_str += str(tmp.group(0)) + continue + else: + result_command += str(tmp.group(0)) + continue + + tmp = re.match(r'\x1b\[K[\x08]*', str_r) + if tmp: + result_command, del_len = self.deal_backspace(str(tmp.group(0)), result_command, pattern_str, backspace_num) + reach_backspace_flag = False + backspace_num = 0 + pattern_str = '' + str_r = str_r[del_len:] + continue + + tmp = re.match(r'\x08+', str_r) + if tmp: + str_r = str_r[len(str(tmp.group(0))):] + if len(str_r) != 0: + if reach_backspace_flag: + result_command = result_command[0:-backspace_num] + pattern_str + pattern_str = '' + else: + reach_backspace_flag = True + backspace_num = len(str(tmp.group(0))) + continue + else: + break + + tmp = re.match(r'(\x1b\[1@\w)+', str_r) #处理替换的命令 + if tmp: + result_command,del_len = self.deal_replace_char(str(tmp.group(0)), result_command, backspace_num) + str_r = str_r[del_len:] + backspace_num = 0 + continue + + if reach_backspace_flag: + pattern_str += str_r[0] + else: + result_command += str_r[0] + str_r = str_r[1:] + + if backspace_num > 0: + result_command = result_command[0:-backspace_num] + pattern_str + + result_command = self.remove_control_char(result_command) + return result_command + + def get_log(self): + """ + Logging user command and output. + 记录用户的日志 + """ + tty_log_dir = os.path.join(LOG_DIR, 'tty') + date_today = datetime.datetime.now() + date_start = date_today.strftime('%Y%m%d') + time_start = date_today.strftime('%H%M%S') + today_connect_log_dir = os.path.join(tty_log_dir, date_start) + log_file_path = os.path.join(today_connect_log_dir, '%s_%s_%s' % (self.username, self.asset_name, time_start)) + +<<<<<<< HEAD def log_record(username, host): """Logging user command and output.""" connect_log_dir = os.path.join(LOG_DIR, 'connect') @@ -94,305 +255,570 @@ def log_record(username, host): ip_list = os.popen("who | grep %s | awk '{ print $5 }'" % pts).read().strip('()\n') if not os.path.isdir(today_connect_log_dir): +======= +>>>>>>> dev try: - os.makedirs(today_connect_log_dir) - os.chmod(today_connect_log_dir, 0777) + mkdir(os.path.dirname(today_connect_log_dir), mode=0777) + mkdir(today_connect_log_dir, mode=0777) except OSError: - raise ServerError('Create %s failed, Please modify %s permission.' % (today_connect_log_dir, connect_log_dir)) + logger.debug('创建目录 %s 失败,请修改%s目录权限' % (today_connect_log_dir, tty_log_dir)) + raise ServerError('创建目录 %s 失败,请修改%s目录权限' % (today_connect_log_dir, tty_log_dir)) - try: - log_file = open(log_file_path, 'a') - except IOError: - raise ServerError('Create logfile failed, Please modify %s permission.' % today_connect_log_dir) + try: + log_file_f = open(log_file_path + '.log', 'a') + log_time_f = open(log_file_path + '.time', 'a') + except IOError: + logger.debug('创建tty日志文件失败, 请修改目录%s权限' % today_connect_log_dir) + raise ServerError('创建tty日志文件失败, 请修改目录%s权限' % today_connect_log_dir) - log = Log(user=username, host=host, remote_ip=ip_list, dept_name=dept_name, - log_path=log_file_path, start_time=datetime.datetime.now(), pid=pid) - log_file.write('Starttime is %s\n' % datetime.datetime.now()) - log.save() - return log_file, log + if self.login_type == 'ssh': # 如果是ssh连接过来,记录connect.py的pid,web terminal记录为日志的id + pid = os.getpid() + self.remote_ip = remote_ip # 获取远端IP + else: + pid = 0 + log = Log(user=self.username, host=self.asset_name, remote_ip=self.remote_ip, login_type=self.login_type, + log_path=log_file_path, start_time=date_today, pid=pid) + log.save() + if self.login_type == 'web': + log.pid = log.id # 设置log id为websocket的id, 然后kill时干掉websocket + log.save() -def posix_shell(chan, username, host): - """ - Use paramiko channel connect server interactive. - """ - log_file, log = log_record(username, host) - old_tty = termios.tcgetattr(sys.stdin) - try: - tty.setraw(sys.stdin.fileno()) - tty.setcbreak(sys.stdin.fileno()) - chan.settimeout(0.0) + log_file_f.write('Start at %s\r\n' % datetime.datetime.now()) + return log_file_f, log_time_f, log - while True: - try: - r, w, e = select.select([chan, sys.stdin], [], []) - except: - pass + def get_connect_info(self): + """ + 获取需要登陆的主机的信息和映射用户的账号密码 + """ + asset_info = get_asset_info(self.asset) + role_key = get_role_key(self.user, self.role) # 获取角色的key,因为ansible需要权限是600,所以统一生成用户_角色key + role_pass = CRYPTOR.decrypt(self.role.password) + connect_info = {'user': self.user, 'asset': self.asset, 'ip': asset_info.get('ip'), + 'port': int(asset_info.get('port')), 'role_name': self.role.name, + 'role_pass': role_pass, 'role_key': role_key} + logger.debug(connect_info) + return connect_info - if chan in r: + def get_connection(self): + """ + 获取连接成功后的ssh + """ + connect_info = self.get_connect_info() + + # 发起ssh连接请求 Make a ssh connection + ssh = paramiko.SSHClient() + ssh.load_system_host_keys() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + try: + role_key = connect_info.get('role_key') + if role_key and os.path.isfile(role_key): try: - x = chan.recv(1024) - if len(x) == 0: - break - sys.stdout.write(x) - sys.stdout.flush() - log_file.write(x) - log_file.flush() - except socket.timeout: + ssh.connect(connect_info.get('ip'), + port=connect_info.get('port'), + username=connect_info.get('role_name'), + password=connect_info.get('role_pass'), + key_filename=role_key, + look_for_keys=False) + return ssh + except (paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException): + logger.warning(u'使用ssh key %s 失败, 尝试只使用密码' % role_key) pass - if sys.stdin in r: - x = os.read(sys.stdin.fileno(), 1) - if len(x) == 0: - break - chan.send(x) + ssh.connect(connect_info.get('ip'), + port=connect_info.get('port'), + username=connect_info.get('role_name'), + password=connect_info.get('role_pass'), + allow_agent=False, + look_for_keys=False) - finally: - termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) - log_file.write('Endtime is %s' % datetime.datetime.now()) - log_file.close() - log.is_finished = True - log.log_finished = False - log.end_time = datetime.datetime.now() - log.save() - print_prompt() - - -def get_user_hostgroup(username): - """Get the hostgroups of under the user control.""" - groups_attr = {} - group_all = user_perm_group_api(username) - for group in group_all: - groups_attr[group.name] = [group.id, group.comment] - return groups_attr - - -def get_user_hostgroup_host(username, gid): - """Get the hostgroup hosts of under the user control.""" - hosts_attr = {} - user = User.objects.get(username=username) - hosts = user_perm_group_hosts_api(gid) - for host in hosts: - alias = AssetAlias.objects.filter(user=user, host=host) - if alias and alias[0].alias != '': - hosts_attr[host.ip] = [host.id, host.ip, alias[0].alias] + except paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException: + raise ServerError('认证失败 Authentication Error.') + except socket.error: + raise ServerError('端口可能不对 Connect SSH Socket Port Error, Please Correct it.') else: - hosts_attr[host.ip] = [host.id, host.ip, host.comment] - return hosts_attr + self.ssh = ssh + return ssh -def verify_connect(username, part_ip): - ip_matched = [] - try: - hosts_attr = get_user_host(username) - hosts = hosts_attr.values() - except ServerError, e: - color_print(e, 'red') - return False - - for ip_info in hosts: - if part_ip in ip_info[1:] and part_ip: - ip_matched = [ip_info[1]] - break - for info in ip_info[1:]: - if part_ip in info: - ip_matched.append(ip_info[1]) - - ip_matched = list(set(ip_matched)) - if len(ip_matched) > 1: - for ip in ip_matched: - print '%-15s -- %s' % (ip, hosts_attr[ip][2]) - elif len(ip_matched) < 1: - color_print('No Permission or No host.', 'red') - else: - username, password, host, port = get_connect_item(username, ip_matched[0]) - connect(username, password, host, port, LOGIN_NAME) - - -def print_prompt(): - msg = """\033[1;32m### Welcome Use JumpServer To Login. ### \033[0m - 1) Type \033[32mIP or Part IP, Host Alias or Comments \033[0m To Login. - 2) Type \033[32mP/p\033[0m To Print The Servers You Available. - 3) Type \033[32mG/g\033[0m To Print The Server Groups You Available. - 4) Type \033[32mG/g(1-N)\033[0m To Print The Server Group Hosts You Available. - 5) Type \033[32mE/e\033[0m To Execute Command On Several Servers. - 6) Type \033[32mQ/q\033[0m To Quit. +class SshTty(Tty): """ - print textwrap.dedent(msg) - - -def print_user_host(username): - try: - hosts_attr = get_user_host(username) - except ServerError, e: - color_print(e, 'red') - return - hosts = hosts_attr.keys() - hosts.sort() - for ip in hosts: - print '%-15s -- %s' % (ip, hosts_attr[ip][2]) - print '' - - -def print_user_hostgroup(username): - group_attr = get_user_hostgroup(username) - groups = group_attr.keys() - for g in groups: - print "[%3s] %s -- %s" % (group_attr[g][0], g, group_attr[g][1]) - - -def print_user_hostgroup_host(username, gid): - pattern = re.compile(r'\d+') - match = pattern.match(gid) - if match: - hosts_attr = get_user_hostgroup_host(username, gid) - hosts = hosts_attr.keys() - hosts.sort() - for ip in hosts: - print '%-15s -- %s' % (ip, hosts_attr[ip][2]) - else: - color_print('No such group id, Please check it.', 'red') - - -def connect(username, password, host, port, login_name): + A virtual tty class + 一个虚拟终端类,实现连接ssh和记录日志 """ - Connect server. - """ - ps1 = "PS1='[\u@%s \W]\$ '\n" % host - login_msg = "clear;echo -e '\\033[32mLogin %s done. Enjoy it.\\033[0m'\n" % host - # Make a ssh connection - ssh = paramiko.SSHClient() - ssh.load_system_host_keys() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - try: - ssh.connect(host, port=port, username=username, password=password, compress=True) - except paramiko.ssh_exception.AuthenticationException, paramiko.ssh_exception.SSHException: - raise ServerError('Authentication Error.') - except socket.error: - raise ServerError('Connect SSH Socket Port Error, Please Correct it.') + @staticmethod + def get_win_size(): + """ + This function use to get the size of the windows! + 获得terminal窗口大小 + """ + if 'TIOCGWINSZ' in dir(termios): + TIOCGWINSZ = termios.TIOCGWINSZ + else: + TIOCGWINSZ = 1074295912L + s = struct.pack('HHHH', 0, 0, 0, 0) + x = fcntl.ioctl(sys.stdout.fileno(), TIOCGWINSZ, s) + return struct.unpack('HHHH', x)[0:2] - # Make a channel and set windows size - global channel - win_size = get_win_size() - channel = ssh.invoke_shell(height=win_size[0], width=win_size[1]) - try: - signal.signal(signal.SIGWINCH, set_win_size) - except: - pass + def set_win_size(self, sig, data): + """ + This function use to set the window size of the terminal! + 设置terminal窗口大小 + """ + try: + win_size = self.get_win_size() + self.channel.resize_pty(height=win_size[0], width=win_size[1]) + except Exception: + pass - # Set PS1 and msg it - channel.send(ps1) - channel.send(login_msg) + def posix_shell(self): + """ + Use paramiko channel connect server interactive. + 使用paramiko模块的channel,连接后端,进入交互式 + """ + log_file_f, log_time_f, log = self.get_log() + old_tty = termios.tcgetattr(sys.stdin) + pre_timestamp = time.time() + data = '' + input_mode = False + try: + tty.setraw(sys.stdin.fileno()) + tty.setcbreak(sys.stdin.fileno()) + self.channel.settimeout(0.0) - # Make ssh interactive tunnel - posix_shell(channel, login_name, host) + while True: + try: + r, w, e = select.select([self.channel, sys.stdin], [], []) + flag = fcntl.fcntl(sys.stdin, fcntl.F_GETFL, 0) + fcntl.fcntl(sys.stdin.fileno(), fcntl.F_SETFL, flag|os.O_NONBLOCK) + except Exception: + pass - # Shutdown channel socket - channel.close() - ssh.close() + if self.channel in r: + try: + x = self.channel.recv(10240) + if len(x) == 0: + break + if self.vim_flag: + self.vim_data += x + index = 0 + len_x = len(x) + while index < len_x: + try: + n = os.write(sys.stdout.fileno(), x[index:]) + sys.stdout.flush() + index += n + except OSError as msg: + if msg.errno == errno.EAGAIN: + continue + #sys.stdout.write(x) + #sys.stdout.flush() + now_timestamp = time.time() + log_time_f.write('%s %s\n' % (round(now_timestamp-pre_timestamp, 4), len(x))) + log_time_f.flush() + log_file_f.write(x) + log_file_f.flush() + pre_timestamp = now_timestamp + log_file_f.flush() + if input_mode and not self.is_output(x): + data += x -def remote_exec_cmd(ip, port, username, password, cmd): - try: - time.sleep(5) - ssh = paramiko.SSHClient() - ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - ssh.connect(ip, port, username, password, timeout=5) - stdin, stdout, stderr = ssh.exec_command("bash -l -c '%s'" % cmd) - out = stdout.readlines() - err = stderr.readlines() - color_print('%s:' % ip, 'blue') - for i in out: - color_print(" " * 4 + i.strip(), 'green') - for j in err: - color_print(" " * 4 + j.strip(), 'red') + except socket.timeout: + pass + + if sys.stdin in r: + x = os.read(sys.stdin.fileno(), 4096) + input_mode = True + if str(x) in ['\r', '\n', '\r\n']: + if self.vim_flag: + match = self.ps1_pattern.search(self.vim_data) + if match: + self.vim_flag = False + data = self.deal_command(data)[0:200] + if len(data) > 0: + TtyLog(log=log, datetime=datetime.datetime.now(), cmd=data).save() + else: + data = self.deal_command(data)[0:200] + if len(data) > 0: + TtyLog(log=log, datetime=datetime.datetime.now(), cmd=data).save() + data = '' + self.vim_data = '' + input_mode = False + + if len(x) == 0: + break + self.channel.send(x) + + finally: + termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_tty) + log_file_f.write('End time is %s' % datetime.datetime.now()) + log_file_f.close() + log_time_f.close() + log.is_finished = True + log.end_time = datetime.datetime.now() + log.save() + + def connect(self): + """ + Connect server. + 连接服务器 + """ + # 发起ssh连接请求 Make a ssh connection + ssh = self.get_connection() + + transport = ssh.get_transport() + transport.set_keepalive(30) + transport.use_compression(True) + + # 获取连接的隧道并设置窗口大小 Make a channel and set windows size + global channel + win_size = self.get_win_size() + #self.channel = channel = ssh.invoke_shell(height=win_size[0], width=win_size[1], term='xterm') + self.channel = channel = transport.open_session() + channel.get_pty(term='xterm', height=win_size[0], width=win_size[1]) + channel.invoke_shell() + try: + signal.signal(signal.SIGWINCH, self.set_win_size) + except: + pass + + self.posix_shell() + + # Shutdown channel socket + channel.close() ssh.close() - except Exception as e: - color_print(ip + ':', 'blue') - color_print(str(e), 'red') -def multi_remote_exec_cmd(hosts, username, cmd): - pool = Pool(processes=5) - for host in hosts: - username, password, ip, port = get_connect_item(username, host) - pool.apply_async(remote_exec_cmd, (ip, port, username, password, cmd)) - pool.close() - pool.join() +class Nav(object): + """ + 导航提示类 + """ + def __init__(self, user): + self.user = user + self.search_result = {} + self.user_perm = {} + @staticmethod + def print_nav(): + """ + Print prompt + 打印提示导航 + """ + msg = """\n\033[1;32m### 欢迎使用Jumpserver开源跳板机系统 ### \033[0m -def exec_cmd_servers(username): - color_print("You can choose in the following IP(s), Use glob or ips split by comma. q/Q to PreLayer.", 'green') - print_user_host(LOGIN_NAME) - while True: - hosts = [] - inputs = raw_input('\033[1;32mip(s)>: \033[0m') - if inputs in ['q', 'Q']: - break - get_hosts = get_user_host(username).keys() + 1) 输入 \033[32mID\033[0m 直接登录. + 2) 输入 \033[32m/\033[0m + \033[32mIP, 主机名 or 备注 \033[0m搜索. + 3) 输入 \033[32mP/p\033[0m 显示您有权限的主机. + 4) 输入 \033[32mG/g\033[0m 显示您有权限的主机组. + 5) 输入 \033[32mG/g\033[0m\033[0m + \033[32m组ID\033[0m 显示该组下主机. + 6) 输入 \033[32mE/e\033[0m 批量执行命令. + 7) 输入 \033[32mU/u\033[0m 批量上传文件. + 8) 输入 \033[32mD/d\033[0m 批量下载文件. + 9) 输入 \033[32mH/h\033[0m 帮助. + 0) 输入 \033[32mQ/q\033[0m 退出. + """ + print textwrap.dedent(msg) - if ',' in inputs: - ips_input = inputs.split(',') - for host in ips_input: - if host in get_hosts: - hosts.append(host) + def search(self, str_r=''): + gid_pattern = re.compile(r'^g\d+$') + # 获取用户授权的所有主机信息 + if not self.user_perm: + self.user_perm = get_group_user_perm(self.user) + user_asset_all = self.user_perm.get('asset').keys() + # 搜索结果保存 + user_asset_search = [] + if str_r: + # 资产组组id匹配 + if gid_pattern.match(str_r): + gid = int(str_r.lstrip('g')) + # 获取资产组包含的资产 + user_asset_search = get_object(AssetGroup, id=gid).asset_set.all() + else: + # 匹配 ip, hostname, 备注 + for asset in user_asset_all: + if str_r in asset.ip or str_r in str(asset.hostname) or str_r in str(asset.comment): + user_asset_search.append(asset) else: - for host in get_hosts: - if fnmatch.fnmatch(host, inputs): - hosts.append(host.strip()) + # 如果没有输入就展现所有 + user_asset_search = user_asset_all - if len(hosts) == 0: - color_print("Check again, Not matched any ip!", 'red') - continue - else: - print "You matched ip: %s" % hosts - color_print("Input the Command , The command will be Execute on servers, q/Q to quit.", 'green') + self.search_result = dict(zip(range(len(user_asset_search)), user_asset_search)) + color_print('[%-3s] %-12s %-15s %-5s %-10s %s' % ('ID', u'主机名', 'IP', u'端口', u'系统用户', u'备注'), 'title') + for index, asset in self.search_result.items(): + # 获取该资产信息 + asset_info = get_asset_info(asset) + # 获取该资产包含的角色 + role = [str(role.name) for role in self.user_perm.get('asset').get(asset).get('role')] + print '[%-3s] %-15s %-15s %-5s %-10s %s' % (index, asset.hostname, asset.ip, asset_info.get('port'), + role, asset.comment) + print + + def print_asset_group(self): + """ + 打印用户授权的资产组 + """ + user_asset_group_all = get_group_user_perm(self.user).get('asset_group', []) + color_print('[%-3s] %-20s %s' % ('ID', '组名', '备注'), 'title') + for asset_group in user_asset_group_all: + print '[%-3s] %-15s %s' % (asset_group.id, asset_group.name, asset_group.comment) + print + + def exec_cmd(self): + """ + 批量执行命令 + """ while True: - cmd = raw_input('\033[1;32mCmd(s): \033[0m') - if cmd in ['q', 'Q']: + if not self.user_perm: + self.user_perm = get_group_user_perm(self.user) + + roles = self.user_perm.get('role').keys() + if len(roles) > 1: # 授权角色数大于1 + color_print('[%-2s] %-15s' % ('ID', '系统用户'), 'info') + role_check = dict(zip(range(len(roles)), roles)) + + for i, r in role_check.items(): + print '[%-2s] %-15s' % (i, r.name) + print + print "请输入运行命令所关联系统用户的ID, q退出" + + try: + role_id = raw_input("\033[1;32mRole>:\033[0m ").strip() + if role_id == 'q': + break + except (IndexError, ValueError): + color_print('错误输入') + else: + role = role_check[int(role_id)] + elif len(roles) == 1: # 授权角色数为1 + role = roles[0] + assets = list(self.user_perm.get('role', {}).get(role).get('asset')) # 获取该用户,角色授权主机 + print "授权包含该系统用户的所有主机" + for asset in assets: + print ' %s' % asset.hostname + print + print "请输入主机名或ansile支持的pattern, 多个主机:分隔, q退出" + pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip() + if pattern == 'q': break - exec_log_dir = os.path.join(LOG_DIR, 'exec_cmds') - if not os.path.isdir(exec_log_dir): - os.mkdir(exec_log_dir) - os.chmod(exec_log_dir, 0777) - filename = "%s/%s.log" % (exec_log_dir, time.strftime('%Y%m%d')) - f = open(filename, 'a') - f.write("DateTime: %s User: %s Host: %s Cmds: %s\n" % - (time.strftime('%Y/%m/%d %H:%M:%S'), username, hosts, cmd)) - multi_remote_exec_cmd(hosts, username, cmd) + else: + res = gen_resource({'user': self.user, 'asset': assets, 'role': role}, perm=self.user_perm) + runner = MyRunner(res) + asset_name_str = '' + print "匹配主机:" + for inv in runner.inventory.get_hosts(pattern=pattern): + print ' %s' % inv.name + asset_name_str += '%s ' % inv.name + print + + while True: + print "请输入执行的命令, 按q退出" + command = raw_input("\033[1;32mCmds>:\033[0m ").strip() + if command == 'q': + break + runner.run('shell', command, pattern=pattern) + ExecLog(host=asset_name_str, user=self.user.username, cmd=command, remote_ip=remote_ip, + result=runner.results).save() + for k, v in runner.results.items(): + if k == 'ok': + for host, output in v.items(): + color_print("%s => %s" % (host, 'Ok'), 'green') + print output + print + else: + for host, output in v.items(): + color_print("%s => %s" % (host, k), 'red') + color_print(output, 'red') + print + print "~o~ Task finished ~o~" + print + + def upload(self): + while True: + if not self.user_perm: + self.user_perm = get_group_user_perm(self.user) + try: + print "进入批量上传模式" + print "请输入主机名或ansile支持的pattern, 多个主机:分隔 q退出" + pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip() + if pattern == 'q': + break + else: + assets = self.user_perm.get('asset').keys() + res = gen_resource({'user': self.user, 'asset': assets}, perm=self.user_perm) + runner = MyRunner(res) + asset_name_str = '' + print "匹配主机:" + for inv in runner.inventory.get_hosts(pattern=pattern): + print inv.name + asset_name_str += '%s ' % inv.name + + if not asset_name_str: + color_print('没有匹配主机') + continue + tmp_dir = get_tmp_dir() + logger.debug('Upload tmp dir: %s' % tmp_dir) + os.chdir(tmp_dir) + bash('rz') + filename_str = ' '.join(os.listdir(tmp_dir)) + if not filename_str: + color_print("上传文件为空") + continue + logger.debug('上传文件: %s' % filename_str) + + runner = MyRunner(res) + runner.run('copy', module_args='src=%s dest=%s directory_mode' + % (tmp_dir, tmp_dir), pattern=pattern) + ret = runner.results + FileLog(user=self.user.name, host=asset_name_str, filename=filename_str, + remote_ip=remote_ip, type='upload', result=ret).save() + logger.debug('Upload file: %s' % ret) + if ret.get('failed'): + error = '上传目录: %s \n上传失败: [ %s ] \n上传成功 [ %s ]' % (tmp_dir, + ', '.join(ret.get('failed').keys()), + ', '.join(ret.get('ok').keys())) + color_print(error) + else: + msg = '上传目录: %s \n传送成功 [ %s ]' % (tmp_dir, ', '.join(ret.get('ok').keys())) + color_print(msg, 'green') + print + + except IndexError: + pass + + def download(self): + while True: + if not self.user_perm: + self.user_perm = get_group_user_perm(self.user) + try: + print "进入批量下载模式" + print "请输入主机名或ansile支持的pattern, 多个主机:分隔,q退出" + pattern = raw_input("\033[1;32mPattern>:\033[0m ").strip() + if pattern == 'q': + break + else: + assets = self.user_perm.get('asset').keys() + res = gen_resource({'user': self.user, 'asset': assets}, perm=self.user_perm) + runner = MyRunner(res) + asset_name_str = '' + print "匹配用户:\n" + for inv in runner.inventory.get_hosts(pattern=pattern): + asset_name_str += '%s ' % inv.name + print ' %s' % inv.name + if not asset_name_str: + color_print('没有匹配主机') + continue + print + while True: + tmp_dir = get_tmp_dir() + logger.debug('Download tmp dir: %s' % tmp_dir) + print "请输入文件路径(不支持目录)" + file_path = raw_input("\033[1;32mPath>:\033[0m ").strip() + if file_path == 'q': + break + + if not file_path: + print "文件路径为空" + continue + + runner.run('fetch', module_args='src=%s dest=%s' % (file_path, tmp_dir), pattern=pattern) + ret = runner.results + FileLog(user=self.user.name, host=asset_name_str, filename=file_path, type='download', + remote_ip=remote_ip, result=ret).save() + logger.debug('Download file result: %s' % ret) + os.chdir('/tmp') + tmp_dir_name = os.path.basename(tmp_dir) + if not os.listdir(tmp_dir): + color_print('下载全部失败') + continue + bash('tar czf %s.tar.gz %s && sz %s.tar.gz' % (tmp_dir, tmp_dir_name, tmp_dir)) + + if ret.get('failed'): + error = '文件名称: %s \n下载失败: [ %s ] \n下载成功 [ %s ]' % \ + ('%s.tar.gz' % tmp_dir_name, ', '.join(ret.get('failed').keys()), ', '.join(ret.get('ok').keys())) + color_print(error) + else: + msg = '文件名称: %s \n下载成功 [ %s ]' % ('%s.tar.gz' % tmp_dir_name, ', '.join(ret.get('ok').keys())) + color_print(msg, 'green') + print + except IndexError: + pass -if __name__ == '__main__': - print_prompt() +def main(): + """ + he he + 主程序 + """ + if not login_user: # 判断用户是否存在 + color_print(u'没有该用户,或许你是以root运行的 No that user.', exits=True) + gid_pattern = re.compile(r'^g\d+$') + nav = Nav(login_user) + nav.print_nav() + try: while True: try: - option = raw_input("\033[1;32mOpt or IP>:\033[0m ") + option = raw_input("\033[1;32mOpt or ID>:\033[0m ").strip() except EOFError: - print + nav.print_nav() continue except KeyboardInterrupt: sys.exit(0) - if option in ['P', 'p']: - print_user_host(LOGIN_NAME) + if option in ['P', 'p', '\n', '']: + nav.search() continue + if option.startswith('/') or gid_pattern.match(option): + nav.search(option.lstrip('/')) elif option in ['G', 'g']: - print_user_hostgroup(LOGIN_NAME) - continue - elif gid_pattern.match(option): - gid = option[1:].strip() - print_user_hostgroup_host(LOGIN_NAME, gid) + nav.print_asset_group() continue elif option in ['E', 'e']: - exec_cmd_servers(LOGIN_NAME) + nav.exec_cmd() + continue + elif option in ['U', 'u']: + nav.upload() + elif option in ['D', 'd']: + nav.download() + elif option in ['H', 'h']: + nav.print_nav() elif option in ['Q', 'q', 'exit']: sys.exit() else: try: - verify_connect(LOGIN_NAME, option) + asset = nav.search_result[int(option)] + roles = nav.user_perm.get('asset').get(asset).get('role') + if len(roles) > 1: + role_check = dict(zip(range(len(roles)), roles)) + print "\033[32m[ID] 系统用户\033[0m" + for index, role in role_check.items(): + print "[%-2s] %s" % (index, role.name) + print + print "授权系统用户超过1个,请输入ID, q退出" + try: + role_index = raw_input("\033[1;32mID>:\033[0m ").strip() + if role_index == 'q': + continue + else: + role = role_check[int(role_index)] + except IndexError: + color_print('请输入正确ID', 'red') + continue + elif len(roles) == 1: + role = list(roles)[0] + else: + color_print('没有映射用户', 'red') + continue + ssh_tty = SshTty(login_user, asset, role) + ssh_tty.connect() + except (KeyError, ValueError): + color_print('请输入正确ID', 'red') except ServerError, e: color_print(e, 'red') except IndexError: pass + +if __name__ == '__main__': + main() diff --git a/docs/AddUserAsset.py b/docs/AddUserAsset.py deleted file mode 100644 index f8a5ed63c..000000000 --- a/docs/AddUserAsset.py +++ /dev/null @@ -1,141 +0,0 @@ -#coding:utf-8 -import django -import os -import sys -import random -import datetime - -sys.path.append('../') -os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' -#django.setup() - - -from juser.views import db_add_user, md5_crypt, CRYPTOR, db_add_group -from jasset.models import Asset, IDC, BisGroup -from juser.models import UserGroup, DEPT, User -from jperm.models import CmdGroup -from jlog.models import Log - - -def install(): - IDC.objects.create(name='ALL', comment='ALL') - IDC.objects.create(name='默认', comment='默认') - DEPT.objects.create(name="默认", comment="默认部门") - DEPT.objects.create(name="超管部", comment="超级管理员部门") - dept = DEPT.objects.get(name='超管部') - dept2 = DEPT.objects.get(name='默认') - UserGroup.objects.create(name='ALL', dept=dept, comment='ALL') - UserGroup.objects.create(name='默认', dept=dept, comment='默认') - - BisGroup.objects.create(name='ALL', dept=dept, comment='ALL') - BisGroup.objects.create(name='默认', dept=dept, comment='默认') - - User(id=5000, username="admin", password=md5_crypt('admin'), - name='admin', email='admin@jumpserver.org', role='SU', is_active=True, dept=dept).save() - User(id=5001, username="group_admin", password=md5_crypt('group_admin'), - name='group_admin', email='group_admin@jumpserver.org', role='DA', is_active=True, dept=dept2).save() - - -def test_add_idc(): - for i in range(1, 20): - name = 'IDC' + str(i) - IDC.objects.create(name=name, comment='') - print 'Add: %s' % name - - -def test_add_dept(): - for i in range(1, 100): - name = 'DEPT' + str(i) - print "Add: %s" % name - DEPT.objects.create(name=name, comment=name) - - -def test_add_group(): - dept_all = DEPT.objects.all() - for i in range(1, 100): - name = 'UserGroup' + str(i) - UserGroup.objects.create(name=name, dept=random.choice(dept_all), comment=name) - print 'Add: %s' % name - - -def test_add_cmd_group(): - for i in range(1, 20): - name = 'CMD' + str(i) - cmd = '/sbin/ping%s, /sbin/ifconfig/' % str(i) - CmdGroup.objects.create(name=name, cmd=cmd, comment=name) - print 'Add: %s' % name - - -def test_add_user(): - for i in range(1, 500): - username = "test" + str(i) - dept_all = DEPT.objects.all() - group_all = UserGroup.objects.all() - group_all_id = [group.id for group in group_all] - db_add_user(username=username, - password=md5_crypt(username), - dept=random.choice(dept_all), - name=username, email='%s@jumpserver.org' % username, - groups=[random.choice(group_all_id) for i in range(1, 4)], role='CU', - ssh_key_pwd=CRYPTOR.encrypt(username), - ldap_pwd=CRYPTOR.encrypt(username), - is_active=True, - date_joined=datetime.datetime.now()) - print "Add: %s" % username - - -def test_add_asset_group(): - dept = DEPT.objects.get(name='默认') - for i in range(1, 20): - name = 'AssetGroup' + str(i) - group = BisGroup(name=name, dept=dept, comment=name) - group.save() - print 'Add: %s' % name - - -def test_add_asset(): - idc_all = IDC.objects.all() - test_idc = random.choice(idc_all) - bis_group_all = BisGroup.objects.all() - dept_all = DEPT.objects.all() - for i in range(1, 500): - ip = '192.168.5.' + str(i) - asset = Asset(ip=ip, port=22, login_type='L', idc=test_idc, is_active=True, comment='test') - asset.save() - asset.bis_group = [random.choice(bis_group_all) for i in range(2)] - asset.dept = [random.choice(dept_all) for i in range(2)] - print "Add: %s" % ip - - -def test_add_log(): - li_date = [] - today = datetime.date.today() - oneday = datetime.timedelta(days=1) - for i in range(0, 7): - today = today-oneday - li_date.append(today) - user_list = ['马云', '马化腾', '丁磊', '周鸿祎', '雷军', '柳传志', '陈天桥', '李彦宏', '李开复', '罗永浩'] - for i in range(1, 1000): - user = random.choice(user_list) - ip = random.randint(1, 20) - start_time = random.choice(li_date) - end_time = datetime.datetime.now() - log_path = '/var/log/jumpserver/test.log' - host = '192.168.1.' + str(ip) - Log.objects.create(user=user, host=host, remote_ip='8.8.8.8', dept_name='运维部', log_path=log_path, pid=168, start_time=start_time, - is_finished=1, log_finished=1, end_time=end_time) - - -if __name__ == '__main__': - #install() - #test_add_dept() - #test_add_group() - #test_add_user() - #test_add_idc() - #test_add_asset_group() - test_add_asset() - #test_add_log() - - - - diff --git a/docs/__init__.py b/docs/__init__.py deleted file mode 100644 index bfd53d39f..000000000 --- a/docs/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__author__ = 'Hudie' diff --git a/docs/developer_doc.txt b/docs/developer_doc.txt deleted file mode 100644 index d24cacdcd..000000000 --- a/docs/developer_doc.txt +++ /dev/null @@ -1,36 +0,0 @@ -# coding: utf8 - -Jumpserver开发者文档 - -开发规范: - 1. 遵守PE8规范 1) 命名规范 2) 导入模块规范 3) 空行规范 4) 长度规范 - 2. 缩进统一4个空格 - 3. 变量命名明了易懂多个单词下划线隔开 - 4. 注释到位 - - -框架说明: - 1. 项目名称 Jumpserver - 2. APP: - juser 用户管理 - jasset 资产管理(设备管理) - jpermission 授权管理 - jlog 日志管理 - 3. connect.py 用户登录入口程序 - 4. logs 日志保存目录 - 5. jumpserver.conf 配置文件 - 6. docs 文档目录 - 7. static 静态文件目录 - 8. templates 模板目录 - - -connect.py逻辑说明: - 用户登录系统,运行该脚本,p调用get_user_host函数查看有权限的服务器ip - 输入部分IP,verify_connect匹配该部分ip,如果是匹配到多个,就显示ip - 匹配到0了就显示没有权限或者主机, - 匹配到1个则继续 - 查询该服务器是否支持ldap 如果是,获得ldap用户密码登陆 - 如果否,查询授权表,查看该服务器授权的角色,并返回对应账号密码,登陆 - connect函数是登陆函数,采用paramiko 使用channel登陆,posix_shell 来完成交互,并记录日志 - signal模块来完成窗口改变导致的tty大小随之改变 - PyCrypt是对称加密类 \ No newline at end of file diff --git a/docs/install.py b/docs/install.py deleted file mode 100644 index a032bf327..000000000 --- a/docs/install.py +++ /dev/null @@ -1,135 +0,0 @@ -#coding:utf-8 -import django -import os -import sys -import random -import datetime - -sys.path.append('../') -os.environ['DJANGO_SETTINGS_MODULE'] = 'jumpserver.settings' -#django.setup() - - -from juser.views import db_add_user, md5_crypt, CRYPTOR, db_add_group -from jasset.models import Asset, IDC, BisGroup -from juser.models import UserGroup, DEPT, User -from jasset.views import jasset_group_add -from jperm.models import CmdGroup -from jlog.models import Log - - -def install(): - IDC.objects.create(name='ALL', comment='ALL') - IDC.objects.create(name='默认', comment='默认') - DEPT.objects.create(name="默认", comment="默认部门") - DEPT.objects.create(name="超管部", comment="超级管理员部门") - dept = DEPT.objects.get(name='超管部') - dept2 = DEPT.objects.get(name='默认') - UserGroup.objects.create(name='ALL', dept=dept, comment='ALL') - UserGroup.objects.create(name='默认', dept=dept, comment='默认') - - BisGroup.objects.create(name='ALL', dept=dept, comment='ALL') - BisGroup.objects.create(name='默认', dept=dept, comment='默认') - - User(id=5000, username="admin", password=md5_crypt('admin'), - name='admin', email='admin@jumpserver.org', role='SU', is_active=True, dept=dept).save() - User(id=5001, username="group_admin", password=md5_crypt('group_admin'), - name='group_admin', email='group_admin@jumpserver.org', role='DA', is_active=True, dept=dept2).save() - - -def test_add_idc(): - for i in range(1, 20): - name = 'IDC' + str(i) - IDC.objects.create(name=name, comment='') - print 'Add: %s' % name - - -def test_add_dept(): - for i in range(1, 100): - name = 'DEPT' + str(i) - print "Add: %s" % name - DEPT.objects.create(name=name, comment=name) - - -def test_add_group(): - dept_all = DEPT.objects.all() - for i in range(1, 100): - name = 'UserGroup' + str(i) - UserGroup.objects.create(name=name, dept=random.choice(dept_all), comment=name) - print 'Add: %s' % name - - -def test_add_cmd_group(): - for i in range(1, 20): - name = 'CMD' + str(i) - cmd = '/sbin/ping%s, /sbin/ifconfig/' % str(i) - CmdGroup.objects.create(name=name, cmd=cmd, comment=name) - print 'Add: %s' % name - - -def test_add_user(): - for i in range(1, 500): - username = "test" + str(i) - dept_all = DEPT.objects.all() - group_all = UserGroup.objects.all() - group_all_id = [group.id for group in group_all] - db_add_user(username=username, - password=md5_crypt(username), - dept=random.choice(dept_all), - name=username, email='%s@jumpserver.org' % username, - groups=[random.choice(group_all_id) for i in range(1, 4)], role='CU', - ssh_key_pwd=CRYPTOR.encrypt(username), - ldap_pwd=CRYPTOR.encrypt(username), - is_active=True, - date_joined=datetime.datetime.now()) - print "Add: %s" % username - - -def test_add_asset_group(): - dept = DEPT.objects.get(name='默认') - for i in range(1, 20): - name = 'AssetGroup' + str(i) - group = BisGroup(name=name, dept=dept, comment=name) - group.save() - print 'Add: %s' % name - - -def test_add_asset(): - idc_all = IDC.objects.all() - test_idc = random.choice(idc_all) - bis_group_all = BisGroup.objects.all() - dept_all = DEPT.objects.all() - for i in range(1, 500): - ip = '192.168.1.' + str(i) - asset = Asset(ip=ip, port=22, login_type='L', idc=test_idc, is_active=True, comment='test') - asset.save() - asset.bis_group = [random.choice(bis_group_all) for i in range(2)] - asset.dept = [random.choice(dept_all) for i in range(2)] - print "Add: %s" % ip - - -def test_add_log(): - li_date = [] - today = datetime.date.today() - oneday = datetime.timedelta(days=1) - for i in range(0, 7): - today = today-oneday - li_date.append(today) - user_list = ['马云', '马化腾', '丁磊', '周鸿祎', '雷军', '柳传志', '陈天桥', '李彦宏', '李开复', '罗永浩'] - for i in range(1, 1000): - user = random.choice(user_list) - ip = random.randint(1, 20) - start_time = random.choice(li_date) - end_time = datetime.datetime.now() - log_path = '/var/log/jumpserver/test.log' - host = '192.168.1.' + str(ip) - Log.objects.create(user=user, host=host, log_path=log_path, pid=168, start_time=start_time, - is_finished=1, log_finished=1, end_time=end_time) - - -if __name__ == '__main__': - install() - - - - diff --git a/docs/requirements.txt b/docs/requirements.txt deleted file mode 100644 index d0d83cafc..000000000 --- a/docs/requirements.txt +++ /dev/null @@ -1,9 +0,0 @@ -sphinx-me==0.3 -django==1.6 -python-ldap==2.4.19 -pycrypto==2.6.1 -paramiko==1.15.2 -ecdsa==0.13 -MySQL-python==1.2.5 -django-uuidfield==0.5.0 -psutil==2.2.1 \ No newline at end of file diff --git a/docs/zzjumpserver.sh b/docs/zzjumpserver.sh deleted file mode 100644 index 98598ff18..000000000 --- a/docs/zzjumpserver.sh +++ /dev/null @@ -1,13 +0,0 @@ -#!/bin/bash - -if [ "$USER" == "admin" ] || [ "$USER" == "root" ] || [ "$USER" == "" ];then - echo "" -else - python /opt/jumpserver/connect.py - if [ $USER == 'guanghongwei' ];then - echo - else - exit 3 - echo - fi -fi diff --git a/jasset/models.py b/jasset/models.py index 416c4ddfa..2851b7d56 100644 --- a/jasset/models.py +++ b/jasset/models.py @@ -1,54 +1,111 @@ +# coding: utf-8 + import datetime from django.db import models -from juser.models import User, UserGroup, DEPT +from juser.models import User, UserGroup + +ASSET_ENV = ( + (1, U'生产环境'), + (2, U'测试环境') + ) + +ASSET_STATUS = ( + (1, u"已使用"), + (2, u"未使用"), + (3, u"报废") + ) + +ASSET_TYPE = ( + (1, u"物理机"), + (2, u"虚拟机"), + (3, u"交换机"), + (4, u"路由器"), + (5, u"防火墙"), + (6, u"Docker"), + (7, u"其他") + ) -class IDC(models.Model): - name = models.CharField(max_length=40, unique=True) - comment = models.CharField(max_length=80, blank=True, null=True) - - def __unicode__(self): - return self.name - - -class BisGroup(models.Model): +class AssetGroup(models.Model): GROUP_TYPE = ( ('P', 'PRIVATE'), ('A', 'ASSET'), ) name = models.CharField(max_length=80, unique=True) - dept = models.ForeignKey(DEPT) comment = models.CharField(max_length=160, blank=True, null=True) def __unicode__(self): return self.name +class IDC(models.Model): + name = models.CharField(max_length=32, verbose_name=u'机房名称') + bandwidth = models.CharField(max_length=32, blank=True, null=True, default='', verbose_name=u'机房带宽') + linkman = models.CharField(max_length=16, blank=True, null=True, default='', verbose_name=u'联系人') + phone = models.CharField(max_length=32, blank=True, null=True, default='', verbose_name=u'联系电话') + address = models.CharField(max_length=128, blank=True, null=True, default='', verbose_name=u"机房地址") + network = models.TextField(blank=True, null=True, default='', verbose_name=u"IP地址段") + date_added = models.DateField(auto_now=True, null=True) + operator = models.CharField(max_length=32, blank=True, default='', null=True, verbose_name=u"运营商") + comment = models.CharField(max_length=128, blank=True, default='', null=True, verbose_name=u"备注") + + def __unicode__(self): + return self.name + + class Meta: + verbose_name = u"IDC机房" + verbose_name_plural = verbose_name + + class Asset(models.Model): - LOGIN_TYPE_CHOICES = ( - ('L', 'LDAP'), - ('M', 'MAP'), - ) - ip = models.IPAddressField(unique=True) - port = models.IntegerField(max_length=6) - idc = models.ForeignKey(IDC) - bis_group = models.ManyToManyField(BisGroup) - dept = models.ManyToManyField(DEPT) - login_type = models.CharField(max_length=1, choices=LOGIN_TYPE_CHOICES, default='L') - username = models.CharField(max_length=20, blank=True, null=True) - password = models.CharField(max_length=80, blank=True, null=True) - date_added = models.DateTimeField(auto_now=True, default=datetime.datetime.now(), null=True) - is_active = models.BooleanField(default=True) - comment = models.CharField(max_length=100, blank=True, null=True) + """ + asset modle + """ + ip = models.CharField(max_length=32, blank=True, null=True, verbose_name=u"主机IP") + other_ip = models.CharField(max_length=255, blank=True, null=True, verbose_name=u"其他IP") + hostname = models.CharField(unique=True, max_length=128, verbose_name=u"主机名") + port = models.IntegerField(blank=True, null=True, verbose_name=u"端口号") + group = models.ManyToManyField(AssetGroup, blank=True, verbose_name=u"所属主机组") + username = models.CharField(max_length=16, blank=True, null=True, verbose_name=u"管理用户名") + password = models.CharField(max_length=64, blank=True, null=True, verbose_name=u"密码") + use_default_auth = models.BooleanField(default=True, verbose_name=u"使用默认管理账号") + idc = models.ForeignKey(IDC, blank=True, null=True, on_delete=models.SET_NULL, verbose_name=u'机房') + mac = models.CharField(max_length=20, blank=True, null=True, verbose_name=u"MAC地址") + remote_ip = models.CharField(max_length=16, blank=True, null=True, verbose_name=u'远控卡IP') + brand = models.CharField(max_length=64, blank=True, null=True, verbose_name=u'硬件厂商型号') + cpu = models.CharField(max_length=64, blank=True, null=True, verbose_name=u'CPU') + memory = models.CharField(max_length=128, blank=True, null=True, verbose_name=u'内存') + disk = models.CharField(max_length=128, blank=True, null=True, verbose_name=u'硬盘') + system_type = models.CharField(max_length=32, blank=True, null=True, verbose_name=u"系统类型") + system_version = models.CharField(max_length=8, blank=True, null=True, verbose_name=u"系统版本号") + system_arch = models.CharField(max_length=16, blank=True, null=True, verbose_name=u"系统平台") + cabinet = models.CharField(max_length=32, blank=True, null=True, verbose_name=u'机柜号') + position = models.IntegerField(blank=True, null=True, verbose_name=u'机器位置') + number = models.CharField(max_length=32, blank=True, null=True, verbose_name=u'资产编号') + status = models.IntegerField(choices=ASSET_STATUS, blank=True, null=True, default=1, verbose_name=u"机器状态") + asset_type = models.IntegerField(choices=ASSET_TYPE, blank=True, null=True, verbose_name=u"主机类型") + env = models.IntegerField(choices=ASSET_ENV, blank=True, null=True, verbose_name=u"运行环境") + sn = models.CharField(max_length=128, blank=True, null=True, verbose_name=u"SN编号") + date_added = models.DateTimeField(auto_now=True, null=True) + is_active = models.BooleanField(default=True, verbose_name=u"是否激活") + comment = models.CharField(max_length=128, blank=True, null=True, verbose_name=u"备注") def __unicode__(self): return self.ip +class AssetRecord(models.Model): + asset = models.ForeignKey(Asset) + username = models.CharField(max_length=30, null=True) + alert_time = models.DateTimeField(auto_now_add=True) + content = models.TextField(null=True, blank=True) + comment = models.TextField(null=True, blank=True) + + class AssetAlias(models.Model): user = models.ForeignKey(User) - host = models.ForeignKey(Asset) + asset = models.ForeignKey(Asset) alias = models.CharField(max_length=100, blank=True, null=True) def __unicode__(self): - return self.comment \ No newline at end of file + return self.alias diff --git a/jasset/urls.py b/jasset/urls.py index da52529b6..7daa9be20 100644 --- a/jasset/urls.py +++ b/jasset/urls.py @@ -3,26 +3,22 @@ from django.conf.urls import patterns, include, url from jasset.views import * urlpatterns = patterns('', - url(r'^host_add/$', host_add), - url(r"^host_add_multi/$", host_add_batch), - url(r'^host_list/$', host_list), - url(r'^search/$', host_search), - url(r"^host_detail/$", host_detail), - url(r"^dept_host_ajax/$", dept_host_ajax), - url(r"^show_all_ajax/$", show_all_ajax), - url(r'^idc_add/$', idc_add), - url(r'^idc_list/$', idc_list), - url(r'^idc_edit/$', idc_edit), - url(r'^idc_detail/$', idc_detail), - url(r'^idc_del/$', idc_del), - url(r'^group_add/$', group_add), - url(r'^group_edit/$', group_edit), - url(r'^group_list/$', group_list), - url(r'^group_detail/$', group_detail), - url(r'^group_del_host/$', group_del_host), - url(r'^group_del/$', group_del), - url(r'^host_del/(\w+)/$', host_del), - url(r'^host_edit/$', view_splitter, {'su': host_edit, 'adm': host_edit_adm}), - url(r'^host_edit/batch/$', host_edit_batch), - url(r'^host_edit_common/batch/$', host_edit_common_batch), + url(r'^asset/add/$', asset_add, name='asset_add'), + url(r"^asset/add_batch/$", asset_add_batch, name='asset_add_batch'), + url(r'^asset/list/$', asset_list, name='asset_list'), + url(r'^asset/del/$', asset_del, name='asset_del'), + url(r"^asset/detail/$", asset_detail, name='asset_detail'), + url(r'^asset/edit/$', asset_edit, name='asset_edit'), + url(r'^asset/edit_batch/$', asset_edit_batch, name='asset_edit_batch'), + url(r'^asset/update/$', asset_update, name='asset_update'), + url(r'^asset/update_batch/$', asset_update_batch, name='asset_update_batch'), + url(r'^asset/upload/$', asset_upload, name='asset_upload'), + url(r'^group/del/$', group_del, name='asset_group_del'), + url(r'^group/add/$', group_add, name='asset_group_add'), + url(r'^group/list/$', group_list, name='asset_group_list'), + url(r'^group/edit/$', group_edit, name='asset_group_edit'), + url(r'^idc/add/$', idc_add, name='idc_add'), + url(r'^idc/list/$', idc_list, name='idc_list'), + url(r'^idc/edit/$', idc_edit, name='idc_edit'), + url(r'^idc/del/$', idc_del, name='idc_del'), ) \ No newline at end of file diff --git a/jasset/views.py b/jasset/views.py index 22c72f8e5..a3eef0801 100644 --- a/jasset/views.py +++ b/jasset/views.py @@ -1,589 +1,519 @@ # coding:utf-8 -import ast - from django.db.models import Q -from django.template import RequestContext -from django.shortcuts import get_object_or_404 - -from jperm.models import Perm +from jasset.asset_api import * from jumpserver.api import * - -cryptor = PyCrypt(KEY) +from jumpserver.models import Setting +from jasset.forms import AssetForm, IdcForm +from jasset.models import Asset, IDC, AssetGroup, ASSET_TYPE, ASSET_STATUS +from jperm.perm_api import get_group_asset_perm, get_group_user_perm -class RaiseError(Exception): - pass - - -def my_render(template, data, request): - return render_to_response(template, data, context_instance=RequestContext(request)) - - -def get_host_groups(groups): - """ 获取主机所属的组类 """ - ret = [] - for group_id in groups: - group = BisGroup.objects.filter(id=group_id) - if group: - group = group[0] - ret.append(group) - group_all = get_object_or_404(BisGroup, name='ALL') - ret.append(group_all) - return ret - - -def get_host_depts(depts): - """ 获取主机所属的部门类 """ - ret = [] - for dept_id in depts: - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - ret.append(dept) - return ret - - -def db_host_insert(host_info, username='', password=''): - """ 添加主机时数据库操作函数 """ - ip, port, idc, jtype, group, dept, active, comment = host_info - idc = IDC.objects.filter(id=idc) - if idc: - idc = idc[0] - if jtype == 'M': - password = cryptor.encrypt(password) - a = Asset(ip=ip, port=port, - login_type=jtype, idc=idc, - is_active=int(active), - comment=comment, - username=username, - password=password) - else: - a = Asset(ip=ip, port=port, - login_type=jtype, idc=idc, - is_active=int(active), - comment=comment) - a.save() - - all_group = BisGroup.objects.get(name='ALL') - groups = get_host_groups(group) - groups.append(all_group) - - depts = get_host_depts(dept) - - a.bis_group = groups - a.dept = depts - a.save() - - -def db_host_update(host_info, username='', password=''): - """ 修改主机时数据库操作函数 """ - ip, port, idc, jtype, group, dept, active, comment, host = host_info - idc = IDC.objects.filter(id=idc) - if idc: - idc = idc[0] - groups = get_host_groups(group) - depts = get_host_depts(dept) - host.ip = ip - host.port = port - host.login_type = jtype - host.idc = idc - host.is_active = int(active) - host.comment = comment - - if jtype == 'M': - if password != host.password: - password = cryptor.encrypt(password) - host.password = password - host.username = username - host.password = password - host.save() - host.bis_group = groups - host.dept = depts - host.save() - - -def batch_host_edit(host_info, j_user='', j_password=''): - """ 批量修改主机函数 """ - j_id, j_ip, j_idc, j_port, j_type, j_group, j_dept, j_active, j_comment = host_info - groups, depts = [], [] - is_active = {u'是': '1', u'否': '2'} - login_types = {'LDAP': 'L', 'MAP': 'M'} - a = Asset.objects.get(id=j_id) - if '...' in j_group[0].split(): - groups = a.bis_group.all() - else: - for group in j_group[0].split(): - c = BisGroup.objects.get(name=group.strip()) - groups.append(c) - - if '...' in j_dept[0].split(): - depts = a.dept.all() - else: - for d in j_dept[0].split(): - p = DEPT.objects.get(name=d.strip()) - depts.append(p) - - j_type = login_types[j_type] - j_idc = IDC.objects.get(name=j_idc) - if j_type == 'M': - if a.password != j_password: - j_password = cryptor.decrypt(j_password) - a.ip = j_ip - a.port = j_port - a.login_type = j_type - a.idc = j_idc - a.is_active = j_active - a.comment = j_comment - a.username = j_user - a.password = j_password - else: - a.ip = j_ip - a.port = j_port - a.idc = j_idc - a.login_type = j_type - a.is_active = is_active[j_active] - a.comment = j_comment - a.save() - a.bis_group = groups - a.dept = depts - a.save() - - -def db_host_delete(request, host_id): - """ 删除主机操作 """ - if is_group_admin(request) and not validate(request, asset=[host_id]): - return httperror(request, '删除失败, 您无权删除!') - - asset = Asset.objects.filter(id=host_id) - if asset: - asset.delete() - else: - return httperror(request, '删除失败, 没有此主机!') - - -def db_idc_delete(request, idc_id): - """ 删除IDC操作 """ - if idc_id == 1: - return httperror(request, '删除失败, 默认IDC不能删除!') - - default_idc = IDC.objects.get(id=1) - - idc = IDC.objects.filter(id=idc_id) - if idc: - idc_class = idc[0] - idc_class.asset_set.update(idc=default_idc) - idc.delete() - else: - return httperror(request, '删除失败, 没有这个IDC!') - - -@require_admin -def host_add(request): - """ 添加主机 """ - header_title, path1, path2 = u'添加主机', u'资产管理', u'添加主机' - login_types = {'L': 'LDAP', 'M': 'MAP'} - eidc = IDC.objects.exclude(name='ALL') - if is_super_user(request): - edept = DEPT.objects.all() - egroup = BisGroup.objects.exclude(name='ALL') - elif is_group_admin(request): - dept = get_session_user_info(request)[5] - egroup = dept.bisgroup_set.all() +@require_role('admin') +def group_add(request): + """ + Group add view + 添加资产组 + """ + header_title, path1, path2 = u'添加资产组', u'资产管理', u'添加资产组' + asset_all = Asset.objects.all() if request.method == 'POST': - j_ip = request.POST.get('j_ip') - j_idc = request.POST.get('j_idc') - j_port = request.POST.get('j_port') - j_type = request.POST.get('j_type') - j_group = request.POST.getlist('j_group') - j_active = request.POST.get('j_active') - j_comment = request.POST.get('j_comment') + name = request.POST.get('name', '') + asset_select = request.POST.getlist('asset_select', []) + comment = request.POST.get('comment', '') - if is_super_user(request): - j_dept = request.POST.getlist('j_dept') - host_info = [j_ip, j_port, j_idc, j_type, j_group, j_dept, j_active, j_comment] - elif is_group_admin(request): - j_dept = request.POST.get('j_dept') - host_info = [j_ip, j_port, j_idc, j_type, j_group, [j_dept], j_active, j_comment] + try: + if not name: + emg = u'组名不能为空' + raise ServerError(emg) - if is_group_admin(request) and not validate(request, asset_group=j_group, edept=[j_dept]): - return httperror(request, u'添加失败,您无权操作!') + asset_group_test = get_object(AssetGroup, name=name) + if asset_group_test: + emg = u"该组名 %s 已存在" % name + raise ServerError(emg) + + except ServerError: + pass - if Asset.objects.filter(ip=str(j_ip)): - emg = u'该IP %s 已存在!' % j_ip - return my_render('jasset/host_add.html', locals(), request) - if j_type == 'M': - j_user = request.POST.get('j_user') - j_password = request.POST.get('j_password', '') - db_host_insert(host_info, j_user, j_password) else: - db_host_insert(host_info) - smg = u'主机 %s 添加成功' % j_ip + db_add_group(name=name, comment=comment, asset_select=asset_select) + smg = u"主机组 %s 添加成功" % name - return my_render('jasset/host_add.html', locals(), request) + return my_render('jasset/group_add.html', locals(), request) -@require_admin -def host_add_batch(request): - """ 批量添加主机 """ - header_title, path1, path2 = u'批量添加主机', u'资产管理', u'批量添加主机' - login_types = {'LDAP': 'L', 'MAP': 'M'} - active_types = {'激活': 1, '禁用': 0} - dept_id = get_user_dept(request) +@require_role('admin') +def group_edit(request): + """ + Group edit view + 编辑资产组 + """ + header_title, path1, path2 = u'编辑主机组', u'资产管理', u'编辑主机组' + group_id = request.GET.get('id', '') + group = get_object(AssetGroup, id=group_id) + + asset_all = Asset.objects.all() + asset_select = Asset.objects.filter(group=group) + asset_no_select = [a for a in asset_all if a not in asset_select] + if request.method == 'POST': - multi_hosts = request.POST.get('j_multi').split('\n') - for host in multi_hosts: - if host == '': - break - j_ip, j_port, j_type, j_idc, j_groups, j_depts, j_active, j_comment = host.split() - j_active = active_types[str(j_active)] - j_group = ast.literal_eval(j_groups) - j_dept = ast.literal_eval(j_depts) + name = request.POST.get('name', '') + asset_select = request.POST.getlist('asset_select', []) + comment = request.POST.get('comment', '') - if j_type not in ['LDAP', 'MAP']: - return httperror(request, u'没有%s这种登录方式!' %j_type) + try: + if not name: + emg = u'组名不能为空' + raise ServerError(emg) - j_type = login_types[j_type] - idc = IDC.objects.filter(name=j_idc) - if idc: - j_idc = idc[0].id - else: - return httperror(request, '添加失败, 没有%s这个IDC' % j_idc) + if group.name != name: + asset_group_test = get_object(AssetGroup, name=name) + if asset_group_test: + emg = u"该组名 %s 已存在" % name + raise ServerError(emg) - group_ids, dept_ids = [], [] - for group_name in j_group: - group = BisGroup.objects.filter(name=group_name) - if group: - group_id = group[0].id - else: - return httperror(request, '添加失败, 没有%s这个主机组' % group_name) - group_ids.append(group_id) + except ServerError: + pass - for dept_name in j_dept: - dept = DEPT.objects.filter(name=dept_name) - if dept: - dept_id = dept[0].id - else: - return httperror(request, '添加失败, 没有%s这个部门' % dept_name) - dept_ids.append(dept_id) + else: + group.asset_set.clear() + db_update_group(id=group_id, name=name, comment=comment, asset_select=asset_select) + smg = u"主机组 %s 添加成功" % name - if is_group_admin(request) and not validate(request, asset_group=group_ids, edept=dept_ids): - return httperror(request, '添加失败, 没有%s这个主机组' % group_name) + return HttpResponseRedirect(reverse('asset_group_list')) - if Asset.objects.filter(ip=str(j_ip)): - return httperror(request, '添加失败, 改IP%s已存在' % j_ip) - - host_info = [j_ip, j_port, j_idc, j_type, group_ids, dept_ids, j_active, j_comment] - db_host_insert(host_info) - - smg = u'批量添加添加成功' - return my_render('jasset/host_add_multi.html', locals(), request) - - return my_render('jasset/host_add_multi.html', locals(), request) + return my_render('jasset/group_edit.html', locals(), request) -@require_admin -def host_edit_batch(request): - """ 批量修改主机 """ - if request.method == 'POST': - len_table = request.POST.get('len_table') - for i in range(int(len_table)): - j_id = "editable[" + str(i) + "][j_id]" - j_ip = "editable[" + str(i) + "][j_ip]" - j_port = "editable[" + str(i) + "][j_port]" - j_dept = "editable[" + str(i) + "][j_dept]" - j_idc = "editable[" + str(i) + "][j_idc]" - j_type = "editable[" + str(i) + "][j_type]" - j_group = "editable[" + str(i) + "][j_group]" - j_active = "editable[" + str(i) + "][j_active]" - j_comment = "editable[" + str(i) + "][j_comment]" - - j_id = request.POST.get(j_id).strip() - j_ip = request.POST.get(j_ip).strip() - j_port = request.POST.get(j_port).strip() - j_dept = request.POST.getlist(j_dept) - j_idc = request.POST.get(j_idc).strip() - j_type = request.POST.get(j_type).strip() - j_group = request.POST.getlist(j_group) - j_active = request.POST.get(j_active).strip() - j_comment = request.POST.get(j_comment).strip() - - host_info = [j_id, j_ip, j_idc, j_port, j_type, j_group, j_dept, j_active, j_comment] - batch_host_edit(host_info) - - return HttpResponseRedirect('/jasset/host_list/') - - -@require_login -def host_edit_common_batch(request): - """ 普通用户批量修改主机别名 """ - u = get_session_user_info(request)[2] - if request.method == 'POST': - len_table = request.POST.get('len_table') - for i in range(int(len_table)): - j_id = "editable[" + str(i) + "][j_id]" - j_alias = "editable[" + str(i) + "][j_alias]" - j_id = request.POST.get(j_id, '').strip() - j_alias = request.POST.get(j_alias, '').strip() - a = Asset.objects.get(id=j_id) - asset_alias = AssetAlias.objects.filter(user=u, host=a) - if asset_alias: - asset_alias = asset_alias[0] - asset_alias.alias = j_alias - asset_alias.save() - else: - AssetAlias.objects.create(user=u, host=a, alias=j_alias) - return my_render('jasset/host_list_common.html', locals(), request) - - -@require_login -def host_list(request): - """ 列出主机 """ - header_title, path1, path2 = u'查看主机', u'资产管理', u'查看主机' +@require_role('admin') +def group_list(request): + """ + list asset group + 列出资产组 + """ + header_title, path1, path2 = u'查看资产组', u'资产管理', u'查看资产组' keyword = request.GET.get('keyword', '') - dept_id = get_session_user_info(request)[3] - dept = DEPT.objects.get(id=dept_id) - did = request.GET.get('did', '') - gid = request.GET.get('gid', '') - sid = request.GET.get('sid', '') - user_id = get_session_user_info(request)[0] + asset_group_list = AssetGroup.objects.all() + group_id = request.GET.get('id') + if group_id: + asset_group_list = asset_group_list.filter(id=group_id) + if keyword: + asset_group_list = asset_group_list.filter(Q(name__contains=keyword) | Q(comment__contains=keyword)) - post_all = Asset.objects.all().order_by('ip') - post_keyword_all = Asset.objects.filter(Q(ip__contains=keyword) | - Q(idc__name__contains=keyword) | - Q(bis_group__name__contains=keyword) | - Q(comment__contains=keyword)).distinct().order_by('ip') - if did: - if is_common_user(request): - return httperror(request, u'您无权查看!') + asset_group_list, p, asset_groups, page_range, current_page, show_first, show_end = pages(asset_group_list, request) + return my_render('jasset/group_list.html', locals(), request) - if is_group_admin(request): - user, dept = get_session_user_dept(request) + +@require_role('admin') +def group_del(request): + """ + Group delete view + 删除主机组 + """ + group_ids = request.GET.get('id', '') + group_id_list = group_ids.split(',') + + for group_id in group_id_list: + AssetGroup.objects.filter(id=group_id).delete() + + return HttpResponse(u'删除成功') + + +@require_role('admin') +def asset_add(request): + """ + Asset add view + 添加资产 + """ + header_title, path1, path2 = u'添加资产', u'资产管理', u'添加资产' + asset_group_all = AssetGroup.objects.all() + af = AssetForm() + default_setting = get_object(Setting, name='default') + default_port = default_setting.field2 if default_setting else '' + if request.method == 'POST': + af_post = AssetForm(request.POST) + ip = request.POST.get('ip', '') + hostname = request.POST.get('hostname', '') + is_active = True if request.POST.get('is_active') == '1' else False + use_default_auth = request.POST.get('use_default_auth', '') + try: + if Asset.objects.filter(hostname=str(hostname)): + error = u'该主机名 %s 已存在!' % hostname + raise ServerError(error) + + except ServerError: + pass else: - dept = DEPT.objects.get(id=did) - posts = dept.asset_set.all() - return my_render('jasset/host_list_nop.html', locals(), request) + if af_post.is_valid(): + asset_save = af_post.save(commit=False) + if not use_default_auth: + password = request.POST.get('password', '') + password_encode = CRYPTOR.encrypt(password) + asset_save.password = password_encode + if not ip: + asset_save.ip = hostname + asset_save.is_active = True if is_active else False + asset_save.save() + af_post.save_m2m() - elif gid: - if is_common_user(request): - return httperror(request, u'您无权查看!') - - elif is_group_admin(request) and not validate(request, user_group=[gid]): - return httperror(request, u'您无权查看!') - - posts = [] - user_group = UserGroup.objects.filter(id=gid) - if user_group: - perms = Perm.objects.filter(user_group=user_group) - for perm in perms: - for post in perm.asset_group.asset_set.all(): - posts.append(post) - posts = list(set(posts)) - else: - return httperror(request, u'没有这个小组!') - return my_render('jasset/host_list_nop.html', locals(), request) - - elif sid: - if is_common_user(request): - return httperror(request, u'您无权查看!') - - elif is_group_admin(request) and not validate(request, user_group=[sid]): - return httperror(request, u'您无权查看!') - - posts, asset_groups = [], [] - user_group = UserGroup.objects.filter(id=int(sid)) - if user_group: - user_group = user_group[0] - for perm in user_group.sudoperm_set.all(): - asset_groups.extend(perm.asset_group.all()) - - for asset_group in asset_groups: - posts.extend(asset_group.asset_set.all()) - posts = list(set(posts)) - else: - return httperror(request, u'没有这个sudo授权!') - return my_render('jasset/host_list_nop.html', locals(), request) - - else: - if is_super_user(request): - if keyword: - posts = post_keyword_all + msg = u'主机 %s 添加成功' % hostname else: - posts = post_all - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return my_render('jasset/host_list.html', locals(), request) + esg = u'主机 %s 添加失败' % hostname - elif is_group_admin(request): - if keyword: - posts = post_keyword_all.filter(dept=dept) - else: - posts = post_all.filter(dept=dept) - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return my_render('jasset/host_list.html', locals(), request) - - elif is_common_user(request): - user_id, username = get_session_user_info(request)[0:2] - posts = user_perm_asset_api(username) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return my_render('jasset/host_list_common.html', locals(), request) + return my_render('jasset/asset_add.html', locals(), request) -@require_admin -def host_del(request, offset): - """ 删除主机 """ - if offset == 'multi': - len_list = request.POST.get("len_list") - for i in range(int(len_list)): - key = "id_list[" + str(i) + "]" - host_id = request.POST.get(key) - db_host_delete(request, host_id) - else: - db_host_delete(request, offset) - - return HttpResponseRedirect('/jasset/host_list/') +@require_role('admin') +def asset_add_batch(request): + header_title, path1, path2 = u'添加资产', u'资产管理', u'批量添加' + return my_render('jasset/asset_add_batch.html', locals(), request) -@require_super_user -def host_edit(request): - """ 修改主机 """ - header_title, path1, path2 = u'修改主机', u'资产管理', u'修改主机' - actives = {1: u'激活', 0: u'禁用'} - login_types = {'L': 'LDAP', 'M': 'MAP'} - eidc = IDC.objects.all() - egroup = BisGroup.objects.exclude(name='ALL') - edept = DEPT.objects.all() - host_id = request.GET.get('id', '') - post = Asset.objects.filter(id=int(host_id)) - if post: - post = post[0] - else: - return httperror(request, '没有此主机!') - - e_group = post.bis_group.all() - e_dept = post.dept.all() +@require_role('admin') +def asset_del(request): + """ + del a asset + 删除主机 + """ + asset_id = request.GET.get('id', '') + if asset_id: + Asset.objects.filter(id=asset_id).delete() if request.method == 'POST': - j_ip = request.POST.get('j_ip', '') - j_idc = request.POST.get('j_idc', '') - j_port = request.POST.get('j_port', '') - j_type = request.POST.get('j_type', '') - j_dept = request.POST.getlist('j_dept', '') - j_group = request.POST.getlist('j_group', '') - j_active = request.POST.get('j_active', '') - j_comment = request.POST.get('j_comment', '') + asset_batch = request.GET.get('arg', '') + asset_id_all = str(request.POST.get('asset_id_all', '')) - host_info = [j_ip, j_port, j_idc, j_type, j_group, j_dept, j_active, j_comment, post] - if j_type == 'M': - j_user = request.POST.get('j_user') - j_password = request.POST.get('j_password') - db_host_update(host_info, j_user, j_password) + if asset_batch: + for asset_id in asset_id_all.split(','): + asset = get_object(Asset, id=asset_id) + asset.delete() + + return HttpResponse(u'删除成功') + + +@require_role(role='super') +def asset_edit(request): + """ + edit a asset + 修改主机 + """ + header_title, path1, path2 = u'修改资产', u'资产管理', u'修改资产' + + asset_id = request.GET.get('id', '') + username = request.user.username + asset = get_object(Asset, id=asset_id) + if asset: + password_old = asset.password + # asset_old = copy_model_instance(asset) + af = AssetForm(instance=asset) + if request.method == 'POST': + af_post = AssetForm(request.POST, instance=asset) + ip = request.POST.get('ip', '') + hostname = request.POST.get('hostname', '') + password = request.POST.get('password', '') + is_active = True if request.POST.get('is_active') == '1' else False + use_default_auth = request.POST.get('use_default_auth', '') + try: + asset_test = get_object(Asset, hostname=hostname) + if asset_test and asset_id != unicode(asset_test.id): + emg = u'该主机名 %s 已存在!' % hostname + raise ServerError(emg) + except ServerError: + pass else: - db_host_update(host_info) + if af_post.is_valid(): + af_save = af_post.save(commit=False) + if use_default_auth: + af_save.username = '' + af_save.password = '' + af_save.port = None + else: + if password: + password_encode = CRYPTOR.encrypt(password) + af_save.password = password_encode + else: + af_save.password = password_old + af_save.is_active = True if is_active else False + af_save.save() + af_post.save_m2m() + # asset_new = get_object(Asset, id=asset_id) + # asset_diff_one(asset_old, asset_new) + info = asset_diff(af_post.__dict__.get('initial'), request.POST) + db_asset_alert(asset, username, info) - smg = u'主机 %s 修改成功' % j_ip - return HttpResponseRedirect('/jasset/host_detail/?id=%s' % host_id) + smg = u'主机 %s 修改成功' % ip + else: + emg = u'主机 %s 修改失败' % ip + return my_render('jasset/error.html', locals(), request) + return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id) - return my_render('jasset/host_edit.html', locals(), request) + return my_render('jasset/asset_edit.html', locals(), request) -@require_admin -def host_edit_adm(request): - """ 部门管理员修改主机 """ - header_title, path1, path2 = u'修改主机', u'资产管理', u'修改主机' - actives = {1: u'激活', 0: u'禁用'} - login_types = {'L': 'LDAP', 'M': 'MAP'} - eidc = IDC.objects.all() - dept = get_session_user_info(request)[5] - egroup = BisGroup.objects.exclude(name='ALL').filter(dept=dept) - host_id = request.GET.get('id', '') - post = Asset.objects.filter(id=int(host_id)) - if post: - post = post[0] +@require_role('user') +def asset_list(request): + """ + asset list view + """ + header_title, path1, path2 = u'查看资产', u'资产管理', u'查看资产' + username = request.user.username + user_perm = request.session['role_id'] + idc_all = IDC.objects.filter() + asset_group_all = AssetGroup.objects.all() + asset_types = ASSET_TYPE + asset_status = ASSET_STATUS + idc_name = request.GET.get('idc', '') + group_name = request.GET.get('group', '') + asset_type = request.GET.get('asset_type', '') + status = request.GET.get('status', '') + keyword = request.GET.get('keyword', '') + export = request.GET.get("export", False) + group_id = request.GET.get("group_id", '') + idc_id = request.GET.get("idc_id", '') + asset_id_all = request.GET.getlist("id", '') + + if group_id: + group = get_object(AssetGroup, id=group_id) + if group: + asset_find = Asset.objects.filter(group=group) + elif idc_id: + idc = get_object(IDC, id=idc_id) + if idc: + asset_find = Asset.objects.filter(idc=idc) else: - return httperror(request, '没有此主机!') + if user_perm != 0: + asset_find = Asset.objects.all() + else: + asset_id_all = [] + user = get_object(User, username=username) + asset_perm = get_group_user_perm(user) if user else {'asset': ''} + user_asset_perm = asset_perm['asset'].keys() + for asset in user_asset_perm: + asset_id_all.append(asset.id) + asset_find = Asset.objects.filter(pk__in=asset_id_all) + asset_group_all = list(asset_perm['asset_group']) - e_group = post.bis_group.all() + if idc_name: + asset_find = asset_find.filter(idc__name__contains=idc_name) + + if group_name: + asset_find = asset_find.filter(group__name__contains=group_name) + + if asset_type: + asset_find = asset_find.filter(asset_type__contains=asset_type) + + if status: + asset_find = asset_find.filter(status__contains=status) + + if keyword: + asset_find = asset_find.filter( + Q(hostname__contains=keyword) | + Q(other_ip__contains=keyword) | + Q(ip__contains=keyword) | + Q(remote_ip__contains=keyword) | + Q(comment__contains=keyword) | + Q(username__contains=keyword) | + Q(group__name__contains=keyword) | + Q(cpu__contains=keyword) | + Q(memory__contains=keyword) | + Q(disk__contains=keyword) | + Q(brand__contains=keyword) | + Q(cabinet__contains=keyword) | + Q(sn__contains=keyword) | + Q(system_type__contains=keyword) | + Q(system_version__contains=keyword)) + + if export: + if asset_id_all: + asset_find = [] + for asset_id in asset_id_all: + asset = get_object(Asset, id=asset_id) + if asset: + asset_find.append(asset) + s = write_excel(asset_find) + if s[0]: + file_name = s[1] + smg = u'excel文件已生成,请点击下载!' + return my_render('jasset/asset_excel_download.html', locals(), request) + assets_list, p, assets, page_range, current_page, show_first, show_end = pages(asset_find, request) + if user_perm != 0: + return my_render('jasset/asset_list.html', locals(), request) + else: + return my_render('jasset/asset_cu_list.html', locals(), request) + + +@require_role('admin') +def asset_edit_batch(request): + af = AssetForm() + name = request.user.username + asset_group_all = AssetGroup.objects.all() if request.method == 'POST': - j_ip = request.POST.get('j_ip') - j_idc = request.POST.get('j_idc') - j_port = request.POST.get('j_port') - j_type = request.POST.get('j_type') - j_dept = request.POST.getlist('j_dept') - j_group = request.POST.getlist('j_group') - j_active = request.POST.get('j_active') - j_comment = request.POST.get('j_comment') + env = request.POST.get('env', '') + idc_id = request.POST.get('idc', '') + port = request.POST.get('port', '') + use_default_auth = request.POST.get('use_default_auth', '') + username = request.POST.get('username', '') + password = request.POST.get('password', '') + group = request.POST.getlist('group', []) + cabinet = request.POST.get('cabinet', '') + comment = request.POST.get('comment', '') + asset_id_all = unicode(request.GET.get('asset_id_all', '')) + asset_id_all = asset_id_all.split(',') + for asset_id in asset_id_all: + alert_list = [] + asset = get_object(Asset, id=asset_id) + if asset: + if env: + if asset.env != env: + asset.env = env + alert_list.append([u'运行环境', asset.env, env]) + if idc_id: + idc = get_object(IDC, id=idc_id) + name_old = asset.idc.name if asset.idc else u'' + if idc and idc.name != name_old: + asset.idc = idc + alert_list.append([u'机房', name_old, idc.name]) + if port: + if unicode(asset.port) != port: + asset.port = port + alert_list.append([u'端口号', asset.port, port]) - host_info = [j_ip, j_port, j_idc, j_type, j_group, j_dept, j_active, j_comment] + if use_default_auth: + if use_default_auth == 'default': + asset.use_default_auth = 1 + asset.username = '' + asset.password = '' + alert_list.append([u'使用默认管理账号', asset.use_default_auth, u'默认']) + elif use_default_auth == 'user_passwd': + asset.use_default_auth = 0 + asset.username = username + password_encode = CRYPTOR.encrypt(password) + asset.password = password_encode + alert_list.append([u'使用默认管理账号', asset.use_default_auth, username]) + if group: + group_new, group_old, group_new_name, group_old_name = [], asset.group.all(), [], [] + for group_id in group: + g = get_object(AssetGroup, id=group_id) + if g: + group_new.append(g) + if not set(group_new) < set(group_old): + group_instance = list(set(group_new) | set(group_old)) + for g in group_instance: + group_new_name.append(g.name) + for g in group_old: + group_old_name.append(g.name) + asset.group = group_instance + alert_list.append([u'主机组', ','.join(group_old_name), ','.join(group_new_name)]) + if cabinet: + if asset.cabinet != cabinet: + asset.cabinet = cabinet + alert_list.append([u'机柜号', asset.cabinet, cabinet]) + if comment: + if asset.comment != comment: + asset.comment = comment + alert_list.append([u'备注', asset.comment, comment]) + asset.save() - if not validate(request, asset_group=j_group, edept=j_dept): - emg = u'修改失败,您无权操作!' - return my_render('jasset/host_edit.html', locals(), request) + if alert_list: + recode_name = unicode(name) + ' - ' + u'批量' + AssetRecord.objects.create(asset=asset, username=recode_name, content=alert_list) + return my_render('jasset/asset_update_status.html', locals(), request) - if j_type == 'M': - j_user = request.POST.get('j_user') - j_password = request.POST.get('j_password') - db_host_update(host_info, j_user, j_password, post) - else: - db_host_update(host_info, post) - - smg = u'主机 %s 修改成功' % j_ip - return HttpResponseRedirect('/jasset/host_detail/?id=%s' % host_id) - - return my_render('jasset/host_edit.html', locals(), request) + return my_render('jasset/asset_edit_batch.html', locals(), request) -@require_login -def host_detail(request): - """ 主机详情 """ +@require_role('admin') +def asset_detail(request): + """ + Asset detail view + """ header_title, path1, path2 = u'主机详细信息', u'资产管理', u'主机详情' - host_id = request.GET.get('id', '') - post = Asset.objects.filter(id=host_id) - if not post: - return httperror(request, '没有此主机!') - post = post[0] + asset_id = request.GET.get('id', '') + asset = get_object(Asset, id=asset_id) + perm_info = get_group_asset_perm(asset) + log = Log.objects.filter(host=asset.hostname) + if perm_info: + user_perm = [] + for perm, value in perm_info.items(): + if perm == 'user': + for user, role_dic in value.items(): + user_perm.append([user, role_dic.get('role', '')]) + elif perm == 'user_group' or perm == 'rule': + user_group_perm = value + print perm_info - if is_group_admin(request) and not validate(request, asset=[host_id]): - return httperror(request, '您无权查看!') + asset_record = AssetRecord.objects.filter(asset=asset).order_by('-alert_time') - elif is_common_user(request): - username = get_session_user_info(request)[1] - user_permed_hosts = user_perm_asset_api(username) - if post not in user_permed_hosts: - return httperror(request, '您无权查看!') + return my_render('jasset/asset_detail.html', locals(), request) + + +@require_role('admin') +def asset_update(request): + """ + Asset update host info via ansible view + """ + asset_id = request.GET.get('id', '') + asset = get_object(Asset, id=asset_id) + name = request.user.username + if not asset: + return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id) else: - log_all = Log.objects.filter(host=post.ip) - log, log_more = log_all[:10], log_all[10:] - user_permed_list = asset_perm_api(post) - - return my_render('jasset/host_detail.html', locals(), request) + asset_ansible_update([asset], name) + return HttpResponseRedirect(reverse('asset_detail')+'?id=%s' % asset_id) -@require_super_user +@require_role('admin') +def asset_update_batch(request): + if request.method == 'POST': + arg = request.GET.get('arg', '') + name = unicode(request.user.username) + ' - ' + u'自动更新' + if arg == 'all': + asset_list = Asset.objects.all() + else: + asset_list = [] + asset_id_all = unicode(request.POST.get('asset_id_all', '')) + asset_id_all = asset_id_all.split(',') + for asset_id in asset_id_all: + asset = get_object(Asset, id=asset_id) + if asset: + asset_list.append(asset) + asset_ansible_update(asset_list, name) + return HttpResponse(u'批量更新成功!') + return HttpResponse(u'批量更新成功!') + + +@require_role('admin') def idc_add(request): - """ 添加IDC """ + """ + IDC add view + """ header_title, path1, path2 = u'添加IDC', u'资产管理', u'添加IDC' if request.method == 'POST': - j_idc = request.POST.get('j_idc') - j_comment = request.POST.get('j_comment') - if IDC.objects.filter(name=j_idc): - emg = u'该IDC已存在!' - return my_render('jasset/idc_add.html', locals(), request) - else: - smg = u'IDC:%s添加成功' % j_idc - IDC.objects.create(name=j_idc, comment=j_comment) + idc_form = IdcForm(request.POST) + if idc_form.is_valid(): + idc_name = idc_form.cleaned_data['name'] + if IDC.objects.filter(name=idc_name): + emg = u'添加失败, 此IDC %s 已存在!' % idc_name + return my_render('jasset/idc_add.html', locals(), request) + else: + idc_form.save() + smg = u'IDC: %s添加成功' % idc_name + return HttpResponseRedirect(reverse('idc_list')) + else: + idc_form = IdcForm() return my_render('jasset/idc_add.html', locals(), request) -@require_admin +@require_role('admin') def idc_list(request): - """ 列出IDC """ + """ + IDC list view + """ header_title, path1, path2 = u'查看IDC', u'资产管理', u'查看IDC' - dept_id = get_user_dept(request) - dept = DEPT.objects.get(id=dept_id) + posts = IDC.objects.all() keyword = request.GET.get('keyword', '') if keyword: posts = IDC.objects.filter(Q(name__contains=keyword) | Q(comment__contains=keyword)) @@ -593,338 +523,48 @@ def idc_list(request): return my_render('jasset/idc_list.html', locals(), request) -@require_super_user +@require_role('admin') def idc_edit(request): - """ 修改IDC """ + """ + IDC edit view + """ header_title, path1, path2 = u'编辑IDC', u'资产管理', u'编辑IDC' idc_id = request.GET.get('id', '') - idc = IDC.objects.filter(id=idc_id) - if int(idc_id) == 1: - return httperror(request, u'默认IDC不能编辑!') - if idc: - idc = idc[0] - default = IDC.objects.get(id=1).asset_set.all() - eposts = Asset.objects.filter(idc=idc).order_by('ip') - posts = [g for g in default if g not in eposts] - else: - return httperror(request, u'此IDC不存在') - + idc = get_object(IDC, id=idc_id) if request.method == 'POST': - idc_id = request.POST.get('id') - j_idc = request.POST.get('j_idc') - j_hosts = request.POST.getlist('j_hosts') - j_comment = request.POST.get('j_comment') - idc_default = request.POST.getlist('idc_default') - - idc = IDC.objects.filter(id=idc_id) - if idc: - idc.update(name=j_idc, comment=j_comment) - for host_id in j_hosts: - Asset.objects.filter(id=host_id).update(idc=idc[0]) - - i = IDC.objects.get(id=1) - for host in idc_default: - g = Asset.objects.filter(id=host).update(idc=i) - else: - return httperror(request, u'此IDC不存在') - - return HttpResponseRedirect('/jasset/idc_list/?id=%s' % idc_id) - - return my_render('jasset/idc_edit.html', locals(), request) - - -@require_admin -def idc_detail(request): - """ IDC详情 """ - header_title, path1, path2 = u'IDC详情', u'资产管理', u'IDC详情' - login_types = {'L': 'LDAP', 'M': 'MAP'} - idc_id = request.GET.get('id', '') - idc_filter = IDC.objects.filter(id=idc_id) - if idc_filter: - idc = idc_filter[0] + idc_form = IdcForm(request.POST, instance=idc) + if idc_form.is_valid(): + idc_form.save() + return HttpResponseRedirect(reverse('idc_list')) else: - return httperror(request, '没有此IDC') - dept = get_session_user_info(request)[5] - if is_super_user(request): - posts = Asset.objects.filter(idc=idc).order_by('ip') - elif is_group_admin(request): - posts = Asset.objects.filter(idc=idc, dept=dept).order_by('ip') - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - - return my_render('jasset/idc_detail.html', locals(), request) + idc_form = IdcForm(instance=idc) + return my_render('jasset/idc_edit.html', locals(), request) -@require_super_user +@require_role('admin') def idc_del(request): - """ 删除IDC """ - offset = request.GET.get('id', '') - if offset == 'multi': - len_list = request.POST.get("len_list") - for i in range(int(len_list)): - key = "id_list[" + str(i) + "]" - idc_id = request.POST.get(key) - db_idc_delete(request, int(idc_id)) - else: - db_idc_delete(request, int(offset)) - return HttpResponseRedirect('/jasset/idc_list/') + """ + IDC delete view + """ + idc_ids = request.GET.get('id', '') + idc_id_list = idc_ids.split(',') + + for idc_id in idc_id_list: + IDC.objects.filter(id=idc_id).delete() + + return HttpResponseRedirect(reverse('idc_list')) -@require_admin -def group_add(request): - """ 添加主机组 """ - header_title, path1, path2 = u'添加主机组', u'资产管理', u'添加主机组' - if is_super_user(request): - posts = Asset.objects.all() - edept = DEPT.objects.all() - elif is_group_admin(request): - dept_id = get_user_dept(request) - dept = DEPT.objects.get(id=dept_id) - posts = Asset.objects.filter(dept=dept) - edept = get_session_user_info(request)[5] - +@require_role('admin') +def asset_upload(request): + """ + Upload asset excel file view + """ if request.method == 'POST': - j_group = request.POST.get('j_group', '') - j_dept = request.POST.get('j_dept', '') - j_hosts = request.POST.getlist('j_hosts', '') - j_comment = request.POST.get('j_comment', '') - - try: - if is_group_admin(request) and not validate(request, asset=j_hosts, edept=[j_dept]): - emg = u'添加失败, 您无权操作!' - raise RaiseError - - elif BisGroup.objects.filter(name=j_group): - emg = u'添加失败, 该主机组已存在!' - raise RaiseError - - except RaiseError: - pass - + excel_file = request.FILES.get('file_name', '') + ret = excel_to_db(excel_file) + if ret: + smg = u'批量添加成功' else: - j_dept = DEPT.objects.filter(id=j_dept)[0] - group = BisGroup.objects.create(name=j_group, dept=j_dept, comment=j_comment) - for host in j_hosts: - g = Asset.objects.get(id=host) - group.asset_set.add(g) - smg = u'主机组 %s 添加成功' % j_group - - return my_render('jasset/group_add.html', locals(), request) - - -@require_admin -def group_list(request): - """ 列出主机组 """ - header_title, path1, path2 = u'查看主机组', u'资产管理', u'查看主机组' - dept_id = get_user_dept(request) - dept = DEPT.objects.get(id=dept_id) - keyword = request.GET.get('keyword', '') - gid = request.GET.get('gid') - sid = request.GET.get('sid') - if gid: - if is_common_user(request): - return httperror(request, u'您无权查看!') - - elif is_group_admin(request) and not validate(request, user_group=[gid]): - return httperror(request, u'您无权查看!') - - posts = [] - user_group = UserGroup.objects.filter(id=gid) - if user_group: - user_group = user_group[0] - perms = Perm.objects.filter(user_group=user_group) - for perm in perms: - posts.append(perm.asset_group) - - elif sid: - if is_common_user(request): - return httperror(request, u'您无权查看!') - - elif is_group_admin(request) and not validate(request, user_group=[sid]): - return httperror(request, u'您无权查看!') - - posts = [] - user_group = UserGroup.objects.filter(id=sid) - if user_group: - user_group = user_group[0] - for perm in user_group.sudoperm_set.all(): - posts.extend(perm.asset_group.all()) - posts = list(set(posts)) - else: - return httperror(request, u'没有此sudo授权!') - - else: - if is_super_user(request): - if keyword: - posts = BisGroup.objects.exclude(name='ALL').filter( - Q(name__contains=keyword) | Q(comment__contains=keyword)) - else: - posts = BisGroup.objects.exclude(name='ALL').order_by('id') - elif is_group_admin(request): - if keyword: - posts = BisGroup.objects.filter(Q(name__contains=keyword) | Q(comment__contains=keyword)).filter( - dept=dept) - else: - posts = BisGroup.objects.filter(dept=dept).order_by('id') - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return my_render('jasset/group_list.html', locals(), request) - - -@require_admin -def group_edit(request): - """ 修改主机组 """ - header_title, path1, path2 = u'编辑主机组', u'资产管理', u'编辑主机组' - group_id = request.GET.get('id', '') - group = BisGroup.objects.filter(id=group_id) - if group: - group = group[0] - else: - httperror(request, u'没有这个主机组!') - - host_all = Asset.objects.all() - dept_id = get_session_user_info(request)[3] - eposts = Asset.objects.filter(bis_group=group) - - if is_group_admin(request) and not validate(request, asset_group=[group_id]): - return httperror(request, '编辑失败, 您无权操作!') - dept = DEPT.objects.filter(id=group.dept.id) - if dept: - dept = dept[0] - else: - return httperror(request, u'没有这个部门!') - - all_dept = dept.asset_set.all() - posts = [g for g in all_dept if g not in eposts] - - if request.method == 'POST': - j_group = request.POST.get('j_group', '') - j_hosts = request.POST.getlist('j_hosts', '') - j_dept = request.POST.get('j_dept', '') - j_comment = request.POST.get('j_comment', '') - - j_dept = DEPT.objects.filter(id=int(j_dept)) - j_dept = j_dept[0] - - group.asset_set.clear() - for host in j_hosts: - g = Asset.objects.get(id=host) - group.asset_set.add(g) - BisGroup.objects.filter(id=group_id).update(name=j_group, dept=j_dept, comment=j_comment) - smg = u'主机组%s修改成功' % j_group - return HttpResponseRedirect('/jasset/group_list') - - return my_render('jasset/group_edit.html', locals(), request) - - -@require_admin -def group_detail(request): - """ 主机组详情 """ - header_title, path1, path2 = u'主机组详情', u'资产管理', u'主机组详情' - login_types = {'L': 'LDAP', 'M': 'MAP'} - dept = get_session_user_info(request)[5] - group_id = request.GET.get('id', '') - group = BisGroup.objects.get(id=group_id) - if is_super_user(request): - posts = Asset.objects.filter(bis_group=group).order_by('ip') - - elif is_group_admin(request): - if not validate(request, asset_group=[group_id]): - return httperror(request, u'您无权查看!') - posts = Asset.objects.filter(bis_group=group).filter(dept=dept).order_by('ip') - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return my_render('jasset/group_detail.html', locals(), request) - - -@require_admin -def group_del_host(request): - """ 主机组中剔除主机, 并不删除真实主机 """ - if request.method == 'POST': - group_id = request.POST.get('group_id') - offset = request.GET.get('id', '') - group = BisGroup.objects.get(id=group_id) - if offset == 'group': - len_list = request.POST.get("len_list") - for i in range(int(len_list)): - key = "id_list[" + str(i) + "]" - jid = request.POST.get(key) - g = Asset.objects.get(id=jid) - group.asset_set.remove(g) - - else: - offset = request.GET.get('id', '') - group_id = request.GET.get('gid', '') - group = BisGroup.objects.get(id=group_id) - g = Asset.objects.get(id=offset) - group.asset_set.remove(g) - - return HttpResponseRedirect('/jasset/group_detail/?id=%s' % group.id) - - -@require_admin -def group_del(request): - """ 删除主机组 """ - offset = request.GET.get('id', '') - if offset == 'multi': - len_list = request.POST.get("len_list") - for i in range(int(len_list)): - key = "id_list[" + str(i) + "]" - gid = request.POST.get(key) - if is_group_admin(request) and not validate(request, asset_group=[gid]): - return httperror(request, '删除失败, 您无权删除!') - BisGroup.objects.filter(id=gid).delete() - else: - gid = int(offset) - if is_group_admin(request) and not validate(request, asset_group=[gid]): - return httperror(request, '删除失败, 您无权删除!') - BisGroup.objects.filter(id=gid).delete() - return HttpResponseRedirect('/jasset/group_list/') - - -@require_admin -def dept_host_ajax(request): - """ 添加主机组时, 部门联动主机异步 """ - dept_id = request.GET.get('id', '') - if dept_id not in ['1', '2']: - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - hosts = dept.asset_set.all() - else: - hosts = Asset.objects.all() - - return my_render('jasset/dept_host_ajax.html', locals(), request) - - -def show_all_ajax(request): - """ 批量修改主机时, 部门和组全部显示 """ - env = request.GET.get('env', '') - get_id = request.GET.get('id', '') - host = Asset.objects.filter(id=get_id) - if host: - host = host[0] - return my_render('jasset/show_all_ajax.html', locals(), request) - - -@require_login -def host_search(request): - """ 搜索主机 """ - keyword = request.GET.get('keyword') - login_types = {'L': 'LDAP', 'M': 'MAP'} - dept = get_session_user_info(request)[5] - post_all = Asset.objects.filter(Q(ip__contains=keyword) | - Q(idc__name__contains=keyword) | - Q(bis_group__name__contains=keyword) | - Q(comment__contains=keyword)).distinct().order_by('ip') - if is_super_user(request): - posts = post_all - - elif is_group_admin(request): - posts = post_all.filter(dept=dept) - - elif is_common_user(request): - user_id, username = get_session_user_info(request)[0:2] - post_perm = user_perm_asset_api(username) - posts = list(set(post_all) & set(post_perm)) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - - return my_render('jasset/host_search.html', locals(), request) \ No newline at end of file + emg = u'批量添加失败,请检查格式.' + return my_render('jasset/asset_add_batch.html', locals(), request) diff --git a/jlog/models.py b/jlog/models.py index 3d2a319df..c8ffd77a2 100644 --- a/jlog/models.py +++ b/jlog/models.py @@ -3,14 +3,13 @@ from django.db import models class Log(models.Model): user = models.CharField(max_length=20, null=True) - host = models.CharField(max_length=20, null=True) + host = models.CharField(max_length=200, null=True) remote_ip = models.CharField(max_length=100) - dept_name = models.CharField(max_length=20) + login_type = models.CharField(max_length=100) log_path = models.CharField(max_length=100) start_time = models.DateTimeField(null=True) - pid = models.IntegerField(max_length=10) + pid = models.IntegerField() is_finished = models.BooleanField(default=False) - log_finished = models.BooleanField(default=False) end_time = models.DateTimeField(null=True) def __unicode__(self): @@ -20,4 +19,31 @@ class Log(models.Model): class Alert(models.Model): msg = models.CharField(max_length=20) time = models.DateTimeField(null=True) - is_finished = models.BigIntegerField(default=False) \ No newline at end of file + is_finished = models.BigIntegerField(default=False) + + +class TtyLog(models.Model): + log = models.ForeignKey(Log) + datetime = models.DateTimeField(auto_now=True) + cmd = models.CharField(max_length=200) + + +class ExecLog(models.Model): + user = models.CharField(max_length=100) + host = models.TextField() + cmd = models.TextField() + remote_ip = models.CharField(max_length=100) + result = models.TextField(default='') + datetime = models.DateTimeField(auto_now=True) + + +class FileLog(models.Model): + user = models.CharField(max_length=100) + host = models.TextField() + filename = models.TextField() + type = models.CharField(max_length=20) + remote_ip = models.CharField(max_length=100) + result = models.TextField(default='') + datetime = models.DateTimeField(auto_now=True) + + diff --git a/jlog/urls.py b/jlog/urls.py index 0b6810d3c..0790c9307 100644 --- a/jlog/urls.py +++ b/jlog/urls.py @@ -3,9 +3,9 @@ from django.conf.urls import patterns, include, url from jlog.views import * urlpatterns = patterns('', - url(r'^$', log_list), - url(r'^log_list/(\w+)/$', log_list), - url(r'^log_kill/', log_kill), - url(r'^history/$', log_history), - url(r'^search/$', log_search), -) \ No newline at end of file + url(r'^list/(\w+)/$', log_list, name='log_list'), + url(r'^detail/(\w+)/$', log_detail, name='log_detail'), + url(r'^history/$', log_history, name='log_history'), + url(r'^log_kill/', log_kill, name='log_kill'), + url(r'^record/$', log_record, name='log_record'), + ) \ No newline at end of file diff --git a/jlog/views.py b/jlog/views.py index 0eb74f815..b8cc089d5 100644 --- a/jlog/views.py +++ b/jlog/views.py @@ -4,78 +4,93 @@ from django.template import RequestContext from django.shortcuts import render_to_response from jumpserver.api import * -from jasset.views import httperror +from jperm.perm_api import user_have_perm from django.http import HttpResponseNotFound +from jlog.log_api import renderTemplate -CONF = ConfigParser() -CONF.read('%s/jumpserver.conf' % BASE_DIR) +from jlog.models import Log, ExecLog, FileLog +from jumpserver.settings import WEB_SOCKET_HOST -def get_user_info(request, offset): - """ 获取用户信息及环境 """ - env_dic = {'online': 0, 'offline': 1} - env = env_dic[offset] - keyword = request.GET.get('keyword', '') - user_info = get_session_user_info(request) - user_id, username = user_info[0:2] - dept_id, dept_name = user_info[3:5] - ret = [request, keyword, env, username, dept_name] - - return ret - - -def get_user_log(ret_list): - """ 获取不同类型用户日志记录 """ - request, keyword, env, username, dept_name = ret_list - post_all = Log.objects.filter(is_finished=env).order_by('-start_time') - post_keyword_all = Log.objects.filter(Q(user__contains=keyword) | - Q(host__contains=keyword)) \ - .filter(is_finished=env).order_by('-start_time') - - if is_super_user(request): - if keyword: - posts = post_keyword_all - else: - posts = post_all - - elif is_group_admin(request): - if keyword: - posts = post_keyword_all.filter(dept_name=dept_name) - else: - posts = post_all.filter(dept_name=dept_name) - - elif is_common_user(request): - if keyword: - posts = post_keyword_all.filter(user=username) - else: - posts = post_all.filter(user=username) - - return posts - - -@require_login +@require_role('admin') def log_list(request, offset): """ 显示日志 """ - header_title, path1, path2 = u'查看日志', u'查看日志', u'在线用户' - keyword = request.GET.get('keyword', '') - web_socket_host = CONF.get('websocket', 'web_socket_host') - posts = get_user_log(get_user_info(request, offset)) + header_title, path1 = u'审计', u'操作审计' + date_seven_day = request.GET.get('start', '') + date_now_str = request.GET.get('end', '') + username_list = request.GET.getlist('username', []) + host_list = request.GET.getlist('host', []) + cmd = request.GET.get('cmd', '') + + if offset == 'online': + keyword = request.GET.get('keyword', '') + posts = Log.objects.filter(is_finished=False).order_by('-start_time') + if keyword: + posts = posts.filter(Q(user__icontains=keyword) | Q(host__icontains=keyword) | + Q(login_type_icontains=keyword)) + + elif offset == 'exec': + posts = ExecLog.objects.all().order_by('-id') + keyword = request.GET.get('keyword', '') + if keyword: + posts = posts.filter(Q(user__icontains=keyword)|Q(host__icontains=keyword)|Q(cmd__icontains=keyword)) + elif offset == 'file': + posts = FileLog.objects.all().order_by('-id') + keyword = request.GET.get('keyword', '') + if keyword: + posts = posts.filter(Q(user__icontains=keyword)|Q(host__icontains=keyword)|Q(filename__icontains=keyword)) + else: + posts = Log.objects.filter(is_finished=True).order_by('-start_time') + username_all = set([log.user for log in Log.objects.all()]) + ip_all = set([log.host for log in Log.objects.all()]) + + if date_seven_day and date_now_str: + datetime_start = datetime.datetime.strptime(date_seven_day + ' 00:00:01', '%m/%d/%Y %H:%M:%S') + datetime_end = datetime.datetime.strptime(date_now_str + ' 23:59:59', '%m/%d/%Y %H:%M:%S') + posts = posts.filter(start_time__gte=datetime_start).filter(start_time__lte=datetime_end) + + if username_list: + posts = posts.filter(user__in=username_list) + + if host_list: + posts = posts.filter(host__in=host_list) + + if cmd: + log_id_list = set([log.log_id for log in TtyLog.objects.filter(cmd__contains=cmd)]) + posts = posts.filter(id__in=log_id_list) + + if not date_seven_day: + date_now = datetime.datetime.now() + date_now_str = date_now.strftime('%m/%d/%Y') + date_seven_day = (date_now + datetime.timedelta(days=-7)).strftime('%m/%d/%Y') + contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) + web_monitor_uri = 'ws://%s/monitor' % WEB_SOCKET_HOST + web_kill_uri = 'http://%s/kill' % WEB_SOCKET_HOST + session_id = request.session.session_key return render_to_response('jlog/log_%s.html' % offset, locals(), context_instance=RequestContext(request)) -@require_admin +@require_role('admin') +def log_detail(request): + return my_render('jlog/exec_detail.html', locals(), request) + + +@require_role('admin') def log_kill(request): """ 杀掉connect进程 """ pid = request.GET.get('id', '') log = Log.objects.filter(pid=pid) if log: log = log[0] +<<<<<<< HEAD dept_name = log.dept_name deptname = get_session_user_info(request)[4] if is_group_admin(request) and dept_name != deptname: return httperror(request, u'Kill失败, 您无权操作!') +======= +>>>>>>> dev try: os.kill(int(pid), 9) except OSError: @@ -86,35 +101,56 @@ def log_kill(request): return HttpResponseNotFound(u'没有此进程!') -@require_login +@require_role('admin') def log_history(request): """ 命令历史记录 """ + log_id = request.GET.get('id', 0) + log = Log.objects.filter(id=log_id) + if log: + log = log[0] + tty_logs = log.ttylog_set.all() + + if tty_logs: + content = '' + for tty_log in tty_logs: + content += '%s: %s\n' % (tty_log.datetime.strftime('%Y-%m-%d %H:%M:%S'), tty_log.cmd) + return HttpResponse(content) + + return HttpResponse('无日志记录!') + + +@require_role('admin') +def log_record(request): log_id = request.GET.get('id', 0) log = Log.objects.filter(id=int(log_id)) if log: log = log[0] - dept_name = log.dept_name - deptname = get_session_user_info(request)[4] - if is_group_admin(request) and dept_name != deptname: - return httperror(request, '查看失败, 您无权查看!') - - elif is_common_user(request): - return httperror(request, '查看失败, 您无权查看!') - - log_his = "%s.his" % log.log_path - if os.path.isfile(log_his): - f = open(log_his) - content = f.read() + log_file = log.log_path + '.log' + log_time = log.log_path + '.time' + if os.path.isfile(log_file) and os.path.isfile(log_time): + content = renderTemplate(log_file, log_time) return HttpResponse(content) else: - return httperror(request, '无日志记录, 请查看日志处理脚本是否开启!') + return HttpResponse('无日志记录!') -@require_login -def log_search(request): - """ 日志搜索 """ - offset = request.GET.get('env', '') - keyword = request.GET.get('keyword', '') - posts = get_user_log(get_user_info(request, offset)) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return render_to_response('jlog/log_search.html', locals(), context_instance=RequestContext(request)) +@require_role('admin') +def log_detail(request, offset): + log_id = request.GET.get('id') + if offset == 'exec': + log = get_object(ExecLog, id=log_id) + assets_hostname = log.host.split(' ') + try: + result = eval(str(log.result)) + except (SyntaxError, NameError): + result = {} + return my_render('jlog/exec_detail.html', locals(), request) + elif offset == 'file': + log = get_object(FileLog, id=log_id) + assets_hostname = log.host.split(' ') + file_list = log.filename.split(' ') + try: + result = eval(str(log.result)) + except (SyntaxError, NameError): + result = {} + return my_render('jlog/file_detail.html', locals(), request) diff --git a/jperm/models.py b/jperm/models.py index c29cb8e54..425d01410 100644 --- a/jperm/models.py +++ b/jperm/models.py @@ -1,54 +1,60 @@ import datetime -from uuidfield import UUIDField - from django.db import models -from juser.models import UserGroup, DEPT -from jasset.models import Asset, BisGroup +from jasset.models import Asset, AssetGroup +from juser.models import User, UserGroup -class Perm(models.Model): - user_group = models.ForeignKey(UserGroup) - asset_group = models.ForeignKey(BisGroup) - - def __unicode__(self): - return '%s_%s' % (self.user_group.name, self.asset_group.name) +class PermLog(models.Model): + datetime = models.DateTimeField(auto_now_add=True) + action = models.CharField(max_length=100, null=True, blank=True, default='') + results = models.CharField(max_length=1000, null=True, blank=True, default='') + is_success = models.BooleanField(default=False) + is_finish = models.BooleanField(default=False) -class CmdGroup(models.Model): - name = models.CharField(max_length=50, unique=True) - cmd = models.CharField(max_length=999) - dept = models.ForeignKey(DEPT) - comment = models.CharField(blank=True, null=True, max_length=50) +class PermSudo(models.Model): + name = models.CharField(max_length=100, unique=True) + date_added = models.DateTimeField(auto_now=True) + commands = models.TextField() + comment = models.CharField(max_length=100, null=True, blank=True, default='') def __unicode__(self): return self.name -class SudoPerm(models.Model): - user_group = models.ForeignKey(UserGroup) - user_runas = models.CharField(max_length=100) - asset_group = models.ManyToManyField(BisGroup) - cmd_group = models.ManyToManyField(CmdGroup) - comment = models.CharField(max_length=30, null=True, blank=True) +class PermRole(models.Model): + name = models.CharField(max_length=100, unique=True) + comment = models.CharField(max_length=100, null=True, blank=True, default='') + password = models.CharField(max_length=100) + key_path = models.CharField(max_length=100) + date_added = models.DateTimeField(auto_now=True) + sudo = models.ManyToManyField(PermSudo, related_name='perm_role') def __unicode__(self): - return self.user_group.name + return self.name -class Apply(models.Model): - uuid = UUIDField(auto=True) - applyer = models.CharField(max_length=20) - admin = models.CharField(max_length=20) - approver = models.CharField(max_length=20) - dept = models.CharField(max_length=20) - bisgroup = models.CharField(max_length=500) - asset = models.CharField(max_length=500) - comment = models.TextField(blank=True, null=True) - status = models.IntegerField(max_length=2) - date_add = models.DateTimeField(null=True) - date_end = models.DateTimeField(null=True) - read = models.IntegerField(max_length=2) +class PermRule(models.Model): + date_added = models.DateTimeField(auto_now=True) + name = models.CharField(max_length=100, unique=True) + comment = models.CharField(max_length=100) + asset = models.ManyToManyField(Asset, related_name='perm_rule') + asset_group = models.ManyToManyField(AssetGroup, related_name='perm_rule') + user = models.ManyToManyField(User, related_name='perm_rule') + user_group = models.ManyToManyField(UserGroup, related_name='perm_rule') + role = models.ManyToManyField(PermRole, related_name='perm_rule') def __unicode__(self): - return self.applyer + return self.name + + +class PermPush(models.Model): + asset = models.ForeignKey(Asset, related_name='perm_push') + role = models.ForeignKey(PermRole, related_name='perm_push') + is_public_key = models.BooleanField(default=False) + is_password = models.BooleanField(default=False) + success = models.BooleanField(default=False) + result = models.TextField(default='') + date_added = models.DateTimeField(auto_now=True) + diff --git a/jperm/urls.py b/jperm/urls.py index 41a7b26ed..5fd3320a6 100644 --- a/jperm/urls.py +++ b/jperm/urls.py @@ -2,32 +2,21 @@ from django.conf.urls import patterns, include, url from jperm.views import * urlpatterns = patterns('jperm.views', - # Examples: - # url(r'^$', 'jumpserver.views.home', name='home'), - # url(r'^blog/', include('blog.urls')), - - (r'^perm_edit/$', view_splitter, {'su': perm_edit, 'adm': perm_edit_adm}), - (r'^dept_perm_edit/$', 'dept_perm_edit'), - (r'^perm_list/$', view_splitter, {'su': perm_list, 'adm': perm_list_adm}), - (r'^dept_perm_list/$', 'dept_perm_list'), - (r'^perm_user_detail/$', 'perm_user_detail'), - (r'^perm_detail/$', 'perm_detail'), - (r'^perm_del/$', 'perm_del'), - (r'^perm_asset_detail/$', 'perm_asset_detail'), - (r'^sudo_list/$', view_splitter, {'su': sudo_list, 'adm': sudo_list_adm}), - (r'^sudo_del/$', 'sudo_del'), - (r'^sudo_edit/$', view_splitter, {'su': sudo_edit, 'adm': sudo_edit_adm}), - (r'^sudo_refresh/$', 'sudo_refresh'), - (r'^sudo_detail/$', 'sudo_detail'), - (r'^cmd_add/$', view_splitter, {'su': cmd_add, 'adm': cmd_add_adm}), - (r'^cmd_list/$', 'cmd_list'), - (r'^cmd_del/$', 'cmd_del'), - (r'^cmd_edit/$', 'cmd_edit'), - (r'^cmd_detail/$', 'cmd_detail'), - (r'^apply/$', 'perm_apply'), - (r'^apply_show/(\w+)/$', 'perm_apply_log'), - (r'^apply_exec/$', 'perm_apply_exec'), - (r'^apply_info/$', 'perm_apply_info'), - (r'^apply_del/$', 'perm_apply_del'), - (r'^apply_search/$', 'perm_apply_search'), -) + url(r'^rule/list/$', perm_rule_list, name='rule_list'), + url(r'^rule/add/$', perm_rule_add, name='rule_add'), + url(r'^rule/detail/$', perm_rule_detail, name='rule_detail'), + url(r'^rule/edit/$', perm_rule_edit, name='rule_edit'), + url(r'^rule/del/$', perm_rule_delete, name='rule_del'), + url(r'^role/list/$', perm_role_list, name='role_list'), + url(r'^role/add/$', perm_role_add, name='role_add'), + url(r'^role/del/$', perm_role_delete, name='role_del'), + url(r'^role/detail/$', perm_role_detail, name='role_detail'), + url(r'^role/edit/$', perm_role_edit, name='role_edit'), + url(r'^role/push/$', perm_role_push, name='role_push'), + url(r'^role/recycle/$', perm_role_recycle, name='role_recycle'), + url(r'^role/get/$', perm_role_get, name='role_get'), + url(r'^sudo/list/$', perm_sudo_list, name='sudo_list'), + url(r'^sudo/add/$', perm_sudo_add, name='sudo_add'), + url(r'^sudo/del/$', perm_sudo_delete, name='sudo_del'), + url(r'^sudo/edit/$', perm_sudo_edit, name='sudo_edit'), + ) diff --git a/jperm/views.py b/jperm/views.py index 4f38f09b9..e42faec06 100644 --- a/jperm/views.py +++ b/jperm/views.py @@ -1,184 +1,188 @@ -# coding: utf-8 -import sys +# -*- coding: utf-8 -*- -reload(sys) -sys.setdefaultencoding('utf8') - -from django.shortcuts import render_to_response -from django.template import RequestContext -from jperm.models import Perm, SudoPerm, CmdGroup, Apply from django.db.models import Q -from jumpserver.api import * +from paramiko import SSHException +from jperm.perm_api import * + +from juser.models import User, UserGroup +from jasset.models import Asset, AssetGroup +from jperm.models import PermRole, PermRule, PermSudo, PermPush +from jumpserver.models import Setting + +from jperm.utils import gen_keys +from jperm.ansible_api import MyTask +from jperm.perm_api import get_role_info, get_role_push_host +from jumpserver.api import my_render, get_object, CRYPTOR + +# 设置PERM APP Log +from jumpserver.settings import LOG_LEVEL +logger = set_log(LOG_LEVEL, filename='jumpserver_perm.log') -def asset_cmd_groups_get(asset_groups_select='', cmd_groups_select=''): - asset_groups_select_list = [] - cmd_groups_select_list = [] - - for asset_group_id in asset_groups_select: - asset_groups_select_list.extend(BisGroup.objects.filter(id=asset_group_id)) - - for cmd_group_id in cmd_groups_select: - cmd_groups_select_list.extend(CmdGroup.objects.filter(id=cmd_group_id)) - - return asset_groups_select_list, cmd_groups_select_list - - -@require_admin -def perm_add(request): - header_title, path1, path2 = u'主机授权添加', u'授权管理', u'授权添加' - - if request.method == 'GET': - user_groups = UserGroup.objects.filter(id__gt=2) - asset_groups = BisGroup.objects.all() - - else: - name = request.POST.get('name', '') - user_groups_select = request.POST.getlist('user_groups_select') - asset_groups_select = request.POST.getlist('asset_groups_select') - comment = request.POST.get('comment', '') - - user_groups, asset_groups = user_asset_cmd_groups_get(user_groups_select, asset_groups_select, '')[0:2] - - perm = Perm(name=name, comment=comment) - perm.save() - - perm.user_group = user_groups - perm.asset_group = asset_groups - msg = '添加成功' - return render_to_response('jperm/perm_add.html', locals(), context_instance=RequestContext(request)) - - -def dept_add_asset(dept_id, asset_list): - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - new_perm_asset = [] - for asset_id in asset_list: - asset = Asset.objects.filter(id=asset_id) - new_perm_asset.extend(asset) - - dept.asset_set.clear() - dept.asset_set = new_perm_asset - - -@require_super_user -def dept_perm_edit(request): - header_title, path1, path2 = u'部门授权添加', u'授权管理', u'部门授权添加' - if request.method == 'GET': - dept_id = request.GET.get('id', '') - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - asset_all = Asset.objects.all() - asset_select = dept.asset_set.all() - assets = [asset for asset in asset_all if asset not in asset_select] - else: - dept_id = request.POST.get('dept_id') - asset_select = request.POST.getlist('asset_select') - dept_add_asset(dept_id, asset_select) - return HttpResponseRedirect('/jperm/dept_perm_list/') - return render_to_response('jperm/dept_perm_edit.html', locals(), context_instance=RequestContext(request)) - - -@require_super_user -def perm_list(request): - header_title, path1, path2 = u'小组授权', u'授权管理', u'授权详情' +@require_role('admin') +def perm_rule_list(request): + """ + list rule page + 授权规则列表 + """ + # 渲染数据 + header_title, path1, path2 = "授权规则", "规则管理", "查看规则" + # 获取所有规则 + rules_list = PermRule.objects.all() + rule_id = request.GET.get('id') + # TODO: 搜索和分页 keyword = request.GET.get('search', '') - uid = request.GET.get('uid', '') - agid = request.GET.get('agid', '') + if rule_id: + rules_list = rules_list.filter(id=rule_id) + if keyword: - contact_list = UserGroup.objects.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) - else: - contact_list = UserGroup.objects.all().order_by('name') + rules_list = rules_list.filter(Q(name=keyword)) - if uid: - user = User.objects.filter(id=uid) - print user - if user: - user = user[0] - contact_list = contact_list.filter(user=user) + rules_list, p, rules, page_range, current_page, show_first, show_end = pages(rules_list, request) - if agid: - contact_list_confirm = [] - asset_group = BisGroup.objects.filter(id=agid) - if asset_group: - asset_group = asset_group[0] - for user_group in contact_list: - if asset_group in user_group_perm_asset_group_api(user_group): - contact_list_confirm.append(user_group) - contact_list = contact_list_confirm - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - return render_to_response('jperm/perm_list.html', locals(), context_instance=RequestContext(request)) + return my_render('jperm/perm_rule_list.html', locals(), request) -@require_admin -def perm_list_adm(request): - header_title, path1, path2 = u'小组授权', u'授权管理', u'授权详情' - keyword = request.GET.get('search', '') - uid = request.GET.get('uid', '') - agid = request.GET.get('agid', '') - user, dept = get_session_user_dept(request) - contact_list = dept.usergroup_set.all().order_by('name') - if keyword: - contact_list = contact_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) +@require_role('admin') +def perm_rule_detail(request): + """ + rule detail page + 授权详情 + """ + # 渲染数据 + header_title, path1, path2 = "授权规则", "规则管理", "规则详情" - if uid: - user = User.objects.filter(id=uid) - print user - if user: - user = user[0] - contact_list = contact_list.filter(user=user) + # 根据rule_id 取得rule对象 + try: + if request.method == "GET": + rule_id = request.GET.get("id") + if not rule_id: + raise ServerError("Rule Detail - no rule id get") + rule_obj = PermRule.objects.get(id=rule_id) + user_obj = rule_obj.user.all() + user_group_obj = rule_obj.user_group.all() + asset_obj = rule_obj.asset.all() + asset_group_obj = rule_obj.asset_group.all() + roles_name = [role.name for role in rule_obj.role.all()] - if agid: - contact_list_confirm = [] - asset_group = BisGroup.objects.filter(id=agid) - if asset_group: - asset_group = asset_group[0] - for user_group in contact_list: - if asset_group in user_group_perm_asset_group_api(user_group): - contact_list_confirm.append(user_group) - contact_list = contact_list_confirm + # 渲染数据 + roles_name = ','.join(roles_name) + rule = rule_obj + users = user_obj + user_groups = user_group_obj + assets = asset_obj + asset_groups = asset_group_obj + except ServerError, e: + logger.warning(e) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - return render_to_response('jperm/perm_list.html', locals(), context_instance=RequestContext(request)) + return my_render('jperm/perm_rule_detail.html', locals(), request) -@require_super_user -def dept_perm_list(request): - header_title, path1, path2 = '查看部门', '授权管理', '部门授权' - keyword = request.GET.get('search') - if keyword: - contact_list = DEPT.objects.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)).order_by('name') - else: - contact_list = DEPT.objects.filter(id__gt=2) +def perm_rule_add(request): + """ + add rule page + 添加授权 + """ + # 渲染数据 + header_title, path1, path2 = "授权规则", "规则管理", "添加规则" - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) + # 渲染数据, 获取所有 用户,用户组,资产,资产组,用户角色, 用于添加授权规则 + users = User.objects.all() + user_groups = UserGroup.objects.all() + assets = Asset.objects.all() + asset_groups = AssetGroup.objects.all() + roles = PermRole.objects.all() - return render_to_response('jperm/dept_perm_list.html', locals(), context_instance=RequestContext(request)) + if request.method == 'POST': + # 获取用户选择的 用户,用户组,资产,资产组,用户角色 + users_select = request.POST.getlist('user', []) # 需要授权用户 + user_groups_select = request.POST.getlist('user_group', []) # 需要授权用户组 + assets_select = request.POST.getlist('asset', []) # 需要授权资产 + asset_groups_select = request.POST.getlist('asset_group', []) # 需要授权资产组 + roles_select = request.POST.getlist('role', []) # 需要授权角色 + rule_name = request.POST.get('name') + rule_comment = request.POST.get('comment') + + try: + rule = get_object(PermRule, name=rule_name) + + if rule: + raise ServerError(u'授权规则 %s 已存在' % rule_name) + + if not rule_name or not roles_select: + raise ServerError(u'系统用户名称和规则名称不能为空') + + # 获取需要授权的主机列表 + assets_obj = [Asset.objects.get(id=asset_id) for asset_id in assets_select] + asset_groups_obj = [AssetGroup.objects.get(id=group_id) for group_id in asset_groups_select] + group_assets_obj = [] + for asset_group in asset_groups_obj: + group_assets_obj.extend(list(asset_group.asset_set.all())) + calc_assets = set(group_assets_obj) | set(assets_obj) # 授权资产和资产组包含的资产 + + # 获取需要授权的用户列表 + users_obj = [User.objects.get(id=user_id) for user_id in users_select] + user_groups_obj = [UserGroup.objects.get(id=group_id) for group_id in user_groups_select] + + # 获取授予的角色列表 + roles_obj = [PermRole.objects.get(id=role_id) for role_id in roles_select] + need_push_asset = set() + + for role in roles_obj: + asset_no_push = get_role_push_host(role=role)[1] # 获取某角色已经推送的资产 + need_push_asset.update(set(calc_assets) & set(asset_no_push)) + if need_push_asset: + raise ServerError(u'没有推送系统用户 %s 的主机 %s' + % (role.name, ','.join([asset.hostname for asset in need_push_asset]))) + + # 仅授权成功的,写回数据库(授权规则,用户,用户组,资产,资产组,用户角色) + rule = PermRule(name=rule_name, comment=rule_comment) + rule.save() + rule.user = users_obj + rule.user_group = user_groups_obj + rule.asset = assets_obj + rule.asset_group = asset_groups_obj + rule.role = roles_obj + rule.save() + + msg = u"添加授权规则:%s" % rule.name + return HttpResponseRedirect(reverse('rule_list')) + except ServerError, e: + error = e + return my_render('jperm/perm_rule_add.html', locals(), request) -def perm_group_update(user_group_id, asset_groups_id_list): - user_group = UserGroup.objects.filter(id=user_group_id) - if user_group: - user_group = user_group[0] - old_asset_group = [perm.asset_group for perm in user_group.perm_set.all()] - new_asset_group = [] +@require_role('admin') +def perm_rule_edit(request): + """ + edit rule page + """ + # 渲染数据 + header_title, path1, path2 = "授权规则", "规则管理", "添加规则" - for asset_group_id in asset_groups_id_list: - new_asset_group.extend(BisGroup.objects.filter(id=asset_group_id)) + # 根据rule_id 取得rule对象 + rule_id = request.GET.get("id") + rule = get_object(PermRule, id=rule_id) - del_asset_group = [asset_group for asset_group in old_asset_group if asset_group not in new_asset_group] - add_asset_group = [asset_group for asset_group in new_asset_group if asset_group not in old_asset_group] + # 渲染数据, 获取所选的rule对象 - for asset_group in del_asset_group: - Perm.objects.filter(user_group=user_group, asset_group=asset_group).delete() - - for asset_group in add_asset_group: - Perm(user_group=user_group, asset_group=asset_group).save() + users = User.objects.all() + user_groups = UserGroup.objects.all() + assets = Asset.objects.all() + asset_groups = AssetGroup.objects.all() + roles = PermRole.objects.all() + if request.method == 'POST' and rule_id: + # 获取用户选择的 用户,用户组,资产,资产组,用户角色 + rule_name = request.POST.get('name') + rule_comment = request.POST.get("comment") + users_select = request.POST.getlist('user', []) + user_groups_select = request.POST.getlist('user_group', []) + assets_select = request.POST.getlist('asset', []) + asset_groups_select = request.POST.getlist('asset_group', []) + roles_select = request.POST.getlist('role', []) +<<<<<<< HEAD @require_super_user def perm_edit(request): if request.method == 'GET': @@ -278,234 +282,114 @@ def sudo_ldap_add(user_group, user_runas, asset_groups_select, asset_all = False for asset_group in asset_groups_select: assets.extend(asset_group.asset_set.all()) +======= + try: + if not rule_name or not roles_select: + raise ServerError(u'系统用户和关联系统用户不能为空') - if user_group.name == 'ALL': - user_all = True - users = [] - else: - user_all = False - users = user_group.user_set.all() + assets_obj = [Asset.objects.get(id=asset_id) for asset_id in assets_select] + asset_groups_obj = [AssetGroup.objects.get(id=group_id) for group_id in asset_groups_select] + group_assets_obj = [] + for asset_group in asset_groups_obj: + group_assets_obj.extend(list(asset_group.asset_set.all())) + calc_assets = set(group_assets_obj) | set(assets_obj) # 授权资产和资产组包含的资产 - for cmd_group in cmd_groups_select: - cmds.extend(cmd_group.cmd.split(',')) + # 获取需要授权的用户列表 + users_obj = [User.objects.get(id=user_id) for user_id in users_select] + user_groups_obj = [UserGroup.objects.get(id=group_id) for group_id in user_groups_select] - if user_all: - users_name = ['ALL'] - else: - users_name = list(set([user.username for user in users])) + # 获取授予的角色列表 + roles_obj = [PermRole.objects.get(id=role_id) for role_id in roles_select] + need_push_asset = set() + for role in roles_obj: + asset_no_push = get_role_push_host(role=role)[1] # 获取某角色已经推送的资产 + need_push_asset.update(set(calc_assets) & set(asset_no_push)) + if need_push_asset: + raise ServerError(u'没有推送系统用户 %s 的主机 %s' + % (role.name, ','.join([asset.hostname for asset in need_push_asset]))) - if asset_all: - assets_ip = ['ALL'] - else: - assets_ip = list(set([asset.ip for asset in assets])) + # 仅授权成功的,写回数据库(授权规则,用户,用户组,资产,资产组,用户角色) + rule.user = users_obj + rule.user_group = user_groups_obj + rule.asset = assets_obj + rule.asset_group = asset_groups_obj + rule.role = roles_obj + rule.name = rule_name + rule.comment = rule_comment + rule.save() + msg = u"更新授权规则:%s成功" % rule.name +>>>>>>> dev - name = 'sudo%s' % user_group.id - sudo_dn = 'cn=%s,ou=Sudoers,%s' % (name, LDAP_BASE_DN) - sudo_attr = {'objectClass': ['top', 'sudoRole'], - 'cn': ['%s' % name], - 'sudoCommand': unicode2str(cmds), - 'sudoHost': unicode2str(assets_ip), - 'sudoOption': ['!authenticate'], - 'sudoRunAsUser': unicode2str(user_runas), - 'sudoUser': unicode2str(users_name)} - ldap_conn.delete(sudo_dn) - ldap_conn.add(sudo_dn, sudo_attr) + except ServerError, e: + error = e + + return my_render('jperm/perm_rule_edit.html', locals(), request) -def sudo_update(user_group, user_runas, asset_groups_select, cmd_groups_select, comment): - asset_groups_select_list, cmd_groups_select_list = \ - asset_cmd_groups_get(asset_groups_select, cmd_groups_select) - sudo_perm = user_group.sudoperm_set.all() - if sudo_perm: - sudo_perm.update(user_runas=user_runas, comment=comment) - sudo_perm = sudo_perm[0] - sudo_perm.asset_group = asset_groups_select_list - sudo_perm.cmd_group = cmd_groups_select_list - else: - sudo_perm = SudoPerm(user_group=user_group, user_runas=user_runas, comment=comment) - sudo_perm.save() - sudo_perm.asset_group = asset_groups_select_list - sudo_perm.cmd_group = cmd_groups_select_list - - sudo_ldap_add(user_group, user_runas, asset_groups_select_list, cmd_groups_select_list) - - -@require_super_user -def sudo_list(request): - header_title, path1, path2 = u'Sudo授权', u'权限管理', u'Sudo权限详情' - keyword = request.GET.get('search', '') - contact_list = UserGroup.objects.all().order_by('name') - if keyword: - contact_list = contact_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - return render_to_response('jperm/sudo_list.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def sudo_list_adm(request): - header_title, path1, path2 = u'Sudo授权', u'权限管理', u'Sudo权限详情' - keyword = request.GET.get('search', '') - user, dept = get_session_user_dept(request) - contact_list = dept.usergroup_set.all().order_by('name') - if keyword: - contact_list = contact_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - return render_to_response('jperm/sudo_list.html', locals(), context_instance=RequestContext(request)) - - -@require_super_user -def sudo_edit(request): - header_title, path1, path2 = u'Sudo授权', u'授权管理', u'Sudo授权' - - if request.method == 'GET': - user_group_id = request.GET.get('id', '0') - user_group = UserGroup.objects.filter(id=user_group_id) - asset_group_all = BisGroup.objects.filter() - cmd_group_all = CmdGroup.objects.all() - if user_group: - user_group = user_group[0] - sudo_perm = user_group.sudoperm_set.all() - if sudo_perm: - sudo_perm = sudo_perm[0] - asset_group_permed = sudo_perm.asset_group.all() - cmd_group_permed = sudo_perm.cmd_group.all() - user_runas = sudo_perm.user_runas - comment = sudo_perm.comment - else: - asset_group_permed = [] - cmd_group_permed = [] - - asset_groups = [asset_group for asset_group in asset_group_all if asset_group not in asset_group_permed] - cmd_groups = [cmd_group for cmd_group in cmd_group_all if cmd_group not in cmd_group_permed] - - else: - user_group_id = request.POST.get('user_group_id', '') - users_runas = request.POST.get('runas') if request.POST.get('runas') else 'root' - asset_groups_select = request.POST.getlist('asset_groups_select') - cmd_groups_select = request.POST.getlist('cmd_groups_select') - comment = request.POST.get('comment', '') - user_group = UserGroup.objects.filter(id=user_group_id) - if user_group: - user_group = user_group[0] - if LDAP_ENABLE: - sudo_update(user_group, users_runas, asset_groups_select, cmd_groups_select, comment) - msg = '修改成功' - - return HttpResponseRedirect('/jperm/sudo_list/') - - return render_to_response('jperm/sudo_edit.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def sudo_edit_adm(request): - header_title, path1, path2 = u'Sudo授权', u'授权管理', u'Sudo授权' - user, dept = get_session_user_dept(request) - if request.method == 'GET': - user_group_id = request.GET.get('id', '0') - if not validate(request, user_group=[user_group_id]): - return render_to_response('/jperm/sudo_list/') - user_group = UserGroup.objects.filter(id=user_group_id) - asset_group_all = dept.bisgroup_set.all() - cmd_group_all = dept.cmdgroup_set.all() - if user_group: - user_group = user_group[0] - sudo_perm = user_group.sudoperm_set.all() - if sudo_perm: - sudo_perm = sudo_perm[0] - asset_group_permed = sudo_perm.asset_group.all() - cmd_group_permed = sudo_perm.cmd_group.all() - user_runas = sudo_perm.user_runas - comment = sudo_perm.comment - else: - asset_group_permed = [] - cmd_group_permed = [] - - asset_groups = [asset_group for asset_group in asset_group_all if asset_group not in asset_group_permed] - cmd_groups = [cmd_group for cmd_group in cmd_group_all if cmd_group not in cmd_group_permed] - - else: - user_group_id = request.POST.get('user_group_id', '') - users_runas = request.POST.get('runas', 'root') - asset_groups_select = request.POST.getlist('asset_groups_select') - cmd_groups_select = request.POST.getlist('cmd_groups_select') - comment = request.POST.get('comment', '') - user_group = UserGroup.objects.filter(id=user_group_id) - if not validate(request, user_group=[user_group_id], asset_group=asset_groups_select): - return render_to_response('/jperm/sudo_list/') - if user_group: - user_group = user_group[0] - if LDAP_ENABLE: - sudo_update(user_group, users_runas, asset_groups_select, cmd_groups_select, comment) - msg = '修改成功' - - return HttpResponseRedirect('/jperm/sudo_list/') - return render_to_response('jperm/sudo_edit.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def sudo_detail(request): - header_title, path1, path2 = u'Sudo授权详情', u'授权管理', u'授权详情' - user_group_id = request.GET.get('id') - user_group = UserGroup.objects.filter(id=user_group_id) - if user_group: - asset_groups = [] - cmd_groups = [] - user_group = user_group[0] - users = user_group.user_set.all() - group_user_num = len(users) - - for perm in user_group.sudoperm_set.all(): - asset_groups.extend(perm.asset_group.all()) - cmd_groups.extend(perm.cmd_group.all()) - - print asset_groups - return render_to_response('jperm/sudo_detail.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def sudo_refresh(request): - sudo_perm_all = SudoPerm.objects.all() - for sudo_perm in sudo_perm_all: - user_group = sudo_perm.user_group - user_runas = sudo_perm.user_runas - asset_groups_select = sudo_perm.asset_group.all() - cmd_groups_select = sudo_perm.cmd_group.all() - sudo_ldap_add(user_group, user_runas, asset_groups_select, cmd_groups_select) - return HttpResponse('刷新sudo授权成功') - - -@require_super_user -def cmd_add(request): - header_title, path1, path2 = u'sudo命令添加', u'授权管理', u'命令组添加' - dept_all = DEPT.objects.all() - +@require_role('admin') +def perm_rule_delete(request): + """ + use to delete rule + :param request: + :return: + """ if request.method == 'POST': - name = request.POST.get('name') - dept_id = request.POST.get('dept_id') - cmd = ','.join(request.POST.get('cmd').split('\n')) - comment = request.POST.get('comment') - dept = DEPT.objects.filter(id=dept_id) + # 根据rule_id 取得rule对象 + rule_id = request.POST.get("id") + rule_obj = PermRule.objects.get(id=rule_id) + rule_obj.delete() + return HttpResponse(u"删除授权规则:%s" % rule_obj.name) + else: + return HttpResponse(u"不支持该操作") + + +@require_role('admin') +def perm_role_list(request): + """ + list role page + """ + # 渲染数据 + header_title, path1, path2 = "系统用户", "系统用户管理", "查看系统用户" + + # 获取所有系统角色 + roles_list = PermRole.objects.all() + role_id = request.GET.get('id') + # TODO: 搜索和分页 + keyword = request.GET.get('search', '') + if keyword: + roles_list = roles_list.filter(Q(name=keyword)) + + if role_id: + roles_list = roles_list.filter(id=role_id) + + roles_list, p, roles, page_range, current_page, show_first, show_end = pages(roles_list, request) + + return my_render('jperm/perm_role_list.html', locals(), request) + + +@require_role('admin') +def perm_role_add(request): + """ + add role page + """ + # 渲染数据 + header_title, path1, path2 = "系统用户", "系统用户管理", "添加系统用户" + sudos = PermSudo.objects.all() + + if request.method == "POST": + # 获取参数: name, comment + name = request.POST.get("role_name", "") + comment = request.POST.get("role_comment", "") + password = request.POST.get("role_password", "") + key_content = request.POST.get("role_key", "") + sudo_ids = request.POST.getlist('sudo_name') try: - if CmdGroup.objects.filter(name=name): - error = '%s 命令组已存在' - raise ServerError(error) - - if not dept: - error = u"部门不能为空" - raise ServerError(error) - except ServerError, e: - pass - else: - dept = dept[0] - CmdGroup.objects.create(name=name, dept=dept, cmd=cmd, comment=comment) - msg = u'命令组添加成功' - return HttpResponseRedirect('/jperm/cmd_list/') - - return render_to_response('jperm/sudo_cmd_add.html', locals(), context_instance=RequestContext(request)) + if get_object(PermRole, name=name): + raise ServerError('已经存在该用户 %s' % name) + default = get_object(Setting, name='default') +<<<<<<< HEAD @require_admin def cmd_add_adm(request): @@ -564,252 +448,391 @@ def cmd_edit(request): if not cmd_group: error = '没有该命令组' +======= + if password: + encrypt_pass = CRYPTOR.encrypt(password) + else: + encrypt_pass = CRYPTOR.encrypt(CRYPTOR.gen_rand_pass(20)) + # 生成随机密码,生成秘钥对 + sudos_obj = [get_object(PermSudo, id=sudo_id) for sudo_id in sudo_ids] + if key_content: + key_path = gen_keys(key=key_content) + else: + key_path = gen_keys() + logger.debug('generate role key: %s' % key_path) + role = PermRole(name=name, comment=comment, password=encrypt_pass, key_path=key_path) + role.save() + role.sudo = sudos_obj + msg = u"添加系统用户: %s" % name + return HttpResponseRedirect(reverse('role_list')) +>>>>>>> dev except ServerError, e: - pass - else: - cmd_group.update(name=name, cmd=cmd, dept=dept[0], comment=comment) - return HttpResponseRedirect('/jperm/cmd_list/') - return render_to_response('jperm/sudo_cmd_add.html', locals(), context_instance=RequestContext(request)) + error = e + + return my_render('jperm/perm_role_add.html', locals(), request) -@require_admin -def cmd_list(request): - header_title, path1, path2 = u'sudo命令查看', u'权限管理', u'Sudo命令添加' - - if is_super_user(request): - cmd_groups = contact_list = CmdGroup.objects.all() +@require_role('admin') +def perm_role_delete(request): + """ + delete role page + """ + if request.method == "POST": + # 获取参数删除的role对象 + role_id = request.POST.get("id") + role = get_object(PermRole, id=role_id) + role_key = role.key_path + # 删除推送到主机上的role + recycle_assets = [push.asset for push in role.perm_push.all() if push.success] + logger.debug(u"delete role %s - delete_assets: %s" % (role.name, recycle_assets)) + if recycle_assets: + recycle_resource = gen_resource(recycle_assets) + task = MyTask(recycle_resource) + msg = task.del_user(get_object(PermRole, id=role_id).name) + logger.info(u"delete role %s - execute delete user: %s" % (role.name, msg)) + # TODO: 判断返回结果,处理异常 + # 删除存储的秘钥,以及目录 + key_files = os.listdir(role_key) + for key_file in key_files: + os.remove(os.path.join(role_key, key_file)) + os.rmdir(role_key) + logger.info(u"delete role %s - delete role key directory: %s" % (role.name, role_key)) + # 数据库里删除记录 TODO: 判断返回结果,处理异常 + role.delete() + return HttpResponse(u"删除系统用户: %s" % role.name) else: - user, dept = get_session_user_dept(request) - cmd_groups = contact_list = dept.cmdgroup_set.all() - p = paginator = Paginator(contact_list, 10) + return HttpResponse(u"不支持该操作") + + +@require_role('admin') +def perm_role_detail(request): + """ + the role detail page + the role_info data like: + {'asset_groups': [], + 'assets': [], + 'rules': [], + '': [], + '': []} + """ + # 渲染数据 + header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户详情" try: - page = int(request.GET.get('page', '1')) - except ValueError: - page = 1 + if request.method == "GET": + role_id = request.GET.get("id") + if not role_id: + raise ServerError("not role id") + role = get_object(PermRole, id=role_id) + role_info = get_role_info(role_id) - try: - contacts = paginator.page(page) - except (EmptyPage, InvalidPage): - contacts = paginator.page(paginator.num_pages) - return render_to_response('jperm/sudo_cmd_list.html', locals(), context_instance=RequestContext(request)) + # 渲染数据 + rules = role_info.get("rules") + assets = role_info.get("assets") + asset_groups = role_info.get("asset_groups") + users = role_info.get("users") + user_groups = role_info.get("user_groups") + pushed_asset, need_push_asset = get_role_push_host(get_object(PermRole, id=role_id)) + except ServerError, e: + logger.warning(e) + + return my_render('jperm/perm_role_detail.html', locals(), request) -@require_admin -def cmd_del(request): - cmd_group_id = request.GET.get('id') - cmd_group = CmdGroup.objects.filter(id=cmd_group_id) +@require_role('admin') +def perm_role_edit(request): + """ + edit role page + """ + # 渲染数据 + header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户编辑" - if cmd_group: - cmd_group[0].delete() - return HttpResponseRedirect('/jperm/cmd_list/') + # 渲染数据 + role_id = request.GET.get("id") + role = PermRole.objects.get(id=role_id) + role_pass = CRYPTOR.decrypt(role.password) + sudo_all = PermSudo.objects.all() + role_sudos = role.sudo.all() + sudo_all = PermSudo.objects.all() + if request.method == "GET": + return my_render('jperm/perm_role_edit.html', locals(), request) + + if request.method == "POST": + # 获取 POST 数据 + role_name = request.POST.get("role_name") + role_password = request.POST.get("role_password") + role_comment = request.POST.get("role_comment") + role_sudo_names = request.POST.getlist("sudo_name") + role_sudos = [PermSudo.objects.get(id=sudo_id) for sudo_id in role_sudo_names] + key_content = request.POST.get("role_key", "") + + try: + if not role: + raise ServerError('该系统用户不能存在') + + if role_password: + encrypt_pass = CRYPTOR.encrypt(role_password) + role.password = encrypt_pass + # 生成随机密码,生成秘钥对 + if key_content: + try: + key_path = gen_keys(key=key_content, key_path_dir=role.key_path) + except SSHException: + raise ServerError('输入的密钥不合法') + logger.debug('Recreate role key: %s' % role.key_path) + # 写入数据库 + role.name = role_name + role.comment = role_comment + role.sudo = role_sudos + + role.save() + msg = u"更新系统用户: %s" % role.name + return HttpResponseRedirect(reverse('role_list')) + except ServerError, e: + error = e + + return my_render('jperm/perm_role_edit.html', locals(), request) -@require_admin -def cmd_detail(request): - cmd_ids = request.GET.get('id').split(',') - cmds = [] - if len(cmd_ids) == 1: - if cmd_ids[0]: - cmd_id = cmd_ids[0] +@require_role('admin') +def perm_role_push(request): + """ + the role push page + """ + # 渲染数据 + header_title, path1, path2 = "系统用户", "系统用户管理", "系统用户推送" + role_id = request.GET.get('id') + asset_ids = request.GET.get('asset_id') + role = get_object(PermRole, id=role_id) + assets = Asset.objects.all() + asset_groups = AssetGroup.objects.all() + if asset_ids: + need_push_asset = [get_object(Asset, id=asset_id) for asset_id in asset_ids.split(',')] + + if request.method == "POST": + # 获取推荐角色的名称列表 + # 计算出需要推送的资产列表 + asset_ids = request.POST.getlist("assets") + asset_group_ids = request.POST.getlist("asset_groups") + assets_obj = [Asset.objects.get(id=asset_id) for asset_id in asset_ids] + asset_groups_obj = [AssetGroup.objects.get(id=asset_group_id) for asset_group_id in asset_group_ids] + group_assets_obj = [] + for asset_group in asset_groups_obj: + group_assets_obj.extend(asset_group.asset_set.all()) + calc_assets = list(set(assets_obj) | set(group_assets_obj)) + push_resource = gen_resource(calc_assets) + + # 调用Ansible API 进行推送 + password_push = True if request.POST.get("use_password") else False + key_push = True if request.POST.get("use_publicKey") else False + task = MyTask(push_resource) + ret = {} + + # 因为要先建立用户,所以password 是必选项,而push key是在 password也完成的情况下的 可选项 + # 1. 以秘钥 方式推送角色 + if key_push: + ret["pass_push"] = task.add_user(role.name, CRYPTOR.decrypt(role.password)) + ret["key_push"] = task.push_key(role.name, os.path.join(role.key_path, 'id_rsa.pub')) + + # 2. 推送账号密码 + elif password_push: + ret["pass_push"] = task.add_user(role.name, CRYPTOR.decrypt(role.password)) + + # 3. 推送sudo配置文件 + if password_push or key_push: + sudo_list = set([sudo for sudo in role.sudo.all()]) # set(sudo1, sudo2, sudo3) + if sudo_list: + ret['sudo'] = task.push_sudo_file([role], sudo_list) + + logger.debug('推送role结果: %s' % ret) + success_asset = {} + failed_asset = {} + logger.debug(ret) + for push_type, result in ret.items(): + if result.get('failed'): + for hostname, info in result.get('failed').items(): + if hostname in failed_asset.keys(): + if info in failed_asset.get(hostname): + failed_asset[hostname] += info + else: + failed_asset[hostname] = info + + for push_type, result in ret.items(): + if result.get('ok'): + for hostname, info in result.get('ok').items(): + if hostname in failed_asset.keys(): + continue + elif hostname in success_asset.keys(): + if str(info) in success_asset.get(hostname, ''): + success_asset[hostname] += str(info) + else: + success_asset[hostname] = str(info) + + # 推送成功 回写push表 + for asset in calc_assets: + push_check = PermPush.objects.filter(role=role, asset=asset) + if push_check: + func = push_check.update + else: + def func(**kwargs): + PermPush(**kwargs).save() + + if failed_asset.get(asset.hostname): + func(is_password=password_push, is_public_key=key_push, role=role, asset=asset, success=False, + result=failed_asset.get(asset.hostname)) + else: + func(is_password=password_push, is_public_key=key_push, role=role, asset=asset, success=True) + + if not failed_asset: + msg = u'系统用户 %s 推送成功[ %s ]' % (role.name, ','.join(success_asset.keys())) else: - cmd_id = 1 - cmd_group = CmdGroup.objects.filter(id=cmd_id) - if cmd_group: - cmd_group = cmd_group[0] - cmds.extend(cmd_group.cmd.split(',')) - cmd_group_name = cmd_group.name + error = u'系统用户 %s 推送失败 [ %s ], 推送成功 [ %s ]' % (role.name, + ','.join(failed_asset.keys()), + ','.join(success_asset.keys())) + return my_render('jperm/perm_role_push.html', locals(), request) + + +@require_role('admin') +def perm_sudo_list(request): + """ + list sudo commands alias + :param request: + :return: + """ + # 渲染数据 + header_title, path1, path2 = "Sudo命令", "别名管理", "查看别名" + + # 获取所有sudo 命令别名 + sudos_list = PermSudo.objects.all() + + # TODO: 搜索和分页 + keyword = request.GET.get('search', '') + if keyword: + sudos_list = sudos_list.filter(Q(name=keyword)) + + sudos_list, p, sudos, page_range, current_page, show_first, show_end = pages(sudos_list, request) + + return my_render('jperm/perm_sudo_list.html', locals(), request) + + +@require_role('admin') +def perm_sudo_add(request): + """ + list sudo commands alias + :param request: + :return: + """ + # 渲染数据 + header_title, path1, path2 = "Sudo命令", "别名管理", "添加别名" + + if request.method == "POST": + # 获取参数: name, comment + name = request.POST.get("sudo_name").strip().upper() + comment = request.POST.get("sudo_comment").strip() + commands = request.POST.get("sudo_commands").strip() + + pattern = re.compile(r'[ \n,\r]') + commands = ', '.join(list_drop_str(pattern.split(commands), u'')) + logger.debug(u'添加sudo %s: %s' % (name, commands)) + + if get_object(PermSudo, name=name): + error = 'Sudo别名 %s已经存在' % name + else: + sudo = PermSudo(name=name.strip(), comment=comment, commands=commands) + sudo.save() + msg = u"添加Sudo命令别名: %s" % name + # 渲染数据 + + return my_render('jperm/perm_sudo_add.html', locals(), request) + + +@require_role('admin') +def perm_sudo_edit(request): + """ + list sudo commands alias + :param request: + :return: + """ + # 渲染数据 + header_title, path1, path2 = "Sudo命令", "别名管理", "编辑别名" + + sudo_id = request.GET.get("id") + sudo = PermSudo.objects.get(id=sudo_id) + + if request.method == "POST": + name = request.POST.get("sudo_name").upper() + commands = request.POST.get("sudo_commands") + comment = request.POST.get("sudo_comment") + + pattern = re.compile(r'[ \n,\r]') + commands = ', '.join(list_drop_str(pattern.split(commands), u'')).strip() + logger.debug(u'添加sudo %s: %s' % (name, commands)) + + sudo.name = name.strip() + sudo.commands = commands + sudo.comment = comment + sudo.save() + + msg = u"更新命令别名: %s" % name + + return my_render('jperm/perm_sudo_edit.html', locals(), request) + + +@require_role('admin') +def perm_sudo_delete(request): + """ + list sudo commands alias + :param request: + :return: + """ + if request.method == "POST": + # 获取参数删除的role对象 + sudo_id = request.POST.get("id") + sudo = PermSudo.objects.get(id=sudo_id) + # 数据库里删除记录 + sudo.delete() + return HttpResponse(u"删除系统用户: %s" % sudo.name) else: - cmd_groups = [] - for cmd_id in cmd_ids: - cmd_groups.extend(CmdGroup.objects.filter(id=cmd_id)) - for cmd_group in cmd_groups: - cmds.extend(cmd_group.cmd.split(',')) - - cmds_str = ', '.join(cmds) - - return render_to_response('jperm/sudo_cmd_detail.html', locals(), context_instance=RequestContext(request)) + return HttpResponse(u"不支持该操作") -@require_login -def perm_apply(request): - """ 权限申请 """ - header_title, path1, path2 = u'主机权限申请', u'权限管理', u'申请主机' - user_id, username = get_session_user_info(request)[0:2] - name = User.objects.get(id=user_id).username - dept_id, deptname, dept = get_session_user_info(request)[3:6] - perm_host = user_perm_asset_api(username) - all_host = Asset.objects.filter(dept=dept) +@require_role('admin') +def perm_role_recycle(request): + role_id = request.GET.get('role_id') + asset_ids = request.GET.get('asset_id').split(',') - perm_group = user_perm_group_api(username) - all_group = dept.bisgroup_set.all() + # 仅有推送的角色才回收 + assets = [get_object(Asset, id=asset_id) for asset_id in asset_ids] + recycle_assets = [] + for asset in assets: + if True in [push.success for push in asset.perm_push.all()]: + recycle_assets.append(asset) + recycle_resource = gen_resource(recycle_assets) + task = MyTask(recycle_resource) + # TODO: 判断返回结果,处理异常 + msg = task.del_user(get_object(PermRole, id=role_id).name) - posts = [g for g in all_host if g not in perm_host] - egroup = [d for d in all_group if d not in perm_group] + for asset_id in asset_ids: + asset = get_object(Asset, id=asset_id) + assets.append(asset) + role = get_object(PermRole, id=role_id) + PermPush.objects.filter(asset=asset, role=role).delete() - dept_da = User.objects.filter(dept_id=dept_id, role='DA') - admin = User.objects.get(name='admin') - - if request.method == 'POST': - applyer = request.POST.get('applyer') - dept = request.POST.get('dept') - da = request.POST.get('da') - group = request.POST.getlist('group') - hosts = request.POST.getlist('hosts') - comment = request.POST.get('comment') - if not da: - return httperror(request, u'请选择管理员!') - da = User.objects.get(id=da) - mail_address = da.email - mail_title = '%s - 权限申请' % username - group_lis = ', '.join(group) - hosts_lis = ', '.join(hosts) - time_now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - a = Apply.objects.create(applyer=applyer, admin=da, dept=dept, bisgroup=group, date_add=datetime.datetime.now(), - asset=hosts, status=0, comment=comment, read=0) - uuid = a.uuid - url = "http://%s:%s/jperm/apply_exec/?uuid=%s" % (SEND_IP, SEND_PORT, uuid) - mail_msg = """ - Hi,%s: - 有新的权限申请, 详情如下: - 申请人: %s - 申请主机组: %s - 申请的主机: %s - 申请时间: %s - 申请说明: %s - 请及时审批, 审批完成后, 点击以下链接或登录授权管理-权限审批页面点击确认键,告知申请人。 - - %s - """ % (da.username, applyer, group_lis, hosts_lis, time_now, comment, url) - - send_mail(mail_title, mail_msg, MAIL_FROM, [mail_address], fail_silently=False) - smg = "提交成功,已发邮件至 %s 通知部门管理员。" % mail_address - return render_to_response('jperm/perm_apply.html', locals(), context_instance=RequestContext(request)) - return render_to_response('jperm/perm_apply.html', locals(), context_instance=RequestContext(request)) + return HttpResponse('删除成功') -@require_admin -def perm_apply_exec(request): - """ 确认权限 """ - header_title, path1, path2 = u'主机权限申请', u'权限管理', u'审批完成' - uuid = request.GET.get('uuid') - user_id = request.session.get('user_id') - approver = User.objects.get(id=user_id).name - if uuid: - p_apply = Apply.objects.filter(uuid=str(uuid)) - q_apply = Apply.objects.get(uuid=str(uuid)) - if q_apply.status == 1: - smg = '此权限已经审批完成, 请勿重复审批, 十秒钟后返回首页' - return render_to_response('jperm/perm_apply_exec.html', locals(), context_instance=RequestContext(request)) - else: - user = User.objects.get(username=q_apply.applyer) - mail_address = user.email - time_now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') - p_apply.update(status=1, approver=approver, date_end=time_now) - mail_title = '%s - 权限审批完成' % q_apply.applyer - mail_msg = """ - Hi,%s: - 您所申请的权限已由 %s 在 %s 审批完成, 请登录验证。 - """ % (q_apply.applyer, q_apply.approver, time_now) - send_mail(mail_title, mail_msg, MAIL_FROM, [mail_address], fail_silently=False) - smg = '授权完成, 已邮件通知申请人, 十秒钟后返回首页' - return render_to_response('jperm/perm_apply_exec.html', locals(), context_instance=RequestContext(request)) +@require_role('user') +def perm_role_get(request): + asset_id = request.GET.get('id', 0) + if asset_id: + asset = get_object(Asset, id=asset_id) + if asset: + role = user_have_perm(request.user, asset=asset) + logger.debug('#' + ','.join([i.name for i in role]) + '#') + return HttpResponse(','.join([i.name for i in role])) else: - smg = '没有此授权记录, 十秒钟后返回首页' - return render_to_response('jperm/perm_apply_exec.html', locals(), context_instance=RequestContext(request)) - - -def get_apply_posts(request, status, username, dept_name, keyword=None): - """ 获取申请记录 """ - post_all = Apply.objects.filter(status=status).order_by('-date_add') - post_keyword_all = Apply.objects.filter(Q(applyer__contains=keyword) | - Q(approver__contains=keyword)) \ - .filter(status=status).order_by('-date_add') - - if is_super_user(request): - if keyword: - posts = post_keyword_all - else: - posts = post_all - elif is_group_admin(request): - if keyword: - posts = post_keyword_all.filter(dept=dept_name) - else: - posts = post_all.filter(dept=dept_name) - elif is_common_user(request): - if keyword: - posts = post_keyword_all.filter(applyer=username) - else: - posts = post_all.filter(applyer=username) - - return posts - - -@require_login -def perm_apply_log(request, offset): - """ 申请记录 """ - header_title, path1, path2 = u'权限申请记录', u'权限管理', u'申请记录' - keyword = request.GET.get('keyword', '') - user_id = get_session_user_info(request)[0] - username = User.objects.get(id=user_id).name - dept_name = get_session_user_info(request)[4] - status_dic = {'online': 0, 'offline': 1} - status = status_dic[offset] - posts = get_apply_posts(request, status, username, dept_name, keyword) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return render_to_response('jperm/perm_log_%s.html' % offset, locals(), context_instance=RequestContext(request)) - - -@require_login -def perm_apply_info(request): - """ 申请信息详情 """ - uuid = request.GET.get('uuid', '') - post = Apply.objects.filter(uuid=uuid) - username = get_session_user_info(request)[1] - if post: - post = post[0] - if post.read == 0 and post.applyer != username: - post.read = 1 - post.save() - else: - return httperror(request, u'没有这个申请记录!') - - return render_to_response('jperm/perm_apply_info.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def perm_apply_del(request): - """ 删除日志记录 """ - uuid = request.GET.get('uuid') - u_apply = Apply.objects.filter(uuid=uuid) - if u_apply: - u_apply.delete() - return HttpResponseRedirect('/jperm/apply_show/online/') - - -@require_login -def perm_apply_search(request): - """ 申请搜索 """ - keyword = request.GET.get('keyword') - offset = request.GET.get('env') - username = get_session_user_info(request)[1] - dept_name = get_session_user_info(request)[3] - status_dic = {'online': 0, 'offline': 1} - status = status_dic[offset] - posts = get_apply_posts(request, status, username, dept_name, keyword) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(posts, request) - return render_to_response('jperm/perm_apply_search.html', locals(), context_instance=RequestContext(request)) - - - - - - - - - - - - + roles = get_group_user_perm(request.user).get('role').keys() + return HttpResponse(','.join(i.name for i in roles)) + return HttpResponse('error') diff --git a/jumpserver.conf b/jumpserver.conf index 49f135076..fe8099605 100644 --- a/jumpserver.conf +++ b/jumpserver.conf @@ -1,10 +1,7 @@ -#coding: utf8 - [base] -ip = 192.168.20.209 -port = 80 +url = http://192.168.244.129 key = 88aaaf7ffe3c6c04 - +log = debug [db] host = 127.0.0.1 @@ -13,22 +10,20 @@ user = jumpserver password = mysql234 database = jumpserver - -[ldap] -ldap_enable = 1 -host_url = ldap://127.0.0.1:389 -base_dn = dc=jumpserver, dc=org -root_dn = cn=admin,dc=jumpserver,dc=org -root_pw = secret234 - - [websocket] -web_socket_host = 192.168.40.140:3000 - +web_socket_host = 192.168.244.129:3000 [mail] +mail_enable = 1 email_host = smtp.qq.com email_port = 25 +<<<<<<< HEAD email_host_user = 1152704203@qq.com email_host_password = Hudie117... email_use_tls = False +======= +email_host_user = ibuler@qq.com +email_host_password = Hudie117...qq +email_use_tls = True + +>>>>>>> dev diff --git a/jumpserver/api.py b/jumpserver/api.py index 4a3a6bde7..747cbce00 100644 --- a/jumpserver/api.py +++ b/jumpserver/api.py @@ -1,27 +1,31 @@ # coding: utf-8 - -from django.http import HttpResponseRedirect -import json -import os -from ConfigParser import ConfigParser -import getpass +import os, sys, time, re from Crypto.Cipher import AES +import crypt +import pwd from binascii import b2a_hex, a2b_hex -import ldap -from ldap import modlist import hashlib import datetime +import random import subprocess +import uuid +import json +import logging + +from settings import * from django.core.paginator import Paginator, EmptyPage, InvalidPage from django.http import HttpResponse, Http404 +from django.template import RequestContext +from juser.models import User, UserGroup +from jlog.models import Log, TtyLog +from jasset.models import Asset, AssetGroup +from jperm.models import PermRule, PermRole +from jumpserver.models import Setting +from django.http import HttpResponseRedirect from django.shortcuts import render_to_response -from juser.models import User, UserGroup, DEPT -from jasset.models import Asset, BisGroup, IDC -from jlog.models import Log -from jasset.models import AssetAlias -from django.core.exceptions import ObjectDoesNotExist from django.core.mail import send_mail +<<<<<<< HEAD import json @@ -105,64 +109,208 @@ if LDAP_ENABLE: def md5_crypt(string): return hashlib.new("md5", string).hexdigest() +======= +from django.core.urlresolvers import reverse + + +def set_log(level, filename='jumpserver.log'): + """ + return a log file object + 根据提示设置log打印 + """ + log_file = os.path.join(LOG_DIR, filename) + if not os.path.isfile(log_file): + os.mknod(log_file) + os.chmod(log_file, 0777) + log_level_total = {'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARN, 'error': logging.ERROR, + 'critical': logging.CRITICAL} + logger_f = logging.getLogger('jumpserver') + logger_f.setLevel(logging.DEBUG) + fh = logging.FileHandler(log_file) + fh.setLevel(log_level_total.get(level, logging.DEBUG)) + formatter = logging.Formatter('%(asctime)s - %(filename)s - %(levelname)s - %(message)s') + fh.setFormatter(formatter) + logger_f.addHandler(fh) + return logger_f + + +def list_drop_str(a_list, a_str): + for i in a_list: + if i == a_str: + a_list.remove(a_str) + return a_list + + +def get_asset_info(asset): + """ + 获取资产的相关管理账号端口等信息 + """ + default = get_object(Setting, name='default') + info = {'hostname': asset.hostname, 'ip': asset.ip} + if asset.use_default_auth: + if default: + info['port'] = int(default.field2) + info['username'] = default.field1 + try: + info['password'] = CRYPTOR.decrypt(default.field3) + except ServerError: + pass + if os.path.isfile(default.field4): + info['ssh_key'] = default.field4 + else: + info['port'] = int(asset.port) + info['username'] = asset.username + info['password'] = CRYPTOR.decrypt(asset.password) + + return info + + +def get_role_key(user, role): + """ + 由于role的key的权限是所有人可以读的, ansible执行命令等要求为600,所以拷贝一份到特殊目录 + :param user: + :param role: + :return: self key path + """ + user_role_key_dir = os.path.join(KEY_DIR, 'user') + user_role_key_path = os.path.join(user_role_key_dir, '%s_%s.pem' % (user.username, role.name)) + mkdir(user_role_key_dir, mode=0777) + if not os.path.isfile(user_role_key_path): + with open(os.path.join(role.key_path, 'id_rsa')) as fk: + with open(user_role_key_path, 'w') as fu: + fu.write(fk.read()) + logger.debug(u"创建新的系统用户key %s, Owner: %s" % (user_role_key_path, user.username)) + chown(user_role_key_path, user.username) + os.chmod(user_role_key_path, 0600) + return user_role_key_path + + +def chown(path, user, group=''): + if not group: + group = user + try: + uid = pwd.getpwnam(user).pw_uid + gid = pwd.getpwnam(group).pw_gid + os.chown(path, uid, gid) + except KeyError: + pass +>>>>>>> dev def page_list_return(total, current=1): + """ + page + 分页,返回本次分页的最小页数到最大页数列表 + """ min_page = current - 2 if current - 4 > 0 else 1 max_page = min_page + 4 if min_page + 4 < total else total - return range(min_page, max_page+1) + return range(min_page, max_page + 1) -def pages(posts, r): - """分页公用函数""" - contact_list = posts - p = paginator = Paginator(contact_list, 10) +def pages(post_objects, request): + """ + page public function , return page's object tuple + 分页公用函数,返回分页的对象元组 + """ + paginator = Paginator(post_objects, 20) try: - current_page = int(r.GET.get('page', '1')) + current_page = int(request.GET.get('page', '1')) except ValueError: current_page = 1 - page_range = page_list_return(len(p.page_range), current_page) + page_range = page_list_return(len(paginator.page_range), current_page) try: - contacts = paginator.page(current_page) + page_objects = paginator.page(current_page) except (EmptyPage, InvalidPage): - contacts = paginator.page(paginator.num_pages) + page_objects = paginator.page(paginator.num_pages) if current_page >= 5: show_first = 1 else: show_first = 0 - if current_page <= (len(p.page_range) - 3): + + if current_page <= (len(paginator.page_range) - 3): show_end = 1 else: show_end = 0 - return contact_list, p, contacts, page_range, current_page, show_first, show_end + # 所有对象, 分页器, 本页对象, 所有页码, 本页页码,是否显示第一页,是否显示最后一页 + return post_objects, paginator, page_objects, page_range, current_page, show_first, show_end class PyCrypt(object): - """This class used to encrypt and decrypt password.""" + """ + This class used to encrypt and decrypt password. + 加密类 + """ def __init__(self, key): self.key = key self.mode = AES.MODE_CBC - def encrypt(self, text): - cryptor = AES.new(self.key, self.mode, b'0000000000000000') - length = 16 + @staticmethod + def gen_rand_pass(length, especial=False): + """ + random password + 随机生成密码 + """ + salt_key = '1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_' + symbol = '!@$%^&*()_' + salt_list = [] + if especial: + for i in range(length - 4): + salt_list.append(random.choice(salt_key)) + for i in range(4): + salt_list.append(random.choice(symbol)) + else: + for i in range(length): + salt_list.append(random.choice(salt_key)) + salt = ''.join(salt_list) + return salt + + @staticmethod + def md5_crypt(string): + """ + md5 encrypt method + md5非对称加密方法 + """ + return hashlib.new("md5", string).hexdigest() + + @staticmethod + def gen_sha512(salt, password): + """ + generate sha512 format password + 生成sha512加密密码 + """ + return crypt.crypt(password, '$6$%s$' % salt) + + def encrypt(self, passwd=None, length=32): + """ + encrypt gen password + 对称加密之加密生成密码 + """ + if not passwd: + passwd = self.gen_rand_pass() + + cryptor = AES.new(self.key, self.mode, b'8122ca7d906ad5e1') try: - count = len(text) + count = len(passwd) except TypeError: raise ServerError('Encrypt password error, TYpe error.') + add = (length - (count % length)) - text += ('\0' * add) - ciphertext = cryptor.encrypt(text) - return b2a_hex(ciphertext) + passwd += ('\0' * add) + cipher_text = cryptor.encrypt(passwd) + return b2a_hex(cipher_text) def decrypt(self, text): - cryptor = AES.new(self.key, self.mode, b'0000000000000000') + """ + decrypt pass base the same key + 对称加密之解密,同一个加密随机数 + """ + cryptor = AES.new(self.key, self.mode, b'8122ca7d906ad5e1') try: plain_text = cryptor.decrypt(a2b_hex(text)) except TypeError: @@ -170,95 +318,103 @@ class PyCrypt(object): return plain_text.rstrip('\0') -CRYPTOR = PyCrypt(KEY) - - class ServerError(Exception): + """ + self define exception + 自定义异常 + """ pass def get_object(model, **kwargs): - try: - the_object = model.objects.get(**kwargs) - except ObjectDoesNotExist: - raise ServerError('Object get %s failed.' % str(kwargs.values())) + """ + use this function for query + 使用改封装函数查询数据库 + """ + for value in kwargs.values(): + if not value: + return None + + the_object = model.objects.filter(**kwargs) + if len(the_object) == 1: + the_object = the_object[0] + else: + the_object = None return the_object -def require_login(func): - """要求登录的装饰器""" - def _deco(request, *args, **kwargs): - if not request.session.get('user_id'): - return HttpResponseRedirect('/login/') - return func(request, *args, **kwargs) +def require_role(role='user'): + """ + decorator for require user role in ["super", "admin", "user"] + 要求用户是某种角色 ["super", "admin", "user"]的装饰器 + """ + + def _deco(func): + def __deco(request, *args, **kwargs): + request.session['pre_url'] = request.path + if not request.user.is_authenticated(): + return HttpResponseRedirect(reverse('login')) + if role == 'admin': + # if request.session.get('role_id', 0) < 1: + if request.user.role == 'CU': + return HttpResponseRedirect(reverse('index')) + elif role == 'super': + # if request.session.get('role_id', 0) < 2: + if request.user.role in ['CU', 'GA']: + return HttpResponseRedirect(reverse('index')) + return func(request, *args, **kwargs) + + return __deco + return _deco -def require_super_user(func): - def _deco(request, *args, **kwargs): - if not request.session.get('user_id'): - return HttpResponseRedirect('/login/') - - if request.session.get('role_id', 0) != 2: - return HttpResponseRedirect('/') - return func(request, *args, **kwargs) - return _deco - - -def require_admin(func): - def _deco(request, *args, **kwargs): - if not request.session.get('user_id'): - return HttpResponseRedirect('/login/') - - if request.session.get('role_id', 0) < 1: - return HttpResponseRedirect('/') - return func(request, *args, **kwargs) - return _deco - - -def is_super_user(request): - if request.session.get('role_id') == 2: +def is_role_request(request, role='user'): + """ + require this request of user is right + 要求请求角色正确 + """ + role_all = {'user': 'CU', 'admin': 'GA', 'super': 'SU'} + if request.user.role == role_all.get(role, 'CU'): return True else: return False -def is_group_admin(request): - if request.session.get('role_id') == 1: - return True - else: - return False - - -def is_common_user(request): - if request.session.get('role_id') == 0: - return True - else: - return False - - -@require_login def get_session_user_dept(request): - user_id = request.session.get('user_id', 0) - user = User.objects.filter(id=user_id) - if user: - user = user[0] - dept = user.dept - return user, dept + """ + get department of the user in session + 获取session中用户的部门 + """ + # user_id = request.session.get('user_id', 0) + # print '#' * 20 + # print user_id + # user = User.objects.filter(id=user_id) + # if user: + # user = user[0] + # return user, None + return request.user, None -@require_login +@require_role def get_session_user_info(request): - user_id = request.session.get('user_id', 0) - user = User.objects.filter(id=user_id) - if user: - user = user[0] - dept = user.dept - return [user.id, user.username, user, dept.id, dept.name, dept] + """ + get the user info of the user in session, for example id, username etc. + 获取用户的信息 + """ + # user_id = request.session.get('user_id', 0) + # user = get_object(User, id=user_id) + # if user: + # return [user.id, user.username, user] + return [request.user.id, request.user.username, request.user] def get_user_dept(request): - user_id = request.session.get('user_id') + """ + get the user dept id + 获取用户的部门id + """ + user_id = request.user.id if user_id: user_dept = User.objects.get(id=user_id).dept return user_dept.id @@ -273,129 +429,28 @@ def api_user(request): def view_splitter(request, su=None, adm=None): - if is_super_user(request): + """ + for different user use different view + 视图分页器 + """ + if is_role_request(request, 'super'): return su(request) - elif is_group_admin(request): + elif is_role_request(request, 'admin'): return adm(request) else: - return HttpResponseRedirect('/login/') - - -def user_group_perm_asset_group_api(user_group): - asset_group_list = [] - perm_list = user_group.perm_set.all() - for perm in perm_list: - asset_group_list.append(perm.asset_group) - return asset_group_list - - -def user_perm_group_api(username): - if username: - user = User.objects.get(username=username) - perm_list = [] - user_group_all = user.group.all() - for user_group in user_group_all: - perm_list.extend(user_group.perm_set.all()) - - asset_group_list = [] - for perm in perm_list: - asset_group_list.append(perm.asset_group) - return asset_group_list - - -def user_perm_group_hosts_api(gid): - hostgroup = BisGroup.objects.filter(id=gid) - if hostgroup: - return hostgroup[0].asset_set.all() - else: - return [] - - -def user_perm_asset_api(username): - user = User.objects.filter(username=username) - if user: - user = user[0] - asset_list = [] - asset_group_list = user_perm_group_api(user) - for asset_group in asset_group_list: - asset_list.extend(asset_group.asset_set.all()) - asset_list = list(set(asset_list)) - return asset_list - else: - return [] - - -def asset_perm_api(asset): - if asset: - perm_list = [] - asset_group_all = asset.bis_group.all() - for asset_group in asset_group_all: - perm_list.extend(asset_group.perm_set.all()) - - user_group_list = [] - for perm in perm_list: - user_group_list.append(perm.user_group) - - user_permed_list = [] - for user_group in user_group_list: - user_permed_list.extend(user_group.user_set.all()) - user_permed_list = list(set(user_permed_list)) - return user_permed_list - - -def get_user_host(username): - """Get the hosts of under the user control.""" - hosts_attr = {} - asset_all = user_perm_asset_api(username) - user = User.objects.filter(username=username) - if user: - user = user[0] - for asset in asset_all: - alias = AssetAlias.objects.filter(user=user, host=asset) - if alias and alias[0].alias != '': - hosts_attr[asset.ip] = [asset.id, asset.ip, alias[0].alias] - else: - hosts_attr[asset.ip] = [asset.id, asset.ip, asset.comment] - return hosts_attr - else: - raise ServerError('User %s does not exit!' % username) - - -def get_connect_item(username, ip): - asset = get_object(Asset, ip=ip) - port = int(asset.port) - - if not asset.is_active: - raise ServerError('Host %s is not active.' % ip) - - user = get_object(User, username=username) - - if not user.is_active: - raise ServerError('User %s is not active.' % username) - - login_type_dict = { - 'L': user.ldap_pwd, - } - - if asset.login_type in login_type_dict: - password = CRYPTOR.decrypt(login_type_dict[asset.login_type]) - return username, password, ip, port - - elif asset.login_type == 'M': - username = asset.username - password = CRYPTOR.decrypt(asset.password) - return username, password, ip, port - - else: - raise ServerError('Login type is not in ["L", "M"]') + return HttpResponseRedirect(reverse('login')) def validate(request, user_group=None, user=None, asset_group=None, asset=None, edept=None): + """ + validate the user request + 判定用户请求是否合法 + """ dept = get_session_user_dept(request)[1] if edept: if dept.id != int(edept[0]): return False - + if user_group: dept_user_groups = dept.usergroup_set.all() user_group_ids = [] @@ -480,39 +535,60 @@ def verify(request, user_group=None, user=None, asset_group=None, asset=None, ed def bash(cmd): - """执行bash命令""" + """ + run a bash shell command + 执行bash命令 + """ return subprocess.call(cmd, shell=True) -def is_dir(dir_name, username='root', mode=0755): +def mkdir(dir_name, username='', mode=0755): + """ + insure the dir exist and mode ok + 目录存在,如果不存在就建立,并且权限正确 + """ if not os.path.isdir(dir_name): os.makedirs(dir_name) - bash("chown %s:%s '%s'" % (username, username, dir_name)) - os.chmod(dir_name, mode) + os.chmod(dir_name, mode) + if username: + chown(dir_name, username) -def success(request, msg): +def http_success(request, msg): return render_to_response('success.html', locals()) -def httperror(request, emg): +def http_error(request, emg): message = emg return render_to_response('error.html', locals()) -def node_auth(request): - username = request.POST.get('username', ' ') - seed = request.POST.get('seed', ' ') - filename = request.POST.get('filename', ' ') - user = User.objects.filter(username=username, password=seed) - auth = 1 - if not user: - auth = 0 - if not filename.startswith('/opt/jumpserver/logs/connect/'): - auth = 0 - if auth: - result = {'auth': {'username': username, 'result': 'success'}} - else: - result = {'auth': {'username': username, 'result': 'failed'}} +def my_render(template, data, request): + return render_to_response(template, data, context_instance=RequestContext(request)) - return HttpResponse(json.dumps(result, sort_keys=True, indent=2), content_type='application/json') \ No newline at end of file + +def get_tmp_dir(): + dir_name = os.path.join('/tmp', uuid.uuid4().hex) + mkdir(dir_name, mode=0777) + return dir_name + + +def defend_attack(func): + def _deco(request, *args, **kwargs): + if int(request.session.get('visit', 1)) > 10: + logger.debug('请求次数: %s' % request.session.get('visit', 1)) + return HttpResponse('Forbidden', status=403) + request.session['visit'] = request.session.get('visit', 1) + 1 + request.session.set_expiry(300) + return func(request, *args, **kwargs) + return _deco + + +def get_mac_address(): + node = uuid.getnode() + mac = uuid.UUID(int=node).hex[-12:] + return mac + + +CRYPTOR = PyCrypt(KEY) +logger = set_log(LOG_LEVEL) diff --git a/jumpserver/context_processors.py b/jumpserver/context_processors.py index aac09c7a7..e84cc60ec 100644 --- a/jumpserver/context_processors.py +++ b/jumpserver/context_processors.py @@ -1,26 +1,16 @@ from juser.models import User from jasset.models import Asset from jumpserver.api import * -from jperm.models import Apply def name_proc(request): - user_id = request.session.get('user_id') - role_id = request.session.get('role_id') - if role_id == 2: - user_total_num = User.objects.all().count() - user_active_num = User.objects.filter().count() - host_total_num = Asset.objects.all().count() - host_active_num = Asset.objects.filter(is_active=True).count() - else: - user, dept = get_session_user_dept(request) - user_total_num = dept.user_set.all().count() - user_active_num = dept.user_set.filter(is_active=True).count() - host_total_num = dept.asset_set.all().count() - host_active_num = dept.asset_set.all().filter(is_active=True).count() - - username = User.objects.get(id=user_id).name - apply_info = Apply.objects.filter(admin=username, status=0, read=0) + user_id = request.user.id + role_id = {'SU': 2, 'GA': 1, 'CU': 0}.get(request.user.role, 0) + # role_id = 'SU' + user_total_num = User.objects.all().count() + user_active_num = User.objects.filter().count() + host_total_num = Asset.objects.all().count() + host_active_num = Asset.objects.filter(is_active=True).count() request.session.set_expiry(3600) info_dic = {'session_user_id': user_id, @@ -29,7 +19,7 @@ def name_proc(request): 'user_active_num': user_active_num, 'host_total_num': host_total_num, 'host_active_num': host_active_num, - 'apply_info': apply_info} + } return info_dic diff --git a/jumpserver/settings.py b/jumpserver/settings.py index ce4d7e8b5..fa8431272 100644 --- a/jumpserver/settings.py +++ b/jumpserver/settings.py @@ -11,24 +11,37 @@ https://docs.djangoproject.com/en/1.7/ref/settings/ # Build paths inside the project like this: os.path.join(BASE_DIR, ...) import os import ConfigParser +import getpass config = ConfigParser.ConfigParser() -BASE_DIR = os.path.dirname(os.path.dirname(__file__)) +BASE_DIR = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) config.read(os.path.join(BASE_DIR, 'jumpserver.conf')) +KEY_DIR = os.path.join(BASE_DIR, 'keys') + DB_HOST = config.get('db', 'host') DB_PORT = config.getint('db', 'port') DB_USER = config.get('db', 'user') DB_PASSWORD = config.get('db', 'password') DB_DATABASE = config.get('db', 'database') - +AUTH_USER_MODEL = 'juser.User' # mail config +MAIL_ENABLE = config.get('mail', 'mail_enable') EMAIL_HOST = config.get('mail', 'email_host') EMAIL_PORT = config.get('mail', 'email_port') EMAIL_HOST_USER = config.get('mail', 'email_host_user') EMAIL_HOST_PASSWORD = config.get('mail', 'email_host_password') EMAIL_USE_TLS = config.getboolean('mail', 'email_use_tls') +EMAIL_TIMEOUT = 5 + +# ======== Log ========== +LOG_DIR = os.path.join(BASE_DIR, 'logs') +SSH_KEY_DIR = os.path.join(BASE_DIR, 'keys/role_keys') +KEY = config.get('base', 'key') +URL = config.get('base', 'url') +LOG_LEVEL = config.get('base', 'log') +WEB_SOCKET_HOST = config.get('websocket', 'web_socket_host') # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.7/howto/deployment/checklist/ @@ -43,7 +56,6 @@ TEMPLATE_DEBUG = True ALLOWED_HOSTS = ['0.0.0.0/8'] - # Application definition INSTALLED_APPS = ( @@ -54,6 +66,8 @@ INSTALLED_APPS = ( 'django.contrib.messages', 'django.contrib.staticfiles', 'django.contrib.humanize', + 'django_crontab', + 'bootstrapform', 'jumpserver', 'juser', 'jasset', @@ -64,9 +78,9 @@ INSTALLED_APPS = ( MIDDLEWARE_CLASSES = ( 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', - #'django.middleware.csrf.CsrfViewMiddleware', + # 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', - #'django.contrib.auth.middleware.SessionAuthenticationMiddleware', + # 'django.contrib.auth.middleware.SessionAuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', ) @@ -90,6 +104,12 @@ DATABASES = { } } +# DATABASES = { +# 'default': { +# 'ENGINE': 'django.db.backends.sqlite3', +# 'NAME': os.path.join(BASE_DIR, 'db.sqlite3'), +# } +# } TEMPLATE_CONTEXT_PROCESSORS = ( 'django.contrib.auth.context_processors.auth', 'django.core.context_processors.debug', @@ -98,14 +118,14 @@ TEMPLATE_CONTEXT_PROCESSORS = ( 'django.core.context_processors.static', 'django.core.context_processors.tz', 'django.contrib.messages.context_processors.messages', - 'jumpserver.context_processors.name_proc' + 'jumpserver.context_processors.name_proc', ) TEMPLATE_DIRS = ( os.path.join(BASE_DIR, 'templates'), ) -#STATIC_ROOT = os.path.join(BASE_DIR, 'static') +# STATIC_ROOT = os.path.join(BASE_DIR, 'static') STATICFILES_DIRS = ( os.path.join(BASE_DIR, "static"), @@ -129,4 +149,8 @@ USE_TZ = False STATIC_URL = '/static/' +BOOTSTRAP_COLUMN_COUNT = 10 +CRONJOBS = [ + ('0 1 * * *', 'jasset.asset_api.asset_ansible_update_all') +] diff --git a/jumpserver/templatetags/mytags.py b/jumpserver/templatetags/mytags.py index 6f7567b51..ea27b12e1 100644 --- a/jumpserver/templatetags/mytags.py +++ b/jumpserver/templatetags/mytags.py @@ -5,97 +5,67 @@ import ast import time from django import template -from jperm.models import CmdGroup +from jperm.models import PermPush from jumpserver.api import * -from jasset.models import AssetAlias +from jperm.perm_api import get_group_user_perm register = template.Library() -@register.filter(name='stamp2str') -def stamp2str(value): - try: - return time.strftime('%Y/%m/%d %H:%M:%S', time.localtime(value)) - except AttributeError: - return '0000/00/00 00:00:00' - - @register.filter(name='int2str') def int2str(value): + """ + int 转换为 str + """ return str(value) @register.filter(name='get_role') def get_role(user_id): - user_role = {'SU': u'超级管理员', 'DA': u'部门管理员', 'CU': u'普通用户'} - user = User.objects.filter(id=user_id) + """ + 根据用户id获取用户权限 + """ + + user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'} + user = get_object(User, id=user_id) if user: - user = user[0] return user_role.get(str(user.role), u"普通用户") else: return u"普通用户" -@register.filter(name='groups_str') -def groups_str(user_id): - groups = [] - user = User.objects.get(id=user_id) - for group in user.group.all(): - groups.append(group.name) - if len(groups) < 3: - return ' '.join(groups) - else: - return "%s ..." % ' '.join(groups[0:2]) - - -@register.filter(name='group_str2') -def groups_str2(group_list): +@register.filter(name='groups2str') +def groups2str(group_list): + """ + 将用户组列表转换为str + """ if len(group_list) < 3: return ' '.join([group.name for group in group_list]) else: return '%s ...' % ' '.join([group.name for group in group_list[0:2]]) -@register.filter(name='group_str2_all') -def group_str2_all(group_list): - group_lis = [] - for i in group_list: - if str(i) != 'ALL': - group_lis.append(i) - if len(group_lis) < 3: - return ' '.join([group.name for group in group_lis]) - else: - return '%s ...' % ' '.join([group.name for group in group_lis[0:2]]) +@register.filter(name='user_asset_count') +def user_asset_count(user): + """ + 返回用户权限主机的数量 + """ + assets = user.asset.all() + asset_groups = user.asset_group.all() + + for asset_group in asset_groups: + if asset_group: + assets.extend(asset_group.asset_set.all()) + + return len(assets) -@register.filter(name='group_dept_all') -def group_dept_all(group_list): - group_lis = [] - for i in group_list: - if str(i) != 'ALL': - group_lis.append(i) - return ' '.join([group.name for group in group_lis]) - - -@register.filter(name='group_manage_str') -def group_manage_str(username): - user = User.objects.get(username=username) - group = user.user_group.filter(type='M') - if group: - return group[0].name - else: - return '' - - -@register.filter(name='get_item') -def get_item(dictionary, key): - return dictionary.get(key) - - -@register.filter(name='get_login_type') -def get_login_type(login): - login_types = {'L': 'LDAP', 'M': 'MAP'} - return login_types[login] +@register.filter(name='user_asset_group_count') +def user_asset_group_count(user): + """ + 返回用户权限主机组的数量 + """ + return len(user.asset_group.all()) @register.filter(name='bool2str') @@ -106,166 +76,19 @@ def bool2str(value): return u'否' -# @register.filter(name='user_readonly') -# def user_readonly(user_id): -# user = User.objects.filter(id=user_id) -# if user: -# user = user[0] -# if user.role == 'CU': -# return False -# return True -# - -@register.filter(name='member_count') -def member_count(group_id): - group = UserGroup.objects.get(id=group_id) - return group.user_set.count() - - -@register.filter(name='group_user_count') -def group_user_count(group_id): - group = UserGroup.objects.get(id=group_id) - return group.user_set.count() - - -@register.filter(name='dept_user_num') -def dept_user_num(dept_id): - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - return dept.user_set.count() +@register.filter(name='members_count') +def members_count(group_id): + """统计用户组下成员数量""" + group = get_object(UserGroup, id=group_id) + if group: + return group.user_set.count() else: return 0 -@register.filter(name='dept_group_num') -def dept_group_num(dept_id): - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - return dept.usergroup_set.all().count() - else: - return 0 - - -@register.filter(name='perm_count') -def perm_count(group_id): - group = UserGroup.objects.get(id=group_id) - return group.perm_set.count() - - -@register.filter(name='dept_asset_num') -def dept_asset_num(dept_id): - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - return dept.asset_set.all().count() - return 0 - - -@register.filter(name='ugrp_perm_agrp_count') -def ugrp_perm_agrp_count(user_group_id): - user_group = UserGroup.objects.filter(id=user_group_id) - if user_group: - user_group = user_group[0] - return user_group.perm_set.all().count() - return 0 - - -@register.filter(name='ugrp_sudo_agrp_count') -def ugrp_sudo_agrp_count(user_group_id): - user_group = UserGroup.objects.filter(id=user_group_id) - asset_groups = [] - if user_group: - user_group = user_group[0] - for perm in user_group.sudoperm_set.all(): - asset_groups.extend(perm.asset_group.all()) - return len(set(asset_groups)) - return 0 - - -@register.filter(name='ugrp_perm_asset_count') -def ugrp_perm_asset_count(user_group_id): - user_group = UserGroup.objects.filter(id=user_group_id) - assets = [] - if user_group: - user_group = user_group[0] - asset_groups = [perm.asset_group for perm in user_group.perm_set.all()] - for asset_group in asset_groups: - assets.extend(asset_group.asset_set.all()) - return len(set(assets)) - - -@register.filter(name='ugrp_sudo_asset_count') -def ugrp_sudo_asset_count(user_group_id): - user_group = UserGroup.objects.filter(id=user_group_id) - asset_groups = [] - assets = [] - if user_group: - user_group = user_group[0] - for perm in user_group.sudoperm_set.all(): - asset_groups.extend(perm.asset_group.all()) - - for asset_group in asset_groups: - assets.extend(asset_group.asset_set.all()) - return len(set(assets)) - - -@register.filter(name='get_user_alias') -def get_user_alias(post, user_id): - user = User.objects.get(id=user_id) - host = Asset.objects.get(id=post.id) - alias = AssetAlias.objects.filter(user=user, host=host) - if alias: - return alias[0].alias - else: - return '' - - -@register.filter(name='group_type_to_str') -def group_type_to_str(type_name): - group_types = { - 'P': '用户', - 'M': '部门', - 'A': '用户组', - } - return group_types.get(type_name) - - -@register.filter(name='ast_to_list') -def ast_to_list(lis): - ast_lis = ast.literal_eval(lis) - if len(ast_lis) <= 2: - return ','.join([i for i in ast_lis]) - else: - restr = ','.join([i for i in ast_lis[0:2]]) + '...' - return restr - - -@register.filter(name='get_group_count') -def get_group_count(post, dept): - count = post.asset_set.filter(dept=dept).count() - return count - - -@register.filter(name='get_idc_count') -def get_idc_count(post, dept): - count = post.asset_set.filter(dept=dept).count() - return count - - -@register.filter(name='ast_to_list_1') -def ast_to_list_1(lis): - return ast.literal_eval(lis) - - -@register.filter(name='string_length') -def string_length(string, length): - return '%s ...' % string[0:length] - - @register.filter(name='to_name') def to_name(user_id): + """user id 转位用户名称""" try: user = User.objects.filter(id=int(user_id)) if user: @@ -275,89 +98,182 @@ def to_name(user_id): return '非法用户' -@register.filter(name='to_dept_name') -def to_dept_name(user_id): - try: - user = User.objects.filter(id=int(user_id)) - if user: - user = user[0] - return user.dept.name - except: - return '非法部门' - - @register.filter(name='to_role_name') def to_role_name(role_id): - role_dict = {'0': '普通用户', '1': '部门管理员', '2': '超级管理员'} + """role_id 转变为角色名称""" + role_dict = {'0': '普通用户', '1': '组管理员', '2': '超级管理员'} return role_dict.get(str(role_id), '未知') @register.filter(name='to_avatar') def to_avatar(role_id='0'): + """不同角色不同头像""" role_dict = {'0': 'user', '1': 'admin', '2': 'root'} return role_dict.get(str(role_id), 'user') -@register.filter(name='get_user_asset_group') -def get_user_asset_group(user): - return user_perm_group_api(user) +@register.filter(name='result2bool') +def result2bool(result=''): + """将结果定向为结果""" + result = eval(result) + unreachable = result.get('unreachable', []) + failures = result.get('failures', []) - -@register.filter(name='group_asset_list') -def group_asset_list(group): - return group.asset_set.all() - - -@register.filter(name='group_asset_list_count') -def group_asset_list_count(group): - return group.asset_set.all().count() - - -@register.filter(name='time_delta') -def time_delta(time_before): - delta = datetime.datetime.now() - time_before - days = delta.days - if days: - return "%s 天前" % days + if unreachable or failures: + return '失败' else: - hours = delta.seconds/3600 - if hours: - return "%s 小时前" % hours - else: - mins = delta.seconds/60 - if mins: - return '%s 分钟前' % mins - else: - return '%s 秒前' % delta.seconds + return '成功' -@register.filter(name='sudo_cmd_list') -def sudo_cmd_list(cmd_group_id): - cmd_group = CmdGroup.objects.filter(id=cmd_group_id) - if cmd_group: - cmd_group = cmd_group[0] - return cmd_group.cmd.split(',') +@register.filter(name='rule_member_count') +def rule_member_count(instance, member): + """ + instance is a rule object, + use to get the number of the members + :param instance: + :param member: + :return: + """ + member = getattr(instance, member) + counts = member.all().count() + return str(counts) -@register.filter(name='sudo_cmd_count') -def sudo_cmd_count(user_group_id): - user_group = UserGroup.objects.filter(id=user_group_id) - cmds = [] - if user_group: - user_group = user_group[0] - cmd_groups = [] +@register.filter(name='rule_member_name') +def rule_member_name(instance, member): + """ + instance is a rule object, + use to get the name of the members + :param instance: + :param member: + :return: + """ + member = getattr(instance, member) + names = member.all() - for perm in user_group.sudoperm_set.all(): - cmd_groups.extend(perm.cmd_group.all()) + return names - for cmd_group in cmd_groups: - cmds.extend(cmd_group.cmd.split(',')) - return len(set(cmds)) +@register.filter(name='user_which_groups') +def user_which_group(user, member): + """ + instance is a user object, + use to get the group of the user + :param instance: + :param member: + :return: + """ + member = getattr(user, member) + names = [members.name for members in member.all()] + + return ','.join(names) + + +@register.filter(name='asset_which_groups') +def asset_which_group(asset, member): + """ + instance is a user object, + use to get the group of the user + :param instance: + :param member: + :return: + """ + member = getattr(asset, member) + names = [members.name for members in member.all()] + + return ','.join(names) + + +@register.filter(name='group_str2') +def groups_str2(group_list): + """ + 将用户组列表转换为str + """ + if len(group_list) < 3: + return ' '.join([group.name for group in group_list]) else: - return 0 + return '%s ...' % ' '.join([group.name for group in group_list[0:2]]) +@register.filter(name='str_to_list') +def str_to_list(info): + """ + str to list + """ + print ast.literal_eval(info), type(ast.literal_eval(info)) + return ast.literal_eval(info) + + +@register.filter(name='str_to_dic') +def str_to_dic(info): + """ + str to list + """ + if '{' in info: + info_dic = ast.literal_eval(info).iteritems() + else: + info_dic = {} + return info_dic + + +@register.filter(name='str_to_code') +def str_to_code(char_str): + if char_str: + return char_str + else: + return u'空' + + +@register.filter(name='ip_str_to_list') +def ip_str_to_list(ip_str): + """ + ip str to list + """ + return ip_str.split(',') + + +@register.filter(name='key_exist') +def key_exist(username): + """ + ssh key is exist or not + """ + if os.path.isfile(os.path.join(KEY_DIR, 'user', username+'.pem')): + return True + else: + return False + + +@register.filter(name='check_role') +def check_role(asset_id, user): + """ + ssh key is exist or not + """ + return user + + +@register.filter(name='role_contain_which_sudos') +def role_contain_which_sudos(role): + """ + get role sudo commands + """ + sudo_names = [sudo.name for sudo in role.sudo.all()] + return ','.join(sudo_names) + + +@register.filter(name='get_push_info') +def get_push_info(push_id, arg): + push = get_object(PermPush, id=push_id) + if push and arg: + if arg == 'asset': + return [asset.hostname for asset in push.asset.all()] + if arg == 'asset_group': + return [asset_group.name for asset_group in push.asset_group.all()] + if arg == 'role': + return [role.name for role in push.role.all()] + else: + return [] + +<<<<<<< HEAD @register.filter(name='sudo_cmd_count') def sudo_cmd_count(cmd_group_id): cmd_group = CmdGroup.objects.filter(id=cmd_group_id) @@ -367,22 +283,36 @@ def sudo_cmd_count(cmd_group_id): return len(set(cmd_group.cmd.split(','))) else: return 0 +======= + +@register.filter(name='get_cpu_core') +def get_cpu_core(cpu_info): + cpu_core = cpu_info.split('* ')[1] if cpu_info and '*' in cpu_info else cpu_info + return cpu_core +>>>>>>> dev -@register.filter(name='sudo_cmd_ids') -def sudo_cmd_ids(user_group_id): - user_group = UserGroup.objects.filter(id=user_group_id) - if user_group: - user_group = user_group[0] - cmd_groups = [] - for perm in user_group.sudoperm_set.all(): - cmd_groups.extend(perm.cmd_group.all()) - cmd_ids = [str(cmd_group.id) for cmd_group in cmd_groups] - return ','.join(cmd_ids) +@register.filter(name='get_disk_info') +def get_disk_info(disk_info): + try: + disk_size = 0 + if disk_info: + disk_dic = ast.literal_eval(disk_info) + for disk, size in disk_dic.items(): + disk_size += size + disk_size = int(disk_size) + else: + disk_size = '' + except Exception: + disk_size = '' + return disk_size + + +@register.filter(name='user_perm_asset_num') +def user_perm_asset_num(user_id): + user = get_object(User, id=user_id) + if user: + user_perm_info = get_group_user_perm(user) + return len(user_perm_info.get('asset').keys()) else: - return '0' - - -@register.filter(name='cmd_group_split') -def cmd_group_split(cmd_group): - return cmd_group.cmd.split(',') + return 0 diff --git a/jumpserver/urls.py b/jumpserver/urls.py index bd60d04ba..4bce88592 100644 --- a/jumpserver/urls.py +++ b/jumpserver/urls.py @@ -1,22 +1,20 @@ from django.conf.urls import patterns, include, url -urlpatterns = patterns('', +urlpatterns = patterns('jumpserver.views', # Examples: - (r'^$', 'jumpserver.views.index'), - (r'^api/user/$', 'jumpserver.api.api_user'), - (r'^skin_config/$', 'jumpserver.views.skin_config'), - (r'^install/$', 'jumpserver.views.install'), - (r'^base/$', 'jumpserver.views.base'), - (r'^login/$', 'jumpserver.views.login'), - (r'^logout/$', 'jumpserver.views.logout'), - (r'^file/upload/$', 'jumpserver.views.upload'), - (r'^file/download/$', 'jumpserver.views.download'), - (r'^error/$', 'jumpserver.views.httperror'), - (r'^juser/', include('juser.urls')), - (r'^jasset/', include('jasset.urls')), - (r'^jlog/', include('jlog.urls')), - (r'^jperm/', include('jperm.urls')), - (r'^node_auth/', 'jumpserver.views.node_auth'), - + url(r'^$', 'index', name='index'), + # url(r'^api/user/$', 'api_user'), + url(r'^skin_config/$', 'skin_config', name='skin_config'), + url(r'^login/$', 'Login', name='login'), + url(r'^logout/$', 'Logout', name='logout'), + url(r'^exec_cmd/$', 'exec_cmd', name='exec_cmd'), + url(r'^file/upload/$', 'upload', name='file_upload'), + url(r'^file/download/$', 'download', name='file_download'), + url(r'^setting', 'setting', name='setting'), + url(r'^terminal/$', 'web_terminal', name='terminal'), + url(r'^juser/', include('juser.urls')), + url(r'^jasset/', include('jasset.urls')), + url(r'^jlog/', include('jlog.urls')), + url(r'^jperm/', include('jperm.urls')), ) diff --git a/jumpserver/views.py b/jumpserver/views.py index 31f5b9cfa..bd00aed99 100644 --- a/jumpserver/views.py +++ b/jumpserver/views.py @@ -1,83 +1,98 @@ # coding: utf-8 from __future__ import division +import uuid +import urllib + from django.db.models import Count from django.shortcuts import render_to_response from django.template import RequestContext from django.http import HttpResponseNotFound -from jperm.models import Apply +from django.http import HttpResponse +# from jperm.models import Apply import paramiko from jumpserver.api import * -import uuid -import urllib +from jumpserver.models import Setting +from django.contrib.auth import authenticate, login, logout +from django.contrib.auth.decorators import login_required +from jlog.models import Log, FileLog +from jperm.perm_api import get_group_user_perm, gen_resource +from jasset.models import Asset, IDC +from jperm.ansible_api import MyRunner def getDaysByNum(num): + """ + 输出格式:([datetime.date(2015, 11, 6), datetime.date(2015, 11, 8)], ['11-06', '11-08']) + """ + today = datetime.date.today() oneday = datetime.timedelta(days=1) - li_date, li_str = [], [] + date_li, date_str = [], [] for i in range(0, num): today = today-oneday - li_date.append(today) - li_str.append(str(today)[5:10]) - li_date.reverse() - li_str.reverse() - t = (li_date, li_str) - return t + date_li.append(today) + date_str.append(str(today)[5:10]) + date_li.reverse() + date_str.reverse() + return date_li, date_str -def get_data(data, items, option): - dic = {} - li_date, li_str = getDaysByNum(7) - for item in items: - li = [] - name = item[option] - if option == 'user': - option_data = data.filter(user=name) - elif option == 'host': - option_data = data.filter(host=name) - for t in li_date: - year, month, day = t.year, t.month, t.day - times = option_data.filter(start_time__year=year, start_time__month=month, start_time__day=day).count() - li.append(times) - dic[name] = li - return dic +def get_data(x, y, z): + pass -@require_login -def index_cu(request): - user_id = request.session.get('user_id') - user = User.objects.filter(id=user_id) - if user: - user = user[0] - login_types = {'L': 'LDAP', 'M': 'MAP'} - user_id = request.session.get('user_id') - username = User.objects.get(id=user_id).username - posts = user_perm_asset_api(username) - host_count = len(posts) - new_posts = [] - post_five = [] - for post in posts: - if len(post_five) < 5: - post_five.append(post) +def get_data_by_day(date_li, item): + data_li = [] + for d in date_li: + logs = Log.objects.filter(start_time__year=d.year, + start_time__month=d.month, + start_time__day=d.day) + if item == 'user': + data_li.append(set([log.user for log in logs])) + elif item == 'asset': + data_li.append(set([log.host for log in logs])) + elif item == 'login': + data_li.append(logs) else: - new_posts.append(post_five) - post_five = [] - new_posts.append(post_five) - - return render_to_response('index_cu.html', locals(), context_instance=RequestContext(request)) + pass + return data_li -@require_login +def get_count_by_day(date_li, item): + data_li = get_data_by_day(date_li, item) + data_count_li = [] + for data in data_li: + data_count_li.append(len(data)) + return data_count_li + + +def get_count_by_date(date_li, item): + data_li = get_data_by_day(date_li, item) + data_count_tmp = [] + for data in data_li: + data_count_tmp.extend(list(data)) + + return len(set(data_count_tmp)) + + +@require_role(role='user') +def index_cu(request): + username = request.user.username + return HttpResponseRedirect(reverse('user_detail')) + + +@require_role(role='user') def index(request): li_date, li_str = getDaysByNum(7) today = datetime.datetime.now().day from_week = datetime.datetime.now() - datetime.timedelta(days=7) - if is_common_user(request): + if is_role_request(request, 'user'): return index_cu(request) - elif is_super_user(request): + elif is_role_request(request, 'super'): + # dashboard 显示汇总 users = User.objects.all() hosts = Asset.objects.all() online = Log.objects.filter(is_finished=0) @@ -85,68 +100,52 @@ def index(request): online_user = online.values('user').distinct() active_users = User.objects.filter(is_active=1) active_hosts = Asset.objects.filter(is_active=1) + + # 一个月历史汇总 + date_li, date_str = getDaysByNum(30) + date_month = repr(date_str) + active_user_per_month = str(get_count_by_day(date_li, 'user')) + active_asset_per_month = str(get_count_by_day(date_li, 'asset')) + active_login_per_month = str(get_count_by_day(date_li, 'login')) + + # 活跃用户资产图 + active_user_month = get_count_by_date(date_li, 'user') + disabled_user_count = len(users.filter(is_active=False)) + inactive_user_month = len(users) - active_user_month + active_asset_month = get_count_by_date(date_li, 'asset') + disabled_asset_count = len(hosts.filter(is_active=False)) if hosts.filter(is_active=False) else 0 + inactive_asset_month = len(hosts) - active_asset_month if len(hosts) > active_asset_month else 0 + + # 一周top10用户和主机 week_data = Log.objects.filter(start_time__range=[from_week, datetime.datetime.now()]) + user_top_ten = week_data.values('user').annotate(times=Count('user')).order_by('-times')[:10] + host_top_ten = week_data.values('host').annotate(times=Count('host')).order_by('-times')[:10] - elif is_group_admin(request): - user = get_session_user_info(request)[2] - dept_name, dept = get_session_user_info(request)[4:] - users = User.objects.filter(dept=dept) - hosts = Asset.objects.filter(dept=dept) - online = Log.objects.filter(dept_name=dept_name, is_finished=0) - online_host = online.values('host').distinct() - online_user = online.values('user').distinct() - active_users = users.filter(is_active=1) - active_hosts = hosts.filter(is_active=1) - week_data = Log.objects.filter(dept_name=dept_name, start_time__range=[from_week, datetime.datetime.now()]) + for user_info in user_top_ten: + username = user_info.get('user') + last = Log.objects.filter(user=username).latest('start_time') + user_info['last'] = last - # percent of dashboard - if users.count() == 0: - percent_user, percent_online_user = '0%', '0%' - else: - percent_user = format(active_users.count() / users.count(), '.0%') - percent_online_user = format(online_user.count() / users.count(), '.0%') - if hosts.count() == 0: - percent_host, percent_online_host = '0%', '0%' - else: - percent_host = format(active_hosts.count() / hosts.count(), '.0%') - percent_online_host = format(online_host.count() / hosts.count(), '.0%') + for host_info in host_top_ten: + host = host_info.get('host') + last = Log.objects.filter(host=host).latest('start_time') + host_info['last'] = last - user_top_ten = week_data.values('user').annotate(times=Count('user')).order_by('-times')[:10] - host_top_ten = week_data.values('host').annotate(times=Count('host')).order_by('-times')[:10] - user_dic, host_dic = get_data(week_data, user_top_ten, 'user'), get_data(week_data, host_top_ten, 'host') + # 一周top5 + week_users = week_data.values('user').distinct().count() + week_hosts = week_data.count() - # a week data - week_users = week_data.values('user').distinct().count() - week_hosts = week_data.count() + user_top_five = week_data.values('user').annotate(times=Count('user')).order_by('-times')[:5] + color = ['label-success', 'label-info', 'label-primary', 'label-default', 'label-warnning'] - user_top_five = week_data.values('user').annotate(times=Count('user')).order_by('-times')[:5] - color = ['label-success', 'label-info', 'label-primary', 'label-default', 'label-warnning'] + # 最后10次权限申请 + # perm apply latest 10 + # perm_apply_10 = Apply.objects.order_by('-date_add')[:10] - # perm apply latest 10 - perm_apply_10 = Apply.objects.order_by('-date_add')[:10] + # 最后10次登陆 + login_10 = Log.objects.order_by('-start_time')[:10] + login_more_10 = Log.objects.order_by('-start_time')[10:21] - # latest 10 login - login_10 = Log.objects.order_by('-start_time')[:10] - login_more_10 = Log.objects.order_by('-start_time')[10:21] - - # a week top 10 - for user_info in user_top_ten: - username = user_info.get('user') - last = Log.objects.filter(user=username).latest('start_time') - user_info['last'] = last - - top = {'user': '活跃用户数', 'host': '活跃主机数', 'times': '登录次数'} - top_dic = {} - for key, value in top.items(): - li = [] - for t in li_date: - year, month, day = t.year, t.month, t.day - if key != 'times': - times = week_data.filter(start_time__year=year, start_time__month=month, start_time__day=day).values(key).distinct().count() - else: - times = week_data.filter(start_time__year=year, start_time__month=month, start_time__day=day).count() - li.append(times) - top_dic[value] = li return render_to_response('index.html', locals(), context_instance=RequestContext(request)) @@ -154,34 +153,6 @@ def skin_config(request): return render_to_response('skin_config.html') -def pages(posts, r): - """分页公用函数""" - contact_list = posts - p = paginator = Paginator(contact_list, 10) - try: - current_page = int(r.GET.get('page', '1')) - except ValueError: - current_page = 1 - - page_range = page_list_return(len(p.page_range), current_page) - - try: - contacts = paginator.page(current_page) - except (EmptyPage, InvalidPage): - contacts = paginator.page(paginator.num_pages) - - if current_page >= 5: - show_first = 1 - else: - show_first = 0 - if current_page <= (len(p.page_range) - 3): - show_end = 1 - else: - show_end = 0 - - return contact_list, p, contacts, page_range, current_page, show_first, show_end - - def is_latest(): node = uuid.getnode() jsn = uuid.UUID(int=node).hex[-12:] @@ -193,137 +164,195 @@ def is_latest(): pass -def login(request): +@defend_attack +def Login(request): """登录界面""" - if request.session.get('username'): - return HttpResponseRedirect('/') + error = '' + if request.user.is_authenticated(): + return HttpResponseRedirect(reverse('index')) if request.method == 'GET': return render_to_response('login.html') else: username = request.POST.get('username') password = request.POST.get('password') - user_filter = User.objects.filter(username=username) - if user_filter: - user = user_filter[0] - if md5_crypt(password) == user.password: - request.session['user_id'] = user.id - user_filter.update(last_login=datetime.datetime.now()) - if user.role == 'SU': - request.session['role_id'] = 2 - elif user.role == 'DA': - request.session['role_id'] = 1 + if username and password: + user = authenticate(username=username, password=password) + if user is not None: + if user.is_active: + login(request, user) + # c = {} + # c.update(csrf(request)) + # request.session['csrf_token'] = str(c.get('csrf_token')) + # user_filter = User.objects.filter(username=username) + # if user_filter: + # user = user_filter[0] + # if PyCrypt.md5_crypt(password) == user.password: + # request.session['user_id'] = user.id + # user_filter.update(last_login=datetime.datetime.now()) + if user.role == 'SU': + request.session['role_id'] = 2 + elif user.role == 'GA': + request.session['role_id'] = 1 + else: + request.session['role_id'] = 0 + return HttpResponseRedirect(request.session.get('pre_url', '/')) + # response.set_cookie('username', username, expires=604800) + # response.set_cookie('seed', PyCrypt.md5_crypt(password), expires=604800) + # return response else: - request.session['role_id'] = 0 - response = HttpResponseRedirect('/', ) - response.set_cookie('username', username, expires=604800) - response.set_cookie('seed', md5_crypt(password), expires=604800) - return response + error = '用户未激活' else: - error = '密码错误,请重新输入。' + error = '用户名或密码错误' else: - error = '用户不存在。' + error = '用户名或密码错误' return render_to_response('login.html', {'error': error}) -def logout(request): - request.session.delete() - return HttpResponseRedirect('/login/') +@require_role('user') +def Logout(request): + logout(request) + return HttpResponseRedirect(reverse('index')) -def filter_ajax_api(request): - attr = request.GET.get('attr', 'user') - value = request.GET.get('value', '') - if attr == 'user': - contact_list = User.objects.filter(name__icontains=value) - elif attr == "user_group": - contact_list = UserGroup.objects.filter(name__icontains=value) - elif attr == "asset": - contact_list = Asset.objects.filter(ip__icontains=value) - elif attr == "asset": - contact_list = BisGroup.objects.filter(name__icontains=value) +@require_role('admin') +def setting(request): + header_title, path1 = '项目设置', '设置' + setting_default = get_object(Setting, name='default') - return render_to_response('filter_ajax_api.html', locals()) + if request.method == "POST": + setting_raw = request.POST.get('setting', '') + if setting_raw == 'default': + username = request.POST.get('username', '') + port = request.POST.get('port', '') + password = request.POST.get('password', '') + private_key = request.POST.get('key', '') + + if '' in [username, port]: + return HttpResponse('所填内容不能为空, 且密码和私钥填一个') + else: + private_key_dir = os.path.join(BASE_DIR, 'keys', 'default') + private_key_path = os.path.join(private_key_dir, 'admin_user.pem') + mkdir(private_key_dir) + + if private_key: + with open(private_key_path, 'w') as f: + f.write(private_key) + os.chmod(private_key_path, 0600) + + if setting_default: + if password: + password_encode = CRYPTOR.encrypt(password) + else: + password_encode = password + Setting.objects.filter(name='default').update(field1=username, field2=port, + field3=password_encode, + field4=private_key_path) + + else: + password_encode = CRYPTOR.encrypt(password) + setting_r = Setting(name='default', field1=username, field2=port, + field3=password_encode, + field4=private_key_path).save() + + msg = "设置成功" + return my_render('setting.html', locals(), request) -def install(request): - from juser.models import DEPT, User - if User.objects.filter(id=5000): - return httperror(request, 'Jumpserver已初始化,不能重复安装!') +@login_required(login_url='/login') +def upload(request): + user = request.user + assets = get_group_user_perm(user).get('asset').keys() + asset_select = [] + if request.method == 'POST': + remote_ip = request.META.get('REMOTE_ADDR') + asset_ids = request.POST.getlist('asset_ids', '') + upload_files = request.FILES.getlist('file[]', None) + date_now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + upload_dir = get_tmp_dir() + # file_dict = {} + for asset_id in asset_ids: + asset_select.append(get_object(Asset, id=asset_id)) - dept = DEPT(id=1, name="超管部", comment="超级管理部门") - dept.save() - dept2 = DEPT(id=2, name="默认", comment="默认部门") - dept2.save() - IDC(id=1, name="默认", comment="默认IDC").save() - BisGroup(id=1, name="ALL", dept=dept, comment="所有主机组").save() + if not set(asset_select).issubset(set(assets)): + illegal_asset = set(asset_select).issubset(set(assets)) + return HttpResponse('没有权限的服务器 %s' % ','.join([asset.hostname for asset in illegal_asset])) - User(id=5000, username="admin", password=md5_crypt('admin'), - name='admin', email='admin@jumpserver.org', role='SU', is_active=True, dept=dept).save() - return success(request, u'Jumpserver初始化成功') + for upload_file in upload_files: + file_path = '%s/%s' % (upload_dir, upload_file.name) + with open(file_path, 'w') as f: + for chunk in upload_file.chunks(): + f.write(chunk) + + res = gen_resource({'user': user, 'asset': asset_select}) + runner = MyRunner(res) + runner.run('copy', module_args='src=%s dest=%s directory_mode' + % (upload_dir, upload_dir), pattern='*') + ret = runner.results + logger.debug(ret) + FileLog(user=request.user.username, host=' '.join([asset.hostname for asset in asset_select]), + filename=' '.join([f.name for f in upload_files]), type='upload', remote_ip=remote_ip, + result=ret).save() + if ret.get('failed'): + error = u'上传目录: %s
上传失败: [ %s ]
上传成功 [ %s ]' % (upload_dir, + ', '.join(ret.get('failed').keys()), + ', '.join(ret.get('ok').keys())) + return HttpResponse(error, status=500) + msg = u'上传目录: %s
传送成功 [ %s ]' % (upload_dir, ', '.join(ret.get('ok').keys())) + return HttpResponse(msg) + return my_render('upload.html', locals(), request) +@login_required(login_url='/login') def download(request): + user = request.user + assets = get_group_user_perm(user).get('asset').keys() + asset_select = [] + if request.method == 'POST': + remote_ip = request.META.get('REMOTE_ADDR') + asset_ids = request.POST.getlist('asset_ids', '') + file_path = request.POST.get('file_path') + date_now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") + upload_dir = get_tmp_dir() + for asset_id in asset_ids: + asset_select.append(get_object(Asset, id=asset_id)) + + if not set(asset_select).issubset(set(assets)): + illegal_asset = set(asset_select).issubset(set(assets)) + return HttpResponse(u'没有权限的服务器 %s' % ','.join([asset.hostname for asset in illegal_asset])) + + res = gen_resource({'user': user, 'asset': asset_select}) + runner = MyRunner(res) + runner.run('fetch', module_args='src=%s dest=%s' % (file_path, upload_dir), pattern='*') + FileLog(user=request.user.username, host=' '.join([asset.hostname for asset in asset_select]), + filename=file_path, type='download', remote_ip=remote_ip, result=runner.results).save() + logger.debug(runner.results) + os.chdir('/tmp') + tmp_dir_name = os.path.basename(upload_dir) + tar_file = '%s.tar.gz' % upload_dir + bash('tar czf %s %s' % (tar_file, tmp_dir_name)) + f = open(tar_file) + data = f.read() + f.close() + response = HttpResponse(data, content_type='application/octet-stream') + response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(tar_file) + return response + return render_to_response('download.html', locals(), context_instance=RequestContext(request)) -def transfer(sftp, filenames): - # pool = Pool(processes=5) - for filename, file_path in filenames.items(): - print filename, file_path - sftp.put(file_path, '/tmp/%s' % filename) - # pool.apply_async(transfer, (sftp, file_path, '/tmp/%s' % filename)) - sftp.close() - # pool.close() - # pool.join() +@login_required(login_url='/login') +def exec_cmd(request): + role = request.GET.get('role') + check_assets = request.GET.get('check_assets', '') + web_terminal_uri = 'ws://%s/exec?role=%s' % (WEB_SOCKET_HOST, role) + return my_render('exec_cmd.html', locals(), request) -def upload(request): - user, dept = get_session_user_dept(request) - if request.method == 'POST': - hosts = request.POST.get('hosts') - upload_files = request.FILES.getlist('file[]', None) - upload_dir = "/tmp/%s" % user.username - is_dir(upload_dir) - date_now = datetime.datetime.now().strftime("%Y%m%d%H%M%S") - hosts_list = hosts.split(',') - user_hosts = get_user_host(user.username).keys() - unperm_hosts = [] - filenames = {} - for ip in hosts_list: - if ip not in user_hosts: - unperm_hosts.append(ip) +@require_role('user') +def web_terminal(request): + asset_id = request.GET.get('id') + role_name = request.GET.get('role') + web_terminal_uri = 'ws://%s/terminal?id=%s&role=%s' % (WEB_SOCKET_HOST, asset_id, role_name) + return render_to_response('jlog/web_terminal.html', locals()) - if not hosts: - return HttpResponseNotFound(u'地址不能为空') - if unperm_hosts: - print hosts_list - return HttpResponseNotFound(u'%s 没有权限.' % ', '.join(unperm_hosts)) - - for upload_file in upload_files: - file_path = '%s/%s.%s' % (upload_dir, upload_file.name, date_now) - filenames[upload_file.name] = file_path - f = open(file_path, 'w') - for chunk in upload_file.chunks(): - f.write(chunk) - f.close() - - sftps = [] - for host in hosts_list: - username, password, host, port = get_connect_item(user.username, host) - try: - t = paramiko.Transport((host, port)) - t.connect(username=username, password=password) - sftp = paramiko.SFTPClient.from_transport(t) - sftps.append(sftp) - except paramiko.AuthenticationException: - return HttpResponseNotFound(u'%s 连接失败.' % host) - - # pool = Pool(processes=5) - for sftp in sftps: - transfer(sftp, filenames) - # pool.close() - # pool.join() - return HttpResponse('传送成功') - - return render_to_response('upload.html', locals(), context_instance=RequestContext(request)) diff --git a/juser/models.py b/juser/models.py index d7efd7a28..54d1b94a0 100644 --- a/juser/models.py +++ b/juser/models.py @@ -1,41 +1,54 @@ +# coding: utf-8 + from django.db import models - - -class DEPT(models.Model): - name = models.CharField(max_length=80, unique=True) - comment = models.CharField(max_length=160, blank=True, null=True) - - def __unicode__(self): - return self.name +from django.contrib.auth.models import AbstractUser +import time +# from jasset.models import Asset, AssetGroup class UserGroup(models.Model): name = models.CharField(max_length=80, unique=True) - dept = models.ForeignKey(DEPT) comment = models.CharField(max_length=160, blank=True, null=True) def __unicode__(self): return self.name -class User(models.Model): +class User(AbstractUser): USER_ROLE_CHOICES = ( ('SU', 'SuperUser'), - ('DA', 'DeptAdmin'), + ('GA', 'GroupAdmin'), ('CU', 'CommonUser'), ) - username = models.CharField(max_length=80, unique=True) - password = models.CharField(max_length=100) name = models.CharField(max_length=80) - email = models.EmailField(max_length=75) + uuid = models.CharField(max_length=100) role = models.CharField(max_length=2, choices=USER_ROLE_CHOICES, default='CU') - dept = models.ForeignKey(DEPT) group = models.ManyToManyField(UserGroup) - ldap_pwd = models.CharField(max_length=100) - ssh_key_pwd = models.CharField(max_length=100) - is_active = models.BooleanField(default=True) - last_login = models.DateTimeField(null=True) - date_joined = models.DateTimeField(null=True) + ssh_key_pwd = models.CharField(max_length=200) + # is_active = models.BooleanField(default=True) + # last_login = models.DateTimeField(null=True) + # date_joined = models.DateTimeField(null=True) def __unicode__(self): return self.username + + +class AdminGroup(models.Model): + """ + under the user control group + 用户可以管理的用户组,或组的管理员是该用户 + """ + + user = models.ForeignKey(User) + group = models.ForeignKey(UserGroup) + + def __unicode__(self): + return '%s: %s' % (self.user.username, self.group.name) + + +class Document(models.Model): + def upload_to(self, filename): + return 'upload/'+str(self.user.id)+time.strftime('/%Y/%m/%d/', time.localtime())+filename + + docfile = models.FileField(upload_to=upload_to) + user = models.ForeignKey(User) diff --git a/juser/tests.py b/juser/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/juser/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/juser/urls.py b/juser/urls.py index cbaba7bb0..47952dd11 100644 --- a/juser/urls.py +++ b/juser/urls.py @@ -3,31 +3,23 @@ from jumpserver.api import view_splitter from juser.views import * urlpatterns = patterns('juser.views', - # Examples: - # url(r'^$', 'jumpserver.views.home', name='home'), - # url(r'^blog/', include('blog.urls')), - - (r'^dept_list/$', view_splitter, {'su': dept_list, 'adm': dept_list_adm}), - (r'^dept_add/$', 'dept_add'), - (r'^dept_del/$', 'dept_del'), - (r'^dept_detail/$', 'dept_detail'), - (r'^dept_del_ajax/$', 'dept_del_ajax'), - (r'^dept_edit/$', 'dept_edit'), - (r'^dept_user_ajax/$', 'dept_user_ajax'), - (r'^group_add/$', view_splitter, {'su': group_add, 'adm': group_add_adm}), - (r'^group_list/$', view_splitter, {'su': group_list, 'adm': group_list_adm}), - (r'^group_detail/$', 'group_detail'), - (r'^group_del/$', view_splitter, {'su': group_del, 'adm': group_del_adm}), - (r'^group_del_ajax/$', 'group_del_ajax'), - (r'^group_edit/$', view_splitter, {'su': group_edit, 'adm': group_edit_adm}), - (r'^user_add/$', view_splitter, {'su': user_add, 'adm': user_add_adm}), - (r'^user_list/$', view_splitter, {'su': user_list, 'adm': user_list_adm}), - (r'^user_detail/$', 'user_detail'), - (r'^user_del/$', 'user_del'), - (r'^user_del_ajax/$', 'user_del_ajax'), - (r'^user_edit/$', view_splitter, {'su': user_edit, 'adm': user_edit_adm}), - (r'^profile/$', 'profile'), - (r'^chg_info/$', 'chg_info'), - (r'^chg_role/$', 'chg_role'), - (r'^down_key/$', 'down_key'), -) + # Examples: + # url(r'^$', 'jumpserver.views.home', name='home'), + # url(r'^blog/', include('blog.urls')), + url(r'^group/add/$', 'group_add', name='user_group_add'), + url(r'^group/list/$', 'group_list', name='user_group_list'), + url(r'^group/del/$', 'group_del', name='user_group_del'), + url(r'^group/edit/$', 'group_edit', name='user_group_edit'), + url(r'^user/add/$', 'user_add', name='user_add'), + url(r'^user/del/$', 'user_del', name='user_del'), + url(r'^user/list/$', 'user_list', name='user_list'), + url(r'^user/edit/$', 'user_edit', name='user_edit'), + url(r'^user/detail/$', 'user_detail', name='user_detail'), + url(r'^user/profile/$', 'profile', name='user_profile'), + url(r'^user/update/$', 'change_info', name='user_update'), + url(r'^mail/retry/$', 'send_mail_retry', name='mail_retry'), + url(r'^password/reset/$', 'reset_password', name='password_reset'), + url(r'^password/forget/$', 'forget_password', name='password_forget'), + url(r'^key/gen/$', 'regen_ssh_key', name='key_gen'), + url(r'^key/down/$', 'down_key', name='key_down'), + ) diff --git a/juser/views.py b/juser/views.py index 22a08499b..d3059a460 100644 --- a/juser/views.py +++ b/juser/views.py @@ -2,17 +2,19 @@ # Author: Guanghongwei # Email: ibuler@qq.com -import random -from Crypto.PublicKey import RSA -import crypt +# import random +# from Crypto.PublicKey import RSA +import uuid +from django.contrib.auth.decorators import login_required -from django.shortcuts import render_to_response from django.db.models import Q -from django.template import RequestContext +from juser.user_api import * +from jperm.perm_api import get_group_user_perm -from jumpserver.api import * +MAIL_FROM = EMAIL_HOST_USER +<<<<<<< HEAD def gen_rand_pwd(num): """生成随机密码""" seed = "1234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" @@ -333,718 +335,455 @@ def dept_user_ajax(request): @require_super_user +======= +@require_role(role='super') +>>>>>>> dev def group_add(request): + """ + group add view for route + 添加用户组的视图 + """ error = '' msg = '' - header_title, path1, path2 = '添加小组', '用户管理', '添加小组' + header_title, path1, path2 = '添加用户组', '用户管理', '添加用户组' user_all = User.objects.all() - dept_all = DEPT.objects.all() if request.method == 'POST': group_name = request.POST.get('group_name', '') - dept_id = request.POST.get('dept_id', '') users_selected = request.POST.getlist('users_selected', '') comment = request.POST.get('comment', '') try: - if '' in [group_name, dept_id]: - error = u'组名 或 部门 不能为空' - raise AddError(error) + if not group_name: + error = u'组名 不能为空' + raise ServerError(error) if UserGroup.objects.filter(name=group_name): error = u'组名已存在' - raise AddError(error) - - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - else: - error = u'部门不存在' - raise AddError(error) - - db_add_group(name=group_name, users=users_selected, dept=dept, comment=comment) - except AddError: + raise ServerError(error) + db_add_group(name=group_name, users_id=users_selected, comment=comment) + except ServerError: pass except TypeError: - error = u'保存小组失败' + error = u'添加小组失败' else: msg = u'添加组 %s 成功' % group_name - return render_to_response('juser/group_add.html', locals(), context_instance=RequestContext(request)) + return my_render('juser/group_add.html', locals(), request) -@require_admin -def group_add_adm(request): - error = '' - msg = '' - header_title, path1, path2 = '添加小组', '用户管理', '添加小组' - user, dept = get_session_user_dept(request) - user_all = dept.user_set.all() - - if request.method == 'POST': - group_name = request.POST.get('group_name', '') - users_selected = request.POST.getlist('users_selected', '') - comment = request.POST.get('comment', '') - - try: - if not validate(request, user=users_selected): - raise AddError('没有某用户权限') - if '' in [group_name]: - error = u'组名不能为空' - raise AddError(error) - - db_add_group(name=group_name, users=users_selected, dept=dept, comment=comment) - except AddError: - pass - except TypeError: - error = u'保存小组失败' - else: - msg = u'添加组 %s 成功' % group_name - - return render_to_response('juser/group_add.html', locals(), context_instance=RequestContext(request)) - - -@require_super_user +@require_role(role='super') def group_list(request): - header_title, path1, path2 = '查看小组', '用户管理', '查看小组' + """ + list user group + 用户组列表 + """ + header_title, path1, path2 = '查看用户组', '用户管理', '查看用户组' keyword = request.GET.get('search', '') - did = request.GET.get('did', '') - contact_list = UserGroup.objects.all().order_by('name') - - if did: - dept = DEPT.objects.filter(id=did) - if dept: - dept = dept[0] - contact_list = dept.usergroup_set.all() + user_group_list = UserGroup.objects.all().order_by('name') + group_id = request.GET.get('id', '') if keyword: - contact_list = contact_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) + user_group_list = user_group_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - return render_to_response('juser/group_list.html', locals(), context_instance=RequestContext(request)) + if group_id: + user_group_list = user_group_list.filter(id=int(group_id)) + + user_group_list, p, user_groups, page_range, current_page, show_first, show_end = pages(user_group_list, request) + return my_render('juser/group_list.html', locals(), request) -@require_admin -def group_list_adm(request): - header_title, path1, path2 = '查看部门小组', '用户管理', '查看小组' - keyword = request.GET.get('search', '') - did = request.GET.get('did', '') - user, dept = get_session_user_dept(request) - contact_list = dept.usergroup_set.all().order_by('name') - - if keyword: - contact_list = contact_list.filter(Q(name__icontains=keyword) | Q(comment__icontains=keyword)) - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - return render_to_response('juser/group_list.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def group_detail(request): - group_id = request.GET.get('id', None) - if not group_id: - return HttpResponseRedirect('/') - group = UserGroup.objects.get(id=group_id) - users = group.user_set.all() - return render_to_response('juser/group_detail.html', locals(), context_instance=RequestContext(request)) - - -@require_super_user +@require_role(role='super') def group_del(request): - group_id = request.GET.get('id', '') - if not group_id: - return HttpResponseRedirect('/') - UserGroup.objects.filter(id=group_id).delete() - return HttpResponseRedirect('/juser/group_list/') - - -@require_admin -def group_del_adm(request): - group_id = request.GET.get('id', '') - if not validate(request, user_group=[group_id]): - return HttpResponseRedirect('/juser/group_list/') - if not group_id: - return HttpResponseRedirect('/') - UserGroup.objects.filter(id=group_id).delete() - return HttpResponseRedirect('/juser/group_list/') - - -@require_admin -def group_del_ajax(request): - group_ids = request.POST.get('group_ids') - group_ids = group_ids.split(',') - if request.session.get('role_id') == 1: - if not validate(request, user_group=group_ids): - return "error" - for group_id in group_ids: + """ + del a group + 删除用户组 + """ + group_ids = request.GET.get('id', '') + group_id_list = group_ids.split(',') + for group_id in group_id_list: UserGroup.objects.filter(id=group_id).delete() + return HttpResponse('删除成功') -def group_update_member(group_id, users_id_list): - group = UserGroup.objects.filter(id=group_id) - if group: - group = group[0] - group.user_set.clear() - for user_id in users_id_list: - user = User.objects.get(id=user_id) - group.user_set.add(user) - - -@require_super_user +@require_role(role='super') def group_edit(request): error = '' msg = '' - header_title, path1, path2 = '修改小组信息', '用户管理', '编辑小组' + header_title, path1, path2 = '编辑用户组', '用户管理', '编辑用户组' + if request.method == 'GET': group_id = request.GET.get('id', '') - group = UserGroup.objects.filter(id=group_id) - if group: - group = group[0] - dept_all = DEPT.objects.all() - users_all = User.objects.all() - users_selected = group.user_set.all() - users = [user for user in users_all if user not in users_selected] + user_group = get_object(UserGroup, id=group_id) + # user_group = UserGroup.objects.get(id=group_id) + users_selected = User.objects.filter(group=user_group) + users_remain = User.objects.filter(~Q(group=user_group)) + users_all = User.objects.all() - return render_to_response('juser/group_edit.html', locals(), context_instance=RequestContext(request)) - else: + elif request.method == 'POST': group_id = request.POST.get('group_id', '') group_name = request.POST.get('group_name', '') - dept_id = request.POST.get('dept_id', '') comment = request.POST.get('comment', '') users_selected = request.POST.getlist('users_selected') - users = [] try: if '' in [group_id, group_name]: - raise AddError('组名不能为空') - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - else: - raise AddError('部门不存在') - for user_id in users_selected: - users.extend(User.objects.filter(id=user_id)) + raise ServerError('组名不能为空') - user_group = UserGroup.objects.filter(id=group_id) - if user_group: - user_group.update(name=group_name, comment=comment, dept=dept) - user_group = user_group[0] - user_group.user_set.clear() - user_group.user_set = users - - except AddError, e: + if len(UserGroup.objects.filter(name=group_name)) > 1: + raise ServerError(u'%s 用户组已存在' % group_name) + # add user group + for user in User.objects.filter(id__in=users_selected): + user.group.add(UserGroup.objects.get(id=group_id)) + # delete user group + user_group = UserGroup.objects.get(id=group_id) + for user in [user for user in User.objects.filter(group=user_group) if user not in User.objects.filter(id__in=users_selected)]: + user_group_all = user.group.all() + user.group.clear() + for g in user_group_all: + if g == user_group: + continue + user.group.add(g) + user_group.name = group_name + user_group.comment = comment + user_group.save() + except ServerError, e: error = e + if not error: + return HttpResponseRedirect(reverse('user_group_list')) + else: + users_all = User.objects.all() + users_selected = User.objects.filter(group=user_group) + users_remain = User.objects.filter(~Q(group=user_group)) - return HttpResponseRedirect('/juser/group_list/') + return my_render('juser/group_edit.html', locals(), request) -@require_admin -def group_edit_adm(request): - error = '' - msg = '' - header_title, path1, path2 = '修改小组信息', '用户管理', '编辑小组' - user, dept = get_session_user_dept(request) - if request.method == 'GET': - group_id = request.GET.get('id', '') - if not validate(request, user_group=[group_id]): - return HttpResponseRedirect('/juser/group_list/') - group = UserGroup.objects.filter(id=group_id) - if group: - group = group[0] - users_all = dept.user_set.all() - users_selected = group.user_set.all() - users = [user for user in users_all if user not in users_selected] - - return render_to_response('juser/group_edit.html', locals(), context_instance=RequestContext(request)) - else: - group_id = request.POST.get('group_id', '') - group_name = request.POST.get('group_name', '') - comment = request.POST.get('comment', '') - users_selected = request.POST.getlist('users_selected') - - users = [] - try: - if not validate(request, user=users_selected): - raise AddError(u'右侧非部门用户') - - if not validate(request, user_group=[group_id]): - raise AddError(u'没有权限修改本组') - - for user_id in users_selected: - users.extend(User.objects.filter(id=user_id)) - - user_group = UserGroup.objects.filter(id=group_id) - if user_group: - user_group.update(name=group_name, comment=comment, dept=dept) - user_group = user_group[0] - user_group.user_set.clear() - user_group.user_set = users - - except AddError, e: - error = e - - return HttpResponseRedirect('/juser/group_list/') - - -@require_super_user +@require_role(role='super') def user_add(request): error = '' msg = '' header_title, path1, path2 = '添加用户', '用户管理', '添加用户' - user_role = {'SU': u'超级管理员', 'DA': u'部门管理员', 'CU': u'普通用户'} - dept_all = DEPT.objects.all() + user_role = {'SU': u'超级管理员', 'CU': u'普通用户'} group_all = UserGroup.objects.all() if request.method == 'POST': username = request.POST.get('username', '') - password = gen_rand_pwd(16) + password = PyCrypt.gen_rand_pass(16) name = request.POST.get('name', '') email = request.POST.get('email', '') - dept_id = request.POST.get('dept_id') groups = request.POST.getlist('groups', []) - role_post = request.POST.get('role', 'CU') - ssh_key_pwd = gen_rand_pwd(16) - is_active = True if request.POST.get('is_active', '1') == '1' else False - ldap_pwd = gen_rand_pwd(16) + admin_groups = request.POST.getlist('admin_groups', []) + role = request.POST.get('role', 'CU') + uuid_r = uuid.uuid4().get_hex() + ssh_key_pwd = PyCrypt.gen_rand_pass(16) + extra = request.POST.getlist('extra', []) + is_active = False if '0' in extra else True + ssh_key_login_need = True + send_mail_need = True if '2' in extra else False try: - if '' in [username, password, ssh_key_pwd, name, groups, role_post, is_active]: + if '' in [username, password, ssh_key_pwd, name, role]: error = u'带*内容不能为空' - raise AddError - user = User.objects.filter(username=username) - if user: + raise ServerError + check_user_is_exist = User.objects.filter(username=username) + if check_user_is_exist: error = u'用户 %s 已存在' % username - raise AddError + raise ServerError - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - else: - error = u'部门不存在' - raise AddError(error) - - except AddError: + except ServerError: pass else: try: - user = db_add_user(username=username, - password=md5_crypt(password), - name=name, email=email, dept=dept, - groups=groups, role=role_post, - ssh_key_pwd=md5_crypt(ssh_key_pwd), - ldap_pwd=CRYPTOR.encrypt(ldap_pwd), + user = db_add_user(username=username, name=name, + password=password, + email=email, role=role, uuid=uuid_r, + groups=groups, admin_groups=admin_groups, + ssh_key_pwd=ssh_key_pwd, is_active=is_active, date_joined=datetime.datetime.now()) + server_add_user(username, password, ssh_key_pwd, ssh_key_login_need) + user = get_object(User, username=username) + if groups: + user_groups = [] + for user_group_id in groups: + user_groups.extend(UserGroup.objects.filter(id=user_group_id)) - server_add_user(username, password, ssh_key_pwd) - if LDAP_ENABLE: - ldap_add_user(username, ldap_pwd) - mail_title = u'恭喜你的跳板机用户添加成功 Jumpserver' - mail_msg = """ - Hi, %s - 您的用户名: %s - 您的部门: %s - 您的角色: %s - 您的web登录密码: %s - 您的ssh密钥文件密码: %s - 密钥下载地址: http://%s:%s/juser/down_key/?id=%s - 说明: 请登陆后再下载密钥! - """ % (name, username, dept.name, user_role.get(role_post, ''), - password, ssh_key_pwd, SEND_IP, SEND_PORT, user.id) - - except Exception, e: + except IndexError, e: error = u'添加用户 %s 失败 %s ' % (username, e) try: db_del_user(username) server_del_user(username) - if LDAP_ENABLE: - ldap_del_user(username) except Exception: pass else: - send_mail(mail_title, mail_msg, MAIL_FROM, [email], fail_silently=False) - msg = u'添加用户 %s 成功! 用户密码已发送到 %s 邮箱!' % (username, email) - return render_to_response('juser/user_add.html', locals(), context_instance=RequestContext(request)) + if MAIL_ENABLE and send_mail_need: + user_add_mail(user, kwargs=locals()) + msg = get_display_msg(user, password, ssh_key_pwd, ssh_key_login_need, send_mail_need) + return my_render('juser/user_add.html', locals(), request) -@require_admin -def user_add_adm(request): - error = '' - msg = '' - header_title, path1, path2 = '添加用户', '用户管理', '添加用户' - user, dept = get_session_user_dept(request) - group_all = dept.usergroup_set.all() - - if request.method == 'POST': - username = request.POST.get('username', '') - password = gen_rand_pwd(16) - name = request.POST.get('name', '') - email = request.POST.get('email', '') - groups = request.POST.getlist('groups', []) - ssh_key_pwd = gen_rand_pwd(16) - is_active = True if request.POST.get('is_active', '1') == '1' else False - ldap_pwd = gen_rand_pwd(16) - - try: - if '' in [username, password, ssh_key_pwd, name, groups, is_active]: - error = u'带*内容不能为空' - raise AddError - user = User.objects.filter(username=username) - if user: - error = u'用户 %s 已存在' % username - raise AddError - - except AddError: - pass - else: - try: - user = db_add_user(username=username, - password=md5_crypt(password), - name=name, email=email, dept=dept, - groups=groups, role='CU', - ssh_key_pwd=md5_crypt(ssh_key_pwd), - ldap_pwd=CRYPTOR.encrypt(ldap_pwd), - is_active=is_active, - date_joined=datetime.datetime.now()) - - server_add_user(username, password, ssh_key_pwd) - if LDAP_ENABLE: - ldap_add_user(username, ldap_pwd) - - except Exception, e: - error = u'添加用户 %s 失败 %s ' % (username, e) - try: - db_del_user(username) - server_del_user(username) - if LDAP_ENABLE: - ldap_del_user(username) - except Exception: - pass - else: - mail_title = u'恭喜你的跳板机用户添加成功 Jumpserver' - mail_msg = """ - Hi, %s - 您的用户名: %s - 您的部门: %s - 您的角色: %s - 您的web登录密码: %s - 您的ssh密钥文件密码: %s - 密钥下载地址: http://%s:%s/juser/down_key/?id=%s - 说明: 请登陆后再下载密钥! - """ % (name, username, dept.name, '普通用户', - password, ssh_key_pwd, SEND_IP, SEND_PORT, user.id) - send_mail(mail_title, mail_msg, MAIL_FROM, [email], fail_silently=False) - msg = u'添加用户 %s 成功! 用户密码已发送到 %s 邮箱!' % (username, email) - - return render_to_response('juser/user_add.html', locals(), context_instance=RequestContext(request)) - - -@require_super_user +@require_role(role='super') def user_list(request): user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'} header_title, path1, path2 = '查看用户', '用户管理', '用户列表' keyword = request.GET.get('keyword', '') gid = request.GET.get('gid', '') - did = request.GET.get('did', '') - contact_list = User.objects.all().order_by('name') + users_list = User.objects.all().order_by('username') if gid: user_group = UserGroup.objects.filter(id=gid) if user_group: user_group = user_group[0] - contact_list = user_group.user_set.all() - - if did: - dept = DEPT.objects.filter(id=did) - if dept: - dept = dept[0] - contact_list = dept.user_set.all().order_by('name') + users_list = user_group.user_set.all() if keyword: - contact_list = contact_list.filter(Q(username__icontains=keyword) | Q(name__icontains=keyword)).order_by('name') + users_list = users_list.filter(Q(username__icontains=keyword) | Q(name__icontains=keyword)).order_by('username') - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) + users_list, p, users, page_range, current_page, show_first, show_end = pages(users_list, request) - return render_to_response('juser/user_list.html', locals(), context_instance=RequestContext(request)) + return my_render('juser/user_list.html', locals(), request) -@require_admin -def user_list_adm(request): - user_role = {'SU': u'超级管理员', 'GA': u'组管理员', 'CU': u'普通用户'} - header_title, path1, path2 = '查看用户', '用户管理', '用户列表' - keyword = request.GET.get('keyword', '') - user, dept = get_session_user_dept(request) - gid = request.GET.get('gid', '') - contact_list = dept.user_set.all().order_by('name') - - if gid: - if not validate(request, user_group=[gid]): - return HttpResponseRedirect('/juser/user_list/') - user_group = UserGroup.objects.filter(id=gid) - if user_group: - user_group = user_group[0] - contact_list = user_group.user_set.all() - - if keyword: - contact_list = contact_list.filter(Q(username__icontains=keyword) | Q(name__icontains=keyword)).order_by('name') - - contact_list, p, contacts, page_range, current_page, show_first, show_end = pages(contact_list, request) - - return render_to_response('juser/user_list.html', locals(), context_instance=RequestContext(request)) - - -@require_login +@require_role(role='user') def user_detail(request): - header_title, path1, path2 = '查看用户', '用户管理', '用户详情' + header_title, path1, path2 = '用户详情', '用户管理', '用户详情' if request.session.get('role_id') == 0: - user_id = request.session.get('user_id') + user_id = request.user.id else: user_id = request.GET.get('id', '') - if request.session.get('role_id') == 1: - user, dept = get_session_user_dept(request) - if not validate(request, user=[user_id]): - return HttpResponseRedirect('/') - if not user_id: - return HttpResponseRedirect('/juser/user_list/') - user = User.objects.filter(id=user_id) - if user: - user = user[0] - asset_group_permed = user_perm_group_api(user) - logs_last = Log.objects.filter(user=user.name).order_by('-start_time')[0:10] - logs_all = Log.objects.filter(user=user.name).order_by('-start_time') - logs_num = len(logs_all) + user = get_object(User, id=user_id) + if not user: + return HttpResponseRedirect(reverse('user_list')) - return render_to_response('juser/user_detail.html', locals(), context_instance=RequestContext(request)) + user_perm_info = get_group_user_perm(user) + role_assets = user_perm_info.get('role') + user_log_ten = Log.objects.filter(user=user.username).order_by('id')[0:10] + user_log_last = Log.objects.filter(user=user.username).order_by('id')[0:50] + user_log_last_num = len(user_log_last) + + return my_render('juser/user_detail.html', locals(), request) -@require_admin +@require_role(role='admin') def user_del(request): - user_id = request.GET.get('id', '') - if not user_id: - return HttpResponseRedirect('/juser/user_list/') + if request.method == "GET": + user_ids = request.GET.get('id', '') + user_id_list = user_ids.split(',') + elif request.method == "POST": + user_ids = request.POST.get('id', '') + user_id_list = user_ids.split(',') + else: + return HttpResponse('错误请求') - if request.session.get('role_id', '') == '1': - if not validate(request, user=[user_id]): - return HttpResponseRedirect('/juser/user_list/') - - user = User.objects.filter(id=user_id) - if user and user[0].username != 'admin': - user = user[0] - user.delete() - server_del_user(user.username) - if LDAP_ENABLE: - ldap_del_user(user.username) - return HttpResponseRedirect('/juser/user_list/') - - -@require_admin -def user_del_ajax(request): - user_ids = request.POST.get('ids') - user_ids = user_ids.split(',') - if request.session.get('role_id', '') == 1: - if not validate(request, user=user_ids): - return "error" - for user_id in user_ids: - user = User.objects.filter(id=user_id) - if user and user[0].username != 'admin': - user = user[0] + for user_id in user_id_list: + user = get_object(User, id=user_id) + if user and user.username != 'admin': + logger.debug(u"删除用户 %s " % user.username) + bash('userdel -r %s' % user.username) user.delete() - server_del_user(user.username) - if LDAP_ENABLE: - ldap_del_user(user.username) - return HttpResponse('删除成功') -@require_super_user +@require_role('admin') +def send_mail_retry(request): + uuid_r = request.GET.get('uuid', '1') + user = get_object(User, uuid=uuid_r) + msg = u""" + 跳板机地址: %s + 用户名:%s + 重设密码:%s/juser/password/forget/ + 请登录web点击个人信息页面重新生成ssh密钥 + """ % (URL, user.username, URL) + + try: + send_mail(u'邮件重发', msg, MAIL_FROM, [user.email], fail_silently=False) + except IndexError: + return Http404 + return HttpResponse('发送成功') + + +@defend_attack +def forget_password(request): + if request.method == 'POST': + defend_attack(request) + email = request.POST.get('email', '') + username = request.POST.get('username', '') + name = request.POST.get('name', '') + user = get_object(User, username=username, email=email, name=name) + if user: + timestamp = int(time.time()) + hash_encode = PyCrypt.md5_crypt(str(user.uuid) + str(timestamp) + KEY) + msg = u""" + Hi %s, 请点击下面链接重设密码! + %s/juser/password/reset/?uuid=%s×tamp=%s&hash=%s + """ % (user.name, URL, user.uuid, timestamp, hash_encode) + send_mail('忘记跳板机密码', msg, MAIL_FROM, [email], fail_silently=False) + msg = u'请登陆邮箱,点击邮件重设密码' + return http_success(request, msg) + else: + error = u'用户不存在或邮件地址错误' + + return render_to_response('juser/forget_password.html', locals()) + + +@defend_attack +def reset_password(request): + uuid_r = request.GET.get('uuid', '') + timestamp = request.GET.get('timestamp', '') + hash_encode = request.GET.get('hash', '') + action = '/juser/password/reset/?uuid=%s×tamp=%s&hash=%s' % (uuid_r, timestamp, hash_encode) + + if request.method == 'POST': + password = request.POST.get('password') + password_confirm = request.POST.get('password_confirm') + print password, password_confirm + if password != password_confirm: + return HttpResponse('密码不匹配') + else: + user = get_object(User, uuid=uuid_r) + if user: + user.password = PyCrypt.md5_crypt(password) + user.save() + return http_success(request, u'密码重设成功') + else: + return HttpResponse('用户不存在') + + if hash_encode == PyCrypt.md5_crypt(uuid_r + timestamp + KEY): + if int(time.time()) - int(timestamp) > 600: + return http_error(request, u'链接已超时') + else: + return render_to_response('juser/reset_password.html', locals()) + + return http_error(request, u'错误请求') + + +@require_role(role='super') def user_edit(request): - header_title, path1, path2 = '编辑用户', '用户管理', '用户编辑' + header_title, path1, path2 = '编辑用户', '用户管理', '编辑用户' if request.method == 'GET': user_id = request.GET.get('id', '') if not user_id: - return HttpResponseRedirect('/') + return HttpResponseRedirect(reverse('index')) - user_role = {'SU': u'超级管理员', 'DA': u'部门管理员', 'CU': u'普通用户'} - user = User.objects.filter(id=user_id) - dept_all = DEPT.objects.all() + user_role = {'SU': u'超级管理员', 'CU': u'普通用户'} + user = get_object(User, id=user_id) group_all = UserGroup.objects.all() if user: - user = user[0] groups_str = ' '.join([str(group.id) for group in user.group.all()]) + admin_groups_str = ' '.join([str(admin_group.group.id) for admin_group in user.admingroup_set.all()]) else: - user_id = request.POST.get('user_id', '') + user_id = request.GET.get('id', '') password = request.POST.get('password', '') name = request.POST.get('name', '') email = request.POST.get('email', '') - dept_id = request.POST.get('dept_id') groups = request.POST.getlist('groups', []) role_post = request.POST.get('role', 'CU') - ssh_key_pwd = request.POST.get('ssh_key_pwd', '') - is_active = True if request.POST.get('is_active', '1') == '1' else False - - user_role = {'SU': u'超级管理员', 'DA': u'部门管理员', 'CU': u'普通用户'} - dept = DEPT.objects.filter(id=dept_id) - if dept: - dept = dept[0] - else: - dept = DEPT.objects.get(id='2') + admin_groups = request.POST.getlist('admin_groups', []) + extra = request.POST.getlist('extra', []) + is_active = True if '0' in extra else False + email_need = True if '2' in extra else False + user_role = {'SU': u'超级管理员', 'GA': u'部门管理员', 'CU': u'普通用户'} if user_id: - user = User.objects.filter(id=user_id) - if user: - user = user[0] + user = get_object(User, id=user_id) else: - return HttpResponseRedirect('/juser/user_list/') + return HttpResponseRedirect(reverse('user_list')) - if password != user.password: - password = md5_crypt(password) - - if ssh_key_pwd != user.ssh_key_pwd: - gen_ssh_key(user.username, ssh_key_pwd) - ssh_key_pwd = CRYPTOR.encrypt(ssh_key_pwd) + if password != '': + password_decode = password + else: + password_decode = None db_update_user(user_id=user_id, password=password, name=name, email=email, groups=groups, - dept=dept, + admin_groups=admin_groups, role=role_post, - is_active=is_active, - ssh_key_pwd=ssh_key_pwd) + is_active=is_active) - return HttpResponseRedirect('/juser/user_list/') + if email_need: + msg = u""" + Hi %s: + 您的信息已修改,请登录跳板机查看详细信息 + 地址:%s + 用户名: %s + 密码:%s (如果密码为None代表密码为原密码) + 权限::%s - return render_to_response('juser/user_edit.html', locals(), context_instance=RequestContext(request)) - - -@require_admin -def user_edit_adm(request): - header_title, path1, path2 = '编辑用户', '用户管理', '用户编辑' - user, dept = get_session_user_dept(request) - if request.method == 'GET': - user_id = request.GET.get('id', '') - if not user_id: - return HttpResponseRedirect('/juser/user_list/') - - if not validate(request, user=[user_id]): - return HttpResponseRedirect('/juser/user_list/') - - user = User.objects.filter(id=user_id) - dept_all = DEPT.objects.all() - group_all = dept.usergroup_set.all() - if user: - user = user[0] - groups_str = ' '.join([str(group.id) for group in user.group.all()]) - - else: - user_id = request.POST.get('user_id', '') - password = request.POST.get('password', '') - name = request.POST.get('name', '') - email = request.POST.get('email', '') - groups = request.POST.getlist('groups', []) - ssh_key_pwd = request.POST.get('ssh_key_pwd', '') - is_active = True if request.POST.get('is_active', '1') == '1' else False - - if not validate(request, user=[user_id], user_group=groups): - return HttpResponseRedirect('/juser/user_edit/') - if user_id: - user = User.objects.filter(id=user_id) - if user: - user = user[0] - else: - return HttpResponseRedirect('/juser/user_list/') - - if password != user.password: - password = md5_crypt(password) - - if ssh_key_pwd != user.ssh_key_pwd: - ssh_key_pwd = CRYPTOR.encrypt(ssh_key_pwd) - - db_update_user(user_id=user_id, - password=password, - name=name, - email=email, - groups=groups, - is_active=is_active, - ssh_key_pwd=ssh_key_pwd) - - return HttpResponseRedirect('/juser/user_list/') - - return render_to_response('juser/user_edit.html', locals(), context_instance=RequestContext(request)) + """ % (user.name, URL, user.username, password_decode, user_role.get(role_post, u'')) + send_mail('您的信息已修改', msg, MAIL_FROM, [email], fail_silently=False) + + return HttpResponseRedirect(reverse('user_list')) + return my_render('juser/user_edit.html', locals(), request) +@require_role('user') def profile(request): - user_id = request.session.get('user_id') + user_id = request.user.id if not user_id: - return HttpResponseRedirect('/') + return HttpResponseRedirect(reverse('index')) user = User.objects.get(id=user_id) - return render_to_response('juser/profile.html', locals(), context_instance=RequestContext(request)) + return my_render('juser/profile.html', locals(), request) -def chg_info(request): +def change_info(request): header_title, path1, path2 = '修改信息', '用户管理', '修改个人信息' - user_id = request.session.get('user_id') - user_set = User.objects.filter(id=user_id) + user_id = request.user.id + user = User.objects.get(id=user_id) error = '' - if user_set: - user = user_set[0] - else: - return HttpResponseRedirect('/') + if not user: + return HttpResponseRedirect(reverse('index')) if request.method == 'POST': name = request.POST.get('name', '') password = request.POST.get('password', '') - ssh_key_pwd = request.POST.get('ssh_key_pwd', '') email = request.POST.get('email', '') - if '' in [name, password, ssh_key_pwd, email]: + if '' in [name, email]: error = '不能为空' - if len(password) < 6 or len(ssh_key_pwd) < 6: - error = '密码须大于6位' - if not error: - if password != user.password: - password = md5_crypt(password) - - if ssh_key_pwd != user.ssh_key_pwd: - gen_ssh_key(user.username, ssh_key_pwd) - ssh_key_pwd = md5_crypt(ssh_key_pwd) - - user_set.update(name=name, password=password, ssh_key_pwd=ssh_key_pwd, email=email) + User.objects.filter(id=user_id).update(name=name, email=email) + if len(password) > 0: + user.set_password(password) + user.save() msg = '修改成功' - return render_to_response('juser/chg_info.html', locals(), context_instance=RequestContext(request)) + return my_render('juser/change_info.html', locals(), request) +@require_role(role='user') +def regen_ssh_key(request): + uuid_r = request.GET.get('uuid', '') + user = get_object(User, uuid=uuid_r) + if not user: + return HttpResponse('没有该用户') + + username = user.username + ssh_key_pass = PyCrypt.gen_rand_pass(16) + gen_ssh_key(username, ssh_key_pass) + return HttpResponse('ssh密钥已生成,密码为 %s, 请到下载页面下载' % ssh_key_pass) - -@require_login +@require_role(role='user') def down_key(request): - user_id = '' - if is_super_user(request): - user_id = request.GET.get('id') + if is_role_request(request, 'super'): + uuid_r = request.GET.get('uuid', '') + else: + uuid_r = request.user.uuid - if is_group_admin(request): - user_id = request.GET.get('id') - if not validate(request, user=[user_id]): - user_id = request.session.get('user_id') - - if is_common_user(request): - user_id = request.session.get('user_id') - - if user_id: - user = User.objects.filter(id=user_id) + if uuid_r: + user = get_object(User, uuid=uuid_r) if user: - user = user[0] username = user.username - private_key_dir = os.path.join(BASE_DIR, 'keys/jumpserver/') - private_key_file = os.path.join(private_key_dir, username+".pem") + private_key_file = os.path.join(KEY_DIR, 'user', username+'.pem') + print private_key_file if os.path.isfile(private_key_file): f = open(private_key_file) data = f.read() @@ -1052,5 +791,5 @@ def down_key(request): response = HttpResponse(data, content_type='application/octet-stream') response['Content-Disposition'] = 'attachment; filename=%s' % os.path.basename(private_key_file) return response + return HttpResponse('No Key File. Contact Admin.') - return HttpResponse('No Key File. Contact Admin.') \ No newline at end of file diff --git a/manage.py b/manage.py old mode 100644 new mode 100755 diff --git a/service.sh b/service.sh old mode 100644 new mode 100755 index 33b1a95e1..64791a59a --- a/service.sh +++ b/service.sh @@ -7,7 +7,7 @@ # Date: 2015-04-12 # Version: 2.0.0 # Site: http://www.jumpserver.org -# Author: jumpserver group +# Author: Jumpserver Team . /etc/init.d/functions export PATH=/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:/opt/node/bin @@ -26,17 +26,16 @@ start() { success "$jump_start" else daemon python $base_dir/manage.py runserver 0.0.0.0:80 &>> /tmp/jumpserver.log 2>&1 & - daemon python $base_dir/log_handler.py &> /dev/null 2>&1 & - cd $base_dir/websocket/;daemon node index.js &> /dev/null 2>&1 & + daemon python $base_dir/run_websocket.py &> /dev/null 2>&1 & sleep 2 echo -n "$jump_start" nums=0 - for i in manage.py log_handler.py index.js;do - ps aux | grep "$i" | grep -v 'grep' &> /dev/null && let nums+=1 + for i in manage.py run_websocket.py;do + ps aux | grep "$i" | grep -v 'grep' &> /dev/null && let nums+=1 || echo "$i not running" done - if [ "x$nums" == "x3" ];then + if [ "x$nums" == "x2" ];then success "$jump_start" touch "$lockfile" echo @@ -44,7 +43,6 @@ start() { failure "$jump_start" echo fi - fi @@ -56,7 +54,7 @@ stop() { echo -n $"Stopping ${PROC_NAME} service:" if [ -e $lockfile ];then - ps aux | grep -E 'manage.py|log_handler.py|index.js' | grep -v grep | awk '{print $2}' | xargs kill -9 &> /dev/null + ps aux | grep -E 'manage.py|run_websocket.py' | grep -v grep | awk '{print $2}' | xargs kill -9 &> /dev/null ret=$? if [ $ret -eq 0 ]; then @@ -104,9 +102,3 @@ esac - - - - - - diff --git a/static/.DS_Store b/static/.DS_Store deleted file mode 100644 index 1f949c21808d87add5ebb949967a44aac6745b10..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 6148 zcmeHKUrXaa5Z|r6Gu4+IcZ%T4z6yQl2}iuQ81+#oL@J`vOGvn`grqd7c{Z^ z*AV>@eiC1u+1(bUcW;MCnF+JM+1Z(0_Ls1eWsGsJAAM)cW{g>&h?N>Ne-RuWz(RkvOx$39S^HyzP5g+@@!m;1U z27VC~{oD(BhpH!|I5c)MC^}<%FUyjN9fU0z58C#(tt>49nZ~_WsYm-Vjk1B4_0uRn z^7VZCGAcT4d#TxrPa(A#q;X5;GL(rd{#Z49+V=8rxbFO{i^ixfhR()%T{w<28rf%- zwYv6WbN8S-I={HQy1u!+yMNI9bNE!1d^I?RM=%r>r+zO@vh+9f$FbuWLSldzAO>cF z0du%ni!-qs+5|B`4E#?9@O%)Uh_1m>qdGdE!QV$5uOgy=jduw|VbC>LYJ?FGu2TVZ zDmPaQuG7IUOq^@5)Tq-LS1ZFjW@T<}C|s=$exbq{cQsN=3=jiv8JN>u8|(khzwiHV zlc+}w5Cb#C0IzQQ+a7Gm)YhrZVXYOQzd%tiuGBb50YjByh{aO80;&Z30u4adV5t#2 QAoL@kXrP7|cvl8q08m3=$N&HU diff --git a/static/css/style.css b/static/css/style.css index 5b0e5745d..2dc7a2591 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -2822,7 +2822,9 @@ body.body-small .footer.fixed { .table > thead > tr > td, .table > tbody > tr > td, .table > tfoot > tr > td { - border-top: 1px solid #e7eaec; + /*border-top: 1px solid #e7eaec;*/ + border-bottom: 1px solid #e7eaec; + border-top: none; line-height: 1.42857; padding: 8px; vertical-align: top; @@ -3516,8 +3518,8 @@ body.modal-open { z-index: 100; } .lockscreen.middle-box { - width: 200px; - margin-left: -100px; + width: 400px; + margin-left: -200px; margin-top: -190px; } .loginscreen.middle-box { @@ -4562,3 +4564,10 @@ body.skin-3 { .red-fonts { color: #ed5565; } + +.form-group.required .control-label:after { + content: " *"; + color: red; +} + +.n-invalid {border: 1px solid #f00;} diff --git a/static/img/root.png b/static/img/root.png index aaa37e666d5c719f372f5122e298ae5d949167fb..0d07dbf97fc3e812381f51b00265b58d33b89e85 100644 GIT binary patch literal 108909 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

h1Z67I3z`{DnPEud3Kwq}b5hcIq#)mA_P$fy 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 O9_q^4O79e`p8N;x375$L literal 107108 zcmV)5K*_&}P)aT19y1NS&2c z-CbR+&}d*5BsjKw7#i1>FZ+l0Wo>Ds(TD+-J2M;rt*T3;&d|cO*z4^7Htc z_@}&imG76IseZnEr~Fp+Amw?*&n(OEj`(NoQt>Bz1#i~Q#aEk)Z`IDSeO2XQ#d-0< zVfkaJZZzUJkYx>D6@SuoRo8WVYwgYROtl?d)3h|jXh@o2G%bg&Uy{=#OwuULlPpUm zIm7Wy)3hu@*G+sLp9z9s<*!stTX}0aPEE&dSPjE4@G;Z2{Z()|y~%>~-~PkD|A&A0 z58ZxOQPfnHkfp z_TWKOo@0W0FFzw*GCxFIEq-b;{Vczp7#8!rTlC8<_9medbAIo zapwobU&X@^zXMdVR06*EF76NLH7(uqJXKY6OK&tBG0L{>^!t502Pw^hI8ZW0)iu{_ z*bV@xO{Y_Q3p0Q1EvM5cO=Fe7u675VcCV@Dau~;p>0&bREZZ0i2D;UF_1W{~Dj3~d z{NMlA|JmE?wcL)Z7+I<+hS|We3{zF`obx0}XijTdxftPPVGS@0P1PNzsT#VJg^F%U z02|E>x?0SiRL(WrF-`rnnEJf<;xUSeLJt%_0ZT~}(;KUUSPI1u@|fKJUISz~TS_Qy zf2O*KY)>2hF0Qx^a`6H2i#UFDX4YoQ_IW>Yw(nSCL`>GeVdcTm{yT27I^UvmYEvGi6y7T;ni_3sjvD^ z@zl!?C&Vg>;N)^F#69ASf(fH25@(Tf1#=rn(-d9d7bZ16fPZF5Z0eZzl4aUNtZ~wE z8?9DL)$+w;rl`_tF$ZQkU3V2MJU=@)96o>XEDXK%I*1dU?@k4Bbe*mi@K7by)V2Pg z<3^6>`R~rpU%q@fosNSr4nrLSd3|#k_)#Jyj@6Y_xnaAOW8>PxDDk}&=r@k>$q=K2 zUw~3z+qUiHdSz?)tlMo350CfuhkN)Iu-YDUqIIZSrlx997=r~AS|%j$fvPru^<2q# zlo*?UrJNFi>AB0Qs)HOPTC)UrR36c+CVZ;TuJZe?#k4a0s+22`AoGN)fxpCU3GCt* zd?2fTpqvji3gOQ@wc?jwyq{5|DrA~Gxj5Hm4%3BavRs|XgxP35 zx7$tMU-`k(_km&`2gF&DJj^3C2tCa*n}($V&~j$knrbxNmi6xZ9e`_Cx&>^^r5 zFib@wizL)FTT_(Pa(;Pv30atDzo&)pQ$fQ!(_z*(%6W zf4=l_*!dh54G!6AT7&Mui+z9LsY>cLo87J($LU?Lur(`9{3KK|HCOV~u{C_wZCIA# z1TY+Nj7Ue7^tepzHQ%kYEwzz^4KhR8_K+z!1EO6BF|& zDZ+4~DM~}3V)+t_Ab#hY5;kYCES68UwBccH;iq`MLcdgDvs2CDm0_!v_pPD%S z&RKUqrDdriiL<}ScB_#}jjLn8CQ!SY z4xmYK0%RI-5?EGeZ*PdXIvU+TQd)){1}m6zx~?Uls&K8SblBPaE=@qVnXbtV7^V=E z&8B5JnxTY#oXC+Tn|Ml^LF>-=ZZ!86pTGRV!2ujAia=KRdcF2mtJQjP{^l((Y#B`; ze0q2L^Y;M`1tE<~4}(C8A|3d{B_>J`Mc!hTY7sFw?pud+%>l~5n@e(dJ$vs=90~eY z4${5Ag7(D)Sx%bCsRE-ji_?ThG7~;O)=5}va-kinlYS#Xaxp=-jhFISR2_SzkSQO1 z#)`SJ^cP1US(rcIu9CEC9-7L7e(?0G9Q85C+=j#b`^90mP1*aOPjhB{z1V2=dpmRI z67@lr-#Oyj@Ge&=F>tj-ai`|qxh`xiPIcsquWuXu<-@C48db+&;c#P{wtAxbcVsY? z5@xalGE!AombPU=E+$c)q=BKrhBI_EpDkz0`4|&OX*7e?io~s&313PSq)?W#@ub!2 zBq6?*!=5B?XX$D&TZcZ`nmDo&lM^OVb;Gb6!*1-oKYt6bC^A@Cv!E0#6QByibT+xx zbU8A~qy$u96-#+)nffoUf5K%0_KbOY;fs!C0_U;@paD0sp(`mQh^!^4)oi+8yvvI> zt6=`#eG8S_?RH!J?*8yuM$3~H#qq|Ul=03H%5dS2B`9{?Q89wsmX? z$a`tC>oWy(#S$P(H4xU8{@T11^ZNvDHXh@BSQ2ffl&CtG#7tuA$V32*zhHm4beJZdmcqi9Fus`Uwcb@H+1YK`&yx#JFTVzKu*V# z*>s|-TD$4yN%s23pB&3hk~H(fRhr#hUgM@**EMac+inAyQYytM#Aqff29Rl3CKemm zkzu;NyIFyIWnH77j?*~%XikYb#?5NI2!er?O zs(St&+Ku&SL~Y{LI8eMpqe<_lwYge-=v7YJwh60e`FKC=wy&{oAdK_bY~lxN3+$_ z!_y|Ut(MERzp^Y_Hx$D#4-XIGIJvvKef|3F z_4PFYTb9ExST0vE;P49$v{5?4Ch z+v^VoxLTNqSW3>GKgUY4SS|5Mi~?2`8-D=_0`QIrjS23Ck;pEGTvJVohN^lN$4fF{ z7ipZN058s)B)E*}=UHCO>RsatQR?DFQ_8Nps>C&iYL^J z4$5J`$SOs|twOVf`jX0bb4qxikyZf#%7dVYKK&$g?$nuEijF=fkh#+)+WJ=h(Pn4L zzWD-~h1?b&pj+YpmsLKmK&h;#MY2am=?U3RWR_|clbh}mw#}>4vs}iQS`=tzam!;1 zicrh{7_*AR|6yIke}oxH-h`JoXW+pVKS?eE7&$!%*iJeH0VyJuSNOR)G&S<9@norT zL1iUpZaz=B6ueaDf=X2yh>RCz5qFh%=_1n#zlb=KOx^^DIv9fBJt7dC59EMB1V1^A z2Cfv0VZnej^znGCQ6h&1_+grW06{;nHZh5DPF<6eI9#tM8Y!+|Ih&Za+HzZWx7R={ zc&yXu*j5AgF`F&!?nZ&ARKnp|CtC!@*a zhWxd<8taM8V>PXU+)09HJ*c4}p>v(jpHg$LuQ7 z8)zBbP?ta(z?BI{3Z+lcC~^3(9+;`dz)Fzuv&h)`$g57l?K!qldcV;0)lHloJ`Q_*R{@&H~B_uH*I+;#Do-J6ho%a60 z0scFEcDjGC-|cr=Ztv#$4tg3kC(eb#+OUT02G*L%bh=)yXN!5@2Rf#ep@)7jpUzj_ zEbzQEid53@6vYkw)q1hGxV)H6M^TVq^)oPnnj+>KJ`8X|ITQNQj4>rK&GAs?#e8AS zDisq@5+Y3zL4qPdPrg@{nLrMHp$HK1C|kDK&UoGfKTt`;?B;#i>$CCb$oIll+K#@| z|1AUo#~ts#%gAD+(una&#J3|jbCd3w6|0AwSJbgu@c@zbC8m0HddwDrA+jRn%?cpq z@h)(x7{^j%FF}+V%w0QW0uWgjYdD=%6w`w))AM zUCO_xe5VT7P}34A(El=@DmzS!C1&? zjH#Ms9)xJ<4+eWU>;iV$YUa9~PS^ET!DuvwVF{fHNTj4_a!A~Mzjwy3{i6e19_%{O zx0wuyXIMaJ z+*aFaI-QK}FZJR8cz}rLwEB?CX&6ry%bV+)i>r(AWCp9$(skRlFqlLrX@dVV@C8aL zHhBdXgagXV#dN0VC@j@nP05$0i}+;uq=-W~E?SKf2gYP}MFiuV3P2OeYVCSsLYhOd2VTWNIdyK%9siH=ITiMR>9> zz{o1qF=qpCZ4i3&c|%?;R#8mO9$eo3{&2pSffhl1%|;7=pG>CsCytK;UR+$_0>NvY zZre0X;O+YA_LsM>!Dd+=lIN4w3d(bc!f-N~KnVl7PSb&biVK@Bpoi}?&HVI}S8zH7gv`=SHa#h+`zGB94Rywgy6`3Nu!i<^bo0TA!N`G>` z@z)fbSg6@oC&xS^HI@?hV^njKK4wNgZH^b;RX8rq>x-nwHOKF^`n^wp+;S;ZEzHUl z7G8yIQmv}liieuSA?bgxAi05nMyl5kJP8S!Q@H>^G>!wnQPXwDvasCwD~~e3*$oEH z!Q957V582V3H=qQ4N4M3cYc0hS~_e?$7zINJe^K3_; z*mTUFe*D?@*WeGxRGek)tzn6Fd+od1v2ELpX1hP=_PTq4?*r#iD!ytqV2Dx2dT`y+D#(^KggtSb%({6(U!I1m=`_RTw z7-0#R&1PW~v9vbu*V$|eS_3mgNgQ}V6os^w=^E}Ex~lECSS+CFXA59{zFMzFceim8 zH7P#oU=CqfNuw~6Q`mb6d+;T8vI&z8>_>U@3VC+K)U1Uus{v18*BP+HB(mx6E5Zlz!q2#ek)npcK>Xfl%R@d?iNp^(X^#ZU#%8?6oNofN~TF%w~ephQGw0+J?Pb*({P|D_V)KI z%K{hSj9%a^77O3^fdp^u|M=sNv-v`&ta=Fo_~hgS68Gff?1vwIz-9mDH^0S|zkmN8 zoC&Z2SHM5s$L}}=Ww+-Es1gSU=E1kq=`;u_xQe^$u!zMXLRoJ)q_02c*K{^JK0LH+ z!_cgpoW=xcj066+VL2F9jUA7GVifywMuG)$eKNjVua@9eEN+%#o3?3aS^^UTk49pQ z6tleWa%7}twOp#lEgx_o8!ChZR4G z!)z!zxqFf{rZo2qY#{s5l1z|1jl(#Mf~G_MQ<}$741igd69gXV*dj+vf{4W(0U@*L z0x-bu#Lsyifo+;i2v4wMqv;I#Lo5&&wKz_J96;sf_U`)XW-=M$D`2@4hF6+BfBpie z8jq)^rzeBK5IPwczPh@?p+K{sR@~RimoJZwPH;!SB%mcsIJ_p)(e3RWcoJvFDL?)6 zQydL^j<4W?7xTr{)%DHI$n(~^rggfY>z-x8;?0)JMc{i_mkiA?sKQjmr3As!)a})J zF&l$HC!rr<@d7?o!z5EGqpTr`OiN`Gu*1C0*#Ds9(r%qqJX!HRt67C1Uahp+sb)9G zk%ezolDDJr+p+HK5oDJ9B<)nQ+sG{9nKd=5${RzkEIJixww%BZkhxK_;7mqAzNKa# z_02wBK~jrP7Kg%UrtIM1FlhB?wZ(5|hQoFy?Sm*;mfbt|9@?wbizy#ZMh_y>v7l1~ zK*cJS@~W@Fs4{FKhA-!pB40w+lr$54H-rm6{r4e&IQEY89>Xf!BS z21H8iN6sF5xts%mfM3ixZpX*RS67#|?F@%|U{`zqe`swqaP6ryqaloa3)}R0UXPAzF90;?~Xn39nu0Gh` z-#grUc6zqAx7TX5Y$)@T%EWN{Q51nc!Ou7yFpsN(#Km>u7QphbR3Q^MwOtM42zonB zq@}-H&DWIYq$-wWDyozvm^qmOhSgMP=F7`>*Vor!=$ksO!6r>fjx||H$S0E(Gcqka znevYKN4;c(FhJZPIP+-(O4Mq|o2=PdRp(=QK93-C$2&z;ng}>#O5xQkktyFJQ_M#Z z&y*^hxjA4ZP@gM1kVzgO3IP`x3_?=Zz)k^&tSzW!>hg*5YNciMXEj5;$gKqw<;=)6tZoLZDoqiIFL{cCs4LlWTVJ!wvJDZ!I_xJtMwYj-gLTXwcO8NeRY2RzS-;mDv+|E zHOl}&H$iTiO2wVa#S-x8v^#(I+rOL7=D)oD1;(fE`M69NoVRzkFebsJLd%em*=h-D z#XEwxfzOETO#Fhg;;#TIrR=Jz9|WV(2*+uvV2bxG={{fGA~~20isNfRD4>kx}kQ`X3Yw%E?PV5Ma1#Spu0 z5*t#GO-O(A<20ZN$Jsw%Dsd=$9TOWs!bwFDHe-(}H|QrXF@^yc$1wUlLPbcb80U+k zZl?U&GHzk zfI6*E-;x52#JOzJZ_g`sk`(q9b7%foWyQV2mh~f9n0X}!=8+1LC)Mn3&dm8rh}nd1 zb^~MM;qE9s1(YBnqL}3{!c`k$l2kmz2==iA7Yyj=}&tI)?Mz??^ zlpt0i2v-cnYO!3dRuswXc1cmU+dv?u4v;C7ZNSNW!YD1wbu1;89tH_C2Sx2v-6jQL zNvU!-z11~aXb{k7W>c@toLzz07(?(OWYQAR922AT9-aV4Ayw9T2Nnk_j~Qp$Yn zI%jUAoK{7q$m~-;KRJ@_^K;blHg}4{nvM5F)mbfJSS}L$B>ABX-nL_FJ-#06qg6X? zm9DpG{9(j}{0|yoGtr?ZHFj%jVjQ7YP0wFJ5U#xi21|owX=pkHFhl5niqEGJh>f@{ zB+fMOHw8kDBI)2?iotM2ugF_iEEZyNW8TM6GMP-kQ6gJi*R_7X+v|1O?G7y1fA{bH z-m=Z*ay^|+a4ADd$7^AQ==FNAGY7-rfBmoj<>ch(?CkXQ>tB}3g=IINot<7?USoYd z*gu@lX8_oUUw3zRfE>)uufF=~lTSWrcgRVLAu>#h7(R+D+lCC_4o2$Rw{JnM01Unk zs)U_+a(ujhaDd;_JX_4?04kOhvA{tlU@ToI{#Gl--DVj}VnC9w%RT5;k);*C!-P86xVfhQ%|TB5tcI*%Nad!O;xx0)KsD-sfbaKotbBk zQDwyrEb3WAcHVZv&{iX&zkxG_-K%gPCBd1(fBS?7NZ6S*WU^Db@VwTLDeBhQH_KT7 zCE~Zk9@}~CAN0`HvWtaHuF(wMah%#pxgD0Q^%L4MFg8)_4GwqKB9v9y&`naxq3bk@ zp)IqxY@vdQ2&_ViV#K_4uv#x5STUf06R-rMj@XiW?SPQLps@F_qQg$aOqD1#N;OR* z~>&d$0%^tmAH24kw{Fn zRI;jOpKjY%tz@m~;hLAI>ZirJ3p0^@_CRSy$tp#+IGk7E=%%)*ou*e5`w)L%OWXuz zWKlK%+ljw|pN6W&!~%3uurA*81K+kS914~K*onL*rrBsZ-A)gNbe}dhICqKR$DVhp}4QK&r zv-zBRc~P*)nV#LI9Bm6*X zTt^(O)~oq+26iRY+H|1dR;v}RU)8Djls_1gtWQ%fj9`o^SkZK>M-J}G3j?fUP1kli zy~T8l1!!;3H%#sA`P;jjTO0wpUD6e-YefM?r6cK%ToG2Y#F?8Y%g$Kc!>EbEnFdO> zOT_*Eb9fHT*}-@Kmn+%LEiLaJFrW z#&Z}G!_e7#Npr#vyf_Z1!Z8lx{(VfLl!M#bTM^`h#>U!#@d2KRJm>Q;^1y-g$TS@$t8Ljt0J^%GuX=<1 z)8jKhFz^FF4i>fDY$j0x4XdkqnoI0eo+VV{rkJ|kXt}1QdTXjv?X+4@y03qEldF0r zDJF~o?o=og{>~bA;c=l1Ao(7d`NM+Bx}cRLrBb8)!R~W^D`yh^IQKyj*R@m4%J(G} zM4+NvE~UnVd0FErSJkX^pG`=`fsrk5=Y0a|J?Kp)7o}vh+xMzH(*=->mq#(?ojM-R8sb%=?K~ z6sxg_Wu>VU#WNzkPQFKG>9eJnj}+&yq44E|^9pdYiEv()c5#_$l+yhx>KCUcB0?%r zl7?Q!Giwf*nmenODPAsGWp~X7kmC$q8Vlm=d*iR;F@v^;QhJC$OUa($vSCf@@tD2$+0>G5C@55mA(ErA}LAgbDy zp=enX6I~|G*SX9(ff$CN1<6e{D>`gYL#39Jl+~GLmStd$x{U@dtlR0jZFfAn;{de+ z!w=xrHJE!QWogrM$RHxy?ZIlb?sxXDu5V`Z<>kd_zFPe5_rIS`=dz+gOT#36@%*#@ z_TT^8;lWX7*u}i>_Iv;HFW&=xQJf6>LlE_NG_eVX9gNy)I*+o{$DOQ~3PHleN^*RB zs;kP|cNahiG%Y|#1}7!Nx4>QF$pqv@*0iZN&8FLO^EeNq!1p8Db`A~>my7v$JUu%* zu^Wx^^LG#sPQ!uue0h0^r7Z^!AMDSjvp5Jpd-++r>3Tu1S}gpK*cZkq7&YeXOi9bJ zhK=s7B5%1zITu}6?Qs%0uFD-nDgTdh!>Lmqd0P}wGe+5^+I= z+EbY`8=}rsRjXvJ<+Zq=qEwnrLC0oA!c4|tpNvwIB(C+P6l+eU=2l6vD08OTwTx<$ z@imQ}H)&e%E%i)Q3j(DaMzXfKG>$oBlLN$u5mlx;>OZRS#v0>Qn@ZMlIPNnFSod>w z-o2XL*~+S^D#T2qFrvl>5;$7RRf+jgB6-Z`Ng;-l6jQNj zP%ApjR4P@$L(J(3Q->h#sUT%3nPOLRI{#1q>3_c&kH)uS z-85kSefi~=SGPC&!~Mx}(cS9}TD?F2_0O>%;LZQ#x8F^tv*~DLnwrG@nr+8~6#;Hs ztriA##B;H9Lmu0Xad$g{l-(N+7t7glu{=1SGPJW7FDUT{0#r3{1@qaW;Woee@@s(h z`uZ9}&7nEA)b1ukEk>y5$8tUx{3H z=-ZT2Rgx2w`<@25l-CsRu6p2mTX937PpR3RS-2aMO1bYgB9%FCL|0U1LJPOV9kk_K zaHgOQQH&y7wNx_!3t20Cv*K8yhNa3{+l9+d^jxpzF+N~a(KegaMgBx+u(Bjq zw+UZSLcS2sUsL<@6sA|6YqU>6lydDK?G$3^Hh^s@)anX{&)KTXrNokToabrZ}oot?XO<{^kciz zu}mun{k{I)e7*SM)faExylXcbrNjB%&%b>7`Abb9S1A>8C<<^yByU)5tNH%T#mZZW zofK@lLHjc3`uE>|KN#+b*a^GCQrN}~r?FZuy=4F>&&IRKY&7T&F^bTjoZM?dGXw3* zR<1$K2pH>*)mnhoaTI(`{U33crU z6gqBLZTuBWtJsxT|f3yT%?CwL&d* zZcwV7Q$!~>_Lx+sC?A158eEfDcyvU`glMpmG90OwZ&B@;Q=KQT>cWjnNx`g-#6*!R{;m|5#}=)(}B_996hLoer; zWt^RdX8;MSN|;Vc({@x~oqF8hjGAL;8f=R^d3Jb;^Li`av8ex?Wg5VzWtg2-3(^jk z0(>?cOm`hxy5X46qWgQp(DPzS+11l5{pWxFm#@G4^1JW;?)6VU;3>D;Z4e{$bCzaD z#|NN0$Fv-W+@YiKSSBBhvRtijcde$|?7ASFIE@|K?e_<7e);+Kc7o+QCB1D4mrtYF zh_lo-EI;(8^ZBQrehSp#tG!M?#ZjoNJOT>$_xGPae~#PkcKiGL1Dm?|>BHe3;0h(Ho zjL-w2433-&$jEn+@`9dif|J!II+gye(gP0=Yg##b*l+cbofPs?67RG#s~ZtWyS7f* z7@4AcBHvVolsSG9_Z&>i*nntUBKnw7gjNylNZ5+5=FL*IVi7OdQ8~S>R3Gd9T-_h# zVU2lFF|J6DB6B;CRn2;@ZSqx8YsHcKpu@M=UIKrI-NZtO#4Ru*&Y+5Hbv6w&lD?Uy z;geX@Q>tSMu!t{xFMvS^-GysOB^kel_)d@{yy{W;G|V8X>*PM`R28jQpaYnXFf~-! zQSe@R(BCHuI*!|J6S#wdHZ>Di0+Kb(g$9J1j)QMu1!*;0JiS4Guw2ZR%Q@S{`FfQu zX4n7z@BiWA@;vs!;lY8o_He}6d}i6!^OG0QmN^&7!>nDc7pi9DOfWHH!6LSO ze0&Jrn@%QJQ9REBfKN_NzW?j@E?CZ{Zha89KmGXQ$?-9?cMzxjUMHtW3BYfjzkmPk z{Qc3<(O}S@&F1U?%g@ix!Ji<~`Em{&+v|5HlNoLV);6Sar`yFeold8qFT=FRVa83R zsi22?nykm;*=o5ofn(a5g@_Sj3&Bi9d8$EAHsky~ku%Poc170n`>wWmJ<+_<4o~$mDn?=e3BoS=84cmGh$8pjc}-`b&Gr6iSac zQ{%3d%;!bV1!-3z;#P0V?Ock-_8lstCfUZ$-1Zx7hp?(Gy0?+Mong-n88&sV^tVg|!Jx8q!?CDz5uB+=8GdO3 zn80VVsga;b@fC{4#0mAH13cQz#oAj;#{c0z{%=7Xy#D!@-+cGo&#!-K+RcCc>%Sfh zsV?O9_6A(KS}t(ljH@(pZOg?1GTgSQ;}7M>ue{-~-)?s~4Ve7T7>(I%*6nt$uC5f$ zn+05fPhhDx9DMfa=i~7h7;d#(*oc6S=)i!_-~%E|DyltkoWq0tD2OoWT*?k|HA*?6 zwNK_UH%gJsBy#OYg|UMb1T2aVK!~hXYd=^^>`225X*mte(k&I%rj%x4_pyvChZRxT z&$|gKWu(5u<3rzU!I_Fw@3r?xrf1zZyA?k8a7y5l$b@o}6Rb2j?bb+@H{^e<=x|XN zmzO;~b8Vw$H+LZ^Rgq9)?6D|t)lIsM)?mE#1U74GDH^+G#aaNrtl3Dc+~~zL&?+*T4D_S}Be~kTvdYG@15# z-HSKxpPfD9W_%LXg^P=e(P)I<0a1X8R5<6a&ll5Y&(1C`&OI;ab~|AhiL^neO2}0F zJb!CU2*}$`V>lcFx88a^+}o46p&qV^Z$~G2yjZ%B4$Wr6Fkz`e_xgcH1)pRdK#eF( zvqej3COVREwwfX~>MNVIJ^*9A$Uzb+QS{{7kw9AAFK4q=UNy5N->42UfFbrNeE$X`~?KmAu)qKC{RHDm_3|SUy&6vNM!Y4HH%CGdJzhEfK^(-R(fE@W zXU&FtJHC1T>auUqqW z6SLp8GgUgNcmw-){%u6t9XD?(c@Mrb_r62n>Nc5HRAwG{V(MZ^Sv_)^fC-_yz<8&72kl<- zpxvj2%Z^1=jW(-O+krfxe_57BqK-1$O;a;e@TJZ{8Izck!aL+6F;wauDW|lhUyC@& zp`H`6WUE#Lo>ZXpQq#5$_79^lxEW>F2}s%D z;qmp&)p!K5Jp}5I3r_*%trf7Ha!w)~; z0UaG3V~v1j29XX2!)D8cz8T%!VeL3PIGIdtVaF*8~%6Do*n4O5<9?uARtyxHnwB-7>EB5;VwN#fw}Q#wy!oH2+nuf@VyL z#++pv=uNs%UI3gj{zy>)nH|9t@ulR)ETwgiCWdb_Z_MV+h9>#p$^^mb8g)^WsKw*jUwG@#BRk9oZF+G= zdyymw8J(Cic?=c>CfV05S-Ntt-+%S;<%?&B{Z5xU<`}x8>eQ5rT2k7EqF9t*WAZc+ z+Z4&PUp#fWm1sY|P4yWUy3l$Fn$@Vio@#0qCwf!r2qvUyQnpe|8wwdtvq90mjM8_H zPfy;weG6Ny*>*49z5nv-uc(>rYW?cftHojtHiIsOLCD=3)XR&@!=nS*P)y3*cDvv2 z10+-eIr6F^e0XL7F`8#aIu#mSE5j^ zKNco%#b|VASk~U&-j6^22-@WQJ`JnG>FMcmy~1UWrxQmvJM9i0-()hwp*r0buJi8p z`sC=C2w%x8)z(Z+msO4REq-z@XjVQF?~X0ic9D9xxO0fJYG*!XG?jU?J2|uJ6MDLp zM;W{+XWE*l1`@ueFowmJ6aLpH`$aW<2^XDtx?v95{a6a8lf~d*57z9(^))2%_0`qgWb*2Z zFDBzLagIu+9flI1a=_l?LRQvTbJl*Z*MSv!w0~e4#%jF=AOS5=6o&BxVxusc&t``Q zN8kVTuTUF@hX)X)CnqPLefsH7KmGKlKmEyVHfhsr-~Z~Xub>EE+4g%qFdKCVTC9QU z@pOv$2hPMX0bp*J5aI}H-^0_KjV8EXQzqr$FW3Hh38O&DQ>r+Ne3QgJZHva!M$xF3 zy_sfl;X$}pt>sR6MI7Zp&HW~CR;lE}K2gKC6PeZM;C5%m$B-#FQ{h}Caej)u7o1sK zF?gU-hOwn)DLawX?B~ZvBGceLdv>cTdM9f=(Q}}-g>#8ar4k#e`){8l=k4BV_wR`O zC30=0iliwU@GIO@mbNWXQxzsOP(>R)B-Btn_Aqiu7G_C2^%m4!D+c%Ef&nSf zM=spLqI8oY0i?P`XKY?sL>5cVJFM_xYZ#W8f#eR+xC3p3shM-v$#ien1JZ5XzMYSc z_m5Tql;HBKZ@&4%KmH-jlW)KIb~2kbEDO{J%I980ON`9n{{H)mcbcpL2ERR9lM2P|>tzHl(;gJZ|z(c$4CZ{X`g@dCQ|7>2jk>z$vU=#U6DA@U8F`LbB_hj+vIyDvw;@hh$5-Lf|iSoenQuJ-Pcz@n) zw^Nw?nw?XJXs-Ifpwb4r#a_l4Z7`TuvP`8KmdYK6M7H>LN3V}W=61i?5A7Xxk4%ws zR&u83I3hSxK(or3k0bNsDBsx3#3J$zc6zKm*Y29Hhs;?+X1*&-y1o0@({87#+cpyp zbX64{&5C^(1=zu!zeZHr2pGree=bu&-$GXhF_z7dsOPor5jMGqMZrt>VdCO@2Z@0&BP<6 zx)#tNT^8qy6pbdhW>r`1oHj@yLmAAg(Jro;2zVyAk|c3x>QjT0cBf5GO1Hc)CgJCdfxLbD4sw6c8vqiWR%^ZT$KyNGvH{4z^PinP z<1NWOiB1V#l4753(SxAD8b`|Yp4{j%R`%Tnr?s-@AMw7O)7$pJj7)C7Uf zoJonoJ2-L9pdz|P!Oj$!T-BT9v_V=`={`8Ql|AccL?Cs;Sz;cl36sj<&j)eLWE!Mn~QVa_am|3irqDj|s zxe})1d_Esf#;sP%qctnQAmCg0lc+bKQq=-!wEqg$0uj{&uddhLWID#oq!^DR<2c+% zi7L_8>oqO}%#3vrkiNaW#yfeOo3@ok*=#ZCv|J28O1Xh*;Ds1SRo5-Urko6m>eFmW za=Q1UR-=v;=V~RHlzTN&(mC(4{=q)Uk0JA6lF;AGnboLCX=j!mpr_OhdAJj>M9z$ClQ&yq%i2CD+X{I*9#xCh6rp07do*x1iOd~i zs9lIkxvtb~3o4c76}4le-7KS+w@k}vFFdGL&{CAg3BIWF-WSAUA^@7O*GEY~8tw1C z`TCo$zaDnmc=9SltWNo@lm#y7x#%=S4neBbX9Gc$mrzzcgOQm)H8_)A69q&3JJtE6 z)b$hCq}87Kk@IFIoB(G)n7<25eHV1n%n1eGb8h6LsNLZ~kR^-h`euCl*{hdstM%9K zzwdOr0LGj5@6Jx2wc70{h?>pDV!i+Ywp%VeWmN$Q;auHrw@m26%wH}%LsOxCafIb! z5yerf)lQOlHd{arfLb9cMb#KE3B$5UyxJBanS%q`dT=_WcnLs?A-TN123*0ecoU~f zD3}S0cMo?8?p-XFxShpvg$Za`=6bonJ!-5J!T_q+;YmP_rH0$0XtC`WrpfuwHD`Wa znGX-QT_|)ckeP|yejZC`t5r_dkohEwe!v4%MW!q&xx^-gMLhE!nVVke_Z}c>2O$aH ztgy>A`K>iKbglDzE#hChx>~c3O)oP=sbn+kN)?cHTCI;s=-MX<6tp~M8uhAF+40Op z2^48CV1ZNhGtijAqV%F9%o7-y6cNf3)lV|+8zLuUu^SoJQY%zhNwB3&z~Y3GtOM!q ze)Y-kfA??p`Yj;ZlyfS{kU77K41C!Hwdy*D#bw@%u2cw|14$+$_16#x+~h^)#ilIW znI8v~$wkB)_;FucC`pE@q?r~NOrsqGHE+E(Ei;N`%P?r(hr!+G_TXS2BI!?m{Nrzb z^V{`$`IoB9PGPJd$F7k2E(hXYtQqZpPk*@-QL}e08Y=3 z6yApQ=;&xVn_-8YALoyk%dU^_Tl>E1H79@$Kk((JuC*y&tOhAn4FK^!-?ClNr4(5x|Y&jnehEbA0OZyRN z1VF#r8$u3{x`sm8Y%Wl-67!vVrB-u*M5e*cVBS_dT-iqE&UZe@nF5LT8GlTpnJ9nB z?~%D7f8oV?5YH6({@m44#71+GX^;u`XyGQ!i@ZK{v+;sl2_)9`{E~L<$1X~`sqX{t zGm$-P5-sA7wcQZP9;8*MmmTivbpVu;o|w3PC6!Y%WsglpI#QvWSqM`-r*0rAZP!dK z;i%%z@Vq3THf~vz=Ak6VRGTD)nB7svdy0@o1sEC)v*SW^MZf>@9oCDK zK;u!n$~ZD9M?uq6gNl22&tG*TUPRDX_}LP4I8dTY`BY8vSd$Iz!za?9 zG(0dvrOM`9Qh^6uHzPmLC6kr0yjV?pE>%(D9>$~F{-8HoEJk;C&tE)W&%N85n_j;U z>olcaehFEgx!ka<5E>xQVBG<7N`V&%H;5bNOsN#aWjAbC-Fw4s%W2`7xjR9Aa&+A9 zcYprrr*FUg6*$@RRw?9%??d~yx~|4scQhRvx4iZ|5Fi$d>3qE3+jkn)?d2U-gnp}! zfdDq&zki3B<}{tq4-{Rwy_U(LffRjD&5Wk|xJ2Kbu|OS3GDGF3F@1_zDA zRIVz_yw-2F@HJ+e4PCd|$JP4H3j1Pb4$zKH(2pQ3R}~%~Ra6uw0!LDsBw1nl zZc_0?IFpweF6^gF11$60ZAA0|%0(@e^q5TDW#H|OG1luUH;yXaW#Uo)^eV`3f~R3^(8tjgu?!DR4@d!$7p<2K{5pRcnU7hmVuF|I zwan=iq{8VDq~mDEXDP45V9WjC%Ua|TviL@R$D zu1Hx0fGNNa5>aqM(M{ddbjn=^NLI7CWt)(aPQ&SS`v9cy59zx8hd=ybwORq0 zSneQ+<2bqoLDP$6ghA*9Q5f2$>9(8QZXdeWwq4RdA?cBBr+asI6NX8@H`ER7 z?skMZ3>?~6Mcl@EwbC`C(-~6h70Yf@Qp7-~h7$T_orW=x1mk@a zGPeT<8@wo+Juq{@nVTL|qzYkW<-9+2#ycq&+YnXuK@%4%ilS&i#&Y=i@ljcYUF`n% zE{_Iag?cv zq2Y1U2Ylb3)2`3UW=P9X@D;g_OL*;H!3Y003Ev3E`$F^y_dVconZ@i2j(`**!^6FhTKlYi z{^|Ap!4NM@inX`IC9I%#J@S_lk8*-Ll(}IT&8BMs2AaCRx93HE5~sASV53Rn7)8S0 zgDU_sgCJz6nacUfAFzj2l0qTSeiw_(g9337W@}`C@S~azX*s-0}zEay4WQ#M4+#k8< zca_&X8r!bdDl)74*#1p+W|=oD>h;BW3M-7anIJX9oO#drl)F|MfJN49TB_NSGsO-t zPj8sHnSzh@(W?<);d#pMYr-E(^xN6&YdiO7dovDkfnH^2J!`Qbiflun*g`mCm@VhBMm zJhF;FT2|QMvZ!XKJEs1$V(m~kv`py(T*%EaE}3HJ)QW-&6mqdopr#c4V|YiTIEiH5 zbQ#zn^NGB^T0O4nO(q(?9;>AK$!r{p|D^BzLFn ze*c#r_V&6jPR?eN$!fjS$!DFqF6H%-j0ngIm)Kcqa!E+D-AYq1Iq46GTa7pW10X?? zt_uRpMc(Xsy`IizAl#Uo+YxRL2QYLiiNYjHakRjvEsZis+1nptg~Ba@c!$H@c)Y-p z(CM^ULC~1vmS@vhr`w$`rb&{+I)#l5dl+2ZY_(OYaAa?VRIih+wKXk-9JlHboLPh7 zwwI(DHL2zL{Y}UeoXIWV?m4qix7=q)AXD_V-0Uk^*~=^!33GznIG%aWnaZP_`PgPB z+m>OqDU1BD=Cl^itQMQ^G-7wAAMD8CAcv@6W|bUryl!xrcxo6q>_$aO{J>vO!^S1p z248o~hDv3UCT%@Udu%9}NhuVsEGLOiTOFvHYs=q$@#VK)eqn)4sEEnLo4g66jNw&j zCoGGXK;Z){dYqKrs`92PUB!ewpNjWXf+hReIYUHv!-@n-Mc&@oNwlsI8PbkzuW7f1 zhG9~F1I%g1vS%x3bU#s~XL|?z;o$1>5~g6g-8y@A;(Ni>P0O$2BZJtZJ4#99& z$>0y;5En`nWlj_DOTvWR*Ls)RPd6G4&~|-&9Z+(D3r!0`p3bL!=+Q><)W^Mx0Rx%i zW@+=Xh7CP0iESvkJ)|(p^7}fH{K1yeqDGnLel-W^iAdbX+nG=MWtnzI; zE3m$8IOWPxUwxH#aTAblK)l-I0?VmXGlt=kCgcf8Lxt1j zbSZC``pf5M-+l4bu+xEYi7VG}ELmrgEc!2TstHL^4SdV10h1+pXGNAzvByz?OjSH4 z{-RcxLXs${AxoEBc8X4dm}F1!A2T@}jSoy5E4HB^TO^@&K`;aXgCGb+Uuo!DPHOdj z|M!3Y>5CVamlwWAMU}t)_M6pW75agu)28;T)dC7)F`K&tOS{u<;W!3Vy(C9*0^&VB zIq}1g%xPX4sFEvA_74wmmNjSFV*M4F48ZE!H*XxLNo6yXIhE?VNj@w3MLjw=Rtzol zBk0&Tl?+42=W$b~r)SjamKyvz?Y3*%_F}c@^*Uh?HQh$5)#bt+Y6xcYp4;3DpNquq ztDI}?Lga_Lb>!RGd5_D`N)Mqr8$ZbYd)hZ!cIevXOnHM$-mj4}+Bwp5nlzS;_;0dd53+v!u_sCku5V$diaF zNTP_N-nph)eh@`m6BbgOlPpF`R-~~?nx?EIQrEVB`^`6JCkKG7&dev@LecoHH!YhAcinv(w47YVag>(kO z1+;ay(_@1o@Af)p&z=)u+ID}qM-N9XqT*#M%CZwpk*E2<&MdmP){yx~R{DLPy9fP* z9;w*;V5WiSJYF7?n*$XI1<59ABI22)rrfxz*xKZ3lfupv*TCt*b&L64D{RDPtvlZK zUhDPk6@(X?>*dl)x1MUJuhPFYpAOy#Fxxn4)FO%RWkD4 z%6Sxrm}7BFU8fRyQJ3Ug^c0~S0qXscrJ)7W&I?h?=UL`l`b`7s7 z0Igk|zkl)UMW@@LW|NlHYS@0fZd$GRd=jh!7|?FBh0Ex5x+l+`B|*sDw5)c!jn5AT zgZ<%N!VN;!s}-)8>rPdhnltD@=wH9TqzqU|b!e*<1w;*XjTOP7u46jc_N#@dTQT{Y zB3v-kHB43^#ig#4eK`I4}Q@2f7=USnv zXy4uvB4z2$>WX@ws1#F`MFt3#4@mmG!Em)&j_0#(yMuGl?ziL}Rwv^zGV2IcBd@7%?ScvLq-J-K!ETNf4Nk`gr>OHyeU5!g=N+5=~TwSWXeHxotU zJjl1M|w2Pgl%Z*l;e`ItGQsvs*AW#wxTPc znbI{Gg)f?}UnBR0!j4)+rvcw=9?t1~N-FM-F_v*kBV`b38r8grooiIJSm&q~!N}A^ zQ*7FDnr9%mOSSZ-?Lr>Y@v{{6ChShw#kZr;e!s87R@2NlOQ2Lc?XGLPCiy=>daCO* z+R%}`f!lDqtqxv$y}|MEk*OQ2#e%XAvP_c{V0F;$s?a294j^?JE=T|>7i%8$>U=SS zI>7=6kpNtKf$ub(z1|S_=GYBT$5N(D_JA7Kg1EdPPZMZ>xeD{QcGevM;dc?Ky@tpZ#RBqy#_j$4pIg>p@lGtLf z^v!bOYNerhu@5J?;uTrbw(Pfh4fxK6AUk&%DPdM^Fj*@$uLo3wnaCE@&Jwd)sIyGj zyH5k6AO-hRlN0jY5DXI0r(5Dw8%|GyiVB4vm4fPq7Dj0h2Wln>dx^M`Tt9KcG7tOR zll{YftE1=|$q!jE@n;g52@g5P`OW(!}n=kqyC3@iv1wP&H1?U=nD#Wo?f@mOJetX3=9{fUPz=Doej;x7R(-kR;B zkW(eq2aep2p~8=-HQDaPk>?MbCOcE>9&WqxV2Qcs%xaJJowZl@odkA8|94v0rFU@^GkRrAg^g}Ufi^y zP@y(Oi4E${VZfL^IzGj3Nt6uthu|Zi&}}tgjV5udN!tF=euEm^ny|Y&-QLyBRU2&8 z?WmFpN<7#<=ydzsr^_0R#o&P!qnPDVb@Q9;c;=>3e>a3a#sjo_$Dt?VncJLMb|w-f4&`2b1rDXG zVyvl^oozQ!lS0jIIzd0(IXBl?_ z6n18%ius|PxeJ+D37;Jvmq&UXY|DmXvPx7VkZw9bZ;w&!Xxnb5s^MC-6H3n1B=Tsf zlAaa2rjh#x=rDB35658qf(QnKQnR!vU7Ve&-X~-5JuYC2~$M zzZG2vkPJSe$dlUM=;1*f4D~Ih48fEmpJI2!O(Pjj8ssFP&93J397L8uB05ko0gLSf zX`IBS)P`Oc~RwM z4QK2^pg~t*;D~_bhhc#&5Cyv!)2I~$AbZI&Nz3f(?|oywm_L7U8bu)(`O%%DG|6n= z>xCggy|E12^8yF#5Tb0?^@uRWa9tPfDtsS|#^BbOuJRk!23oAz&5|vS$(G^}<7{uJ zwQZSv8@RR$%H^*BHrVo+ZKQ#=%o-(&+8b311K2he^IF-KYO}}mTWh0opf(?)ZiLsLAaKURcru>czccB_eN?2PPMD40AM6f0$ekrEWB^keO8Bid zW~1^#ylzx*MLsaCfzXIyn3S3d($3+!p@okKQ&+6enud-o>iKuDZJXH2ku9E7Q(0X= zGHAk0f=%L&^tmL%Zj^aBoQ$|>iLwGyWP}_T9*l5iO%}bV4^O<`A1;>*2s`f{-y4p` zQNQn*C~FB<*Nb8=@VKfTK6o&@x{$&c4SJPO5YB>mc(x4=5wgc5S%Z-yz5+Yuh-*Pj zOwNYA;njQ!ZqN^cvM5nKrPoWA%c2yM$-!biozAEC@7{-4I?D@q0a&(=yq|Dykwz4O z&PC-%5lQkX$VGh*Hj(J)&YfP|Be69@s)khg+AP~eirr|olU{Z(15#H^S>2!5%m8f_ zH(mt;q`k)NK9lxWU7e65NsazkZ7hpPJ*Bqt*OsJ& zlx-VpV+I=fUO|ONwcRh()~LU*eyTgHly!B?ayPgYSUQo^cYst72OdpRLT+e#!l>jj zxj-!tS=f$`v>voSKu`x69xsrys~-iS>qKF2|K8}q-TStOq)_OMXad7P6(433nHyGU zFd0O#5X-h9)3Y3#dL$N+L?EtC)`PfKMI9>Bbs>j@n6(nL7n%-TJI;1W-TlJ#%^c68 zhO|K{Q^?4~HS*oC5d|T^18cN2DqLbES7I*+e3YptbuCjeBd;na@NtX_LT}$w)Z}&? zw^GdWeF~$EAPxgBL~RE@=Ey$;&d-Vb2r)%f>DtIM=1|Yp@m#oLsC4Fsj_cwrab1UD zQX!QKqa~ffX4TT>~ z?g~{-FQyQJ!;?mQe1GVL5W*O}e$TRDXH;?24@1O1ctH>cePn{bEm-M(118v{zpB%O zZ8T^-Ij!|GZ&zx@fD&>Q(yu`#3%%ItF>U<=Z~wPLW`13pMREpYlVKVmthK=OXHjj| zpfaD-0j(ao*eFVC26LM>8k5+6TwR-0dp%yK%yT_CzAbguZh>@QJ-ZOuPH}9vqJ?QH z-;n*b#>(~tEsfc8v%+?RWgs6VT&s@e3o!4bnCtQ#g^Pl8Z;&EJl~pceWI&Dt3lN;B z0kPh}A#^0JK9tuX6oajU zdBemuu2K#bsnJC{9L`^YJqlauUXO(v7i=0X#HWPhRM_*yU^4S^HwcHYBUIFnG7KL! zJ_Bht4TlM@?;4O38;+j`;Fd|wO83ykXf#Tb~1ua`obnDJ$~) z!Tr0>U%W^b^Sme~lOxx1*T#CWSRl|R#=*3rfDcg??iH9w;Ko7hL_rWRt~%iL?f&@= z-|S8+rLzKC86J^%^zjBCEqrrJ$%&Rs@DZh`&BQ>W{mQCDl4aRW6%J;W*B4I1WRfIGW9xc$Yrf zg#FYgOlFoe@rjyF%5BkG(9X?iZkubI6DAUQnkgGW1p?)j@i^pzc~#;0Tc%Uu^j3YZ z!02&x=oz+6>71sk1yS&U<+`*TAdx(5Ex9Z`eVF)8q2r{RfU7twTnoQr(O{#q&dL0Qz9L1127HDLD-;#7J=%J5 zS~b%sw_swgD>3`{d-e`Dx64I)@6Il7ZO!O~{Z`29%(kjuo6}&6jtOz8SawH>0SGKb z1$P69A!qp2q6uy zswC){6X&AQ2?VY=-O%ZQsS#HuqrBfx4H2WxIv8nHRJt_>kE#?E?kt+etQniY>W@LN z2HEIjpm)P}3fHtN*pFn55Y=65MH+ZC-f0n?rXm=;p7ccg1=8^#Fp%)e8>EC~nGi8j z?2a)QRfL%zM9PpBPN8dl8K(K~@dW5K1dX7;f_eeq=ccP!krfa&gFRhbUZOz5a42be zMg1a(I52WYw(ML$RKdd@r1JsJ65x=9&eRd~gLUu9`S!Mnxx(E{b{D}23N;cTxy=jJZ&S0&m4LVz>m3z= zbsj6>4OoT2NEyM!SZdO^h1!$1>ey0`Yq=&?Xf@G4J?oslMx&wDeAsF)A-+H$>on~| z8>A)vzo0dMfwWoA2jh4+Q8rkrh6+-jX0wUY`9lD2ajRsS7@OcqSRf`PXASWaNJmuU zmFIb+P=N9zNSDy_;b;Z%vLIOm4x1`+5)jkV;1akXd?cno(KM_o<9|dq9|keJ55>p~ zespw{=Gn>FDa4}iK~cX47c-yFhocer)$^;%_#lSZRZ7$a9Q6lg;OT30EN7l9Rl%cP zXju?hh&0W0_EjS|n7D;rU~kB}7YDxc`d307G;VXtIB#CqyHuK^;O}f*c?6}R&ZEXTz-2U1AO>P&KLG$ zC2bgt#wL(Q#kO^wUj>CE+jyXTNp^$O>lA@*iS;|FW<~3Z)+^tOG1#JII8?n+b~#mQg_r$T~BDM{c7cUC9a_hdylZ@EdGuo0d*ybgiYS0UG*d$<~>-I^If; zqqnyVNdJV?3Z=UF1_q`%mp6BNYiGgbu4|6i9<0a8q#&nivdx{8?V^P??E02&EZo$5 zh}zp4*TF?8AWwD>tBYK=_2b8JaWpNR?Qn!BP+6VkXKaC+N>2Ad5OHF?X>uEJyg1Gy zv4@Jv+lZ#B6yi4u2$UhuB#s3fdLE318e8Z19%Qk@!C*F?uc% z=@MJA27`oCq!=Ig9*%eS;z1bn34m$W2Ga6%-FMXrp@{1Z|YjUkM z@U5sUL}5uo^{mZOuV;1>W8V%fC);&gyA|4|0;F1Bx75dO+W&Ta_>Cq_K7%$FQ*BBG z`Uem3DFJa37zHLf!f~zsAg=PX%!?{t`@TCG_I<|%+vtU1J24`pHx!)cH`?Yq&SaNF0V-_^WQpAt0>D;k->jfUp^ zuFs)?J=9Gf%}))nK2kQ|31C`VP`WC?I34;FH1B|DyCmWkv5-U28Yz;=>t|$Y><7-f z(8wD$rZ78IhUvv2-YYyC0fu*QaG*&^uCA`Y0V1n&96x&W2$E0u4ZH{?(Keeqd@j5o z&$F|$Q}`uv|HJ`&I`I-ka2w%T)V4Et1=@ABiZd)_zTc;ohFy=N%HQx&^w8C1{t3ns?P)ctmw$>Q^ep_9@16qHBa1 zC`rt7mV`YIYoWR?C<52Xi>%0UVjk5MY70}yB6n@GAIIbIaB^@k==G5rM_}(>qMo#k zTy>;$0NEL2re@8Xi&XY%v-N0`AjqtCw&==;;T=%~MOA_WEHP)}Wm)Jzbn{^F=W7*E zIx?(s*LHL%gp5#XG^19G)DPT#GccE`@2+l~j3X>|8U>+m=TgG}3(Org!_P z=`%O9csm00G7$zh1po7g5vP&=$=Id&Wj!9C^~q`_e16Ay`~ z13{<;wK1p-#*J-P{TXW^W*UaBb}hG4>C`vSsIlFQwN`i=CAt-(adpeAiDnyVBMmTU z!rw5BTcV&-UK6x));!;uZ3J<%rgc+Fc}v*2+pgSor%lJDgRR@$_BwZ}Hb1F0HBYq{ z#Je?MjX@6KpDG+R=9sT)65e{jA+S`H1UkprILnX(HdU|?QR5PlE9CQFrqLp_P)0-Z z$MNmpNHHcX@Bl_xnibqo4BfI!dGI2O_Qb;u#M4Z z8h0$;Kr#f+c14fpd77SHg8THb)wJ zckiA{HVF=sf_(6co?>-au2qRB1#om4Vxrr0U`wOJIu+v+T zxSI)XZp>!)x{yvRxyzis+p4r~qHJi);HD-@XF8-)gWZK9bm~Ai)!A;3_&ax&j?(0Q zh;vg3WINGztV zlrf{7TT=gT%m3&T!!`gRt#mQANnFXg_!b{8^*InJVVLbLw)HNPT%4)>F!488?T&Hh zp=cX!?!3b6Tj+uMdU#5AV=$L9iC@lY8r3K$pa zXqKhmK*>)A@OB91!QQ}Uo}Qk*^R-9t9W1jb%V*D?*`901n9L*j7UuS5in%YIKp6DE zba4PkNJA5=5ZIVf+)4*iUnLLR-8VP2T(*%HVK*gyuRhHA($(AIsn&B2szC)cU)D_& z3>|*TPL;V+O1+8dvh6o+>h|siB+bG2W^{e88Ml7coG(P?Nx(i@)dne0f_rh`Pd3&+ zGORjE0{+ViInv?xj)8Sr;+jM;6cpy79)z+hB)K#+NKP+Sp^=bMHG;>TC*54KmTkm$ zSw>@HI?ri4%=!)+sO~ONs?WUci(&y+5K>p@eB?TIU`p%6G+mR1hR@ifsark)c^KeV z6_O02ORTP2XNri4d8N0l%`g;PxQE3c@}CyXz7W8G08HGdOr*l6%z#x?k^*xo$~E{> z&v10zSg6{=gYBK4pMw!Xs`<`4?@XuD`Fb@T9zd1~W~}=(41KSo{i+9E*7CT4m@D1X z0so~<6o>Y?G1i$hZUB01Ju+Q6*h5R#af2`Y$eRkV zO?h_L5AQwiy~ppgUTz60+D?8`+pMV2!ii=^6P)80>cC@ZgOM^j@y~Q%MzwF%$E46s z*4ZPXxHk7k+lCG!o8Pp3WBZr-J~X6wP57!Kx|^!jO`_g;S7!jN`D*hgwaeqJGpU+Z z>V}N3KIb}!vS>uSm6%YA(QKELI|&=&mZXHh)puNDQ`rJjynp{bT-}{JceK(b!t~Pg zz4zV&Gj2k0eXo!)tk)Y12q5Wr`dSb_=;J>-v~0I8gWY9wlXblmknmMJ^WH6BFRwr+ zplyy|-PEqQ39Qq>4{S$Y`_Hn4Z+s<45Z|Qve7o${Sr&C3rPu}vgJmr&h7)0n?zk!kAk z7Bva28W2x*h29d;^`eDR(5J79RfzT*X2rI;9uT2#>EL0)v!tho0u0i20vlnZmc|STJ<7~#a#vxHv7i)!&>H}!p&j~r4aXs$Q#IG# z^o<~XmRomnlU_2+zObVRPLk$S5y1*8(sXL!6M7{c6Ge5Xstl`={v|WXtEy|5&Hk!NC`r=6VDQmLAHnq^IM&tuUNcAG1xPd{ z+!gpF6X_i&xIna!Lu>B>kOWsfoLqN#ahOu(7TUJ+9t34Zht(YDZt%)OEN8JFz~LUQfCu zXc;zwNzrfGuuoU$5i8nLafZ=#A@Om+$H4n-!!b#x(lS`+#}ZZ5J;x4F!cQpQ4Ghzk z#U{d+q@IW8>0+M+HAq~nNLaQWE)|j?r<_98A4y1hNwT&`6l8EB&2dnM%dp_{V0nX; zS6IKW2$p74sHKbc2M?yGQt-qE(&3mcVn>k>&oP@)GtK)cnn=U`iXn*1j#rZ6NTnNW z1g7P#>0p-w0q1bSsB8vzNf0W?F-%?L;F1cwSi7$3comyOrl6@YsT{Bb;=weL6-Cut zWN`nnt!ddN0qb>D#>R>^jt{rpvusT~g6y?sR14A6rO>Ft_)riDWjb=ofpW2&r9+VF+oPMzvb zBka}2J?y0n>oh`kkFTddcM*AQ7tGzAI5TI^bxx6N%wPo;+X;ti38AeCWQ zx3K*=k_%ObZ(`x4lCO zLzjpR;XwF>fok4xpGz4iY*UKfaG)^mP;uCU6V8!}J6mOWk>{hML7HTcWruMbgmG3@ zpFewwSfMypHuhx@eBj_#U@%z{cz{R%Bo53P>J?#kdogOejjnbt@#P1+q0G^dLTY~^ z;q}cLh*ARpiFT8h8)k(Kzrv>NXS5k&U%eH3%Y0_rAorD_SjTyG9BnbQtg6-DgIu=m26*9|2Xd6u%IXIbQlbRtmbG=eyade1lnKeR{G?6r*m8zwZx{2W8OA8ItqN}EYEUaRMP7Ehi zu?a+21c}vVo4AxTZ9B(!0lg)Gbx@VrL{};(vnCstB(F=gU8_-Ks8dguk{p+2WoUF; znlyaq${Or}7X{HYX|1tMpl-FHUFaBXU0*eA*A2Z8vv&*ebZ}$gej=Zrh5uMH13OkHqp1_rLd?r5S`r;yXs++XTB+p6Ym1M66L z`}#Zhp}iRbZ#oh+IHJ2rH`ZIj%CrVCB7aIwZ4WJ@s|A}O*Wr<2m8PJHVGI;-yb%If zPS2ZbNfZJSQL_$1#5#o52&xXsr=igkZMb{(;!dx-4h>^~x?Wdp#NwX9EZ_2(uZRTNEu(A-X>|)q`$=O{z`yo4T!X*h zEhSAytFF&%N2G0I+x7T+H<-QHsG9=r-3H)Ine;A?X#4cK5j72%U`bZe@T=Zbsa1`x z53?z$Cq)Rr!zwa4*K)NhXdM>a$1aJJHd@oM+hEsbx8BSR@b)iZ3F>LE2F|YQK%7J~ z>#l%BjsZQ7rGY_83s_4|$up)Ouo!Lb;-ZAeM{WibhzDLEfu6}Xg$1%rgsGdFiAfK3 z>FG&KH1V(yzSeGs_MfVr$Y2WU*NJ{_b9clYG^3UosdS4rBJETrc#ZmG^qp)j$uwo zO^(eyHJi?%CL+_AHOd$*J&|adW>dA=%I}$MKXpwy-bRf#U(Mqo+rhDJAvUEo)j>Ei z+*kx+`ml0IqbB+-kW#CHrc6XljG!xIMrY#_QvZ!8SEQEx#+Pg4gV>ZDNPj`H_B|(d zPHyg~_><>(>l`B1%y(S)&Nhl>u8YMXh}wPETdx*i zla7N}g7b@0%Z0E-(Cipy3blpPwCbzRQtnRk?u~fAdSB$G5=qC7ZaHU-JKC6xY|n5S zyBP6T!C35WKkW#i0}9<6yM7_`W2XnG?f=>~q&^c=(8O((s?S&l+V~iw2FmEfx+W!~ z84>JeD#L8|e)JZjQ^QRHKo`|;G;f;z?Oskh(r&u;dR5hSzJc#GjitK9kN!(w^;L3I zrAKhLLv&qeHDyU=2`g39a0Tq9SVSO3qEW=zAe1Iy)cZCT+=WrO5q_#1z!rr38tOrDOS|*j2zAVN&eWunk?3SG^YI} zQ-QBoBKg(zPHve9>ws-uGBJ7+B4;yC7E8|_*Dz&6DO zUQ>y-YqqSzJn0aF4zcU zL7wB*V03DuH`j}EpC)&RYh!aJtZBFE+Gb;Nm6A3`>JVlbj+UsW&Mh5~u6A5$R6F}b2l4~f0t zRBhKGZC{h@pIV(HMo29H+TqFqJgMt@D$9zp1Pi>H%~p%W;qlSwixYUuXf#43hRpmh zNS0X`MJE>*aU8=2waOAI2s4Ov;rL~VVJ-GFZ6rjZveb>7IIn0?O z2ElG%Z2Xon$(Me#wHKk@+2Gq!C7oN-X_LI-fXv>E`FaMW9*srrJh^W^bIX6~v!(G< zScJrqt$}7z2R}-ykr2@%!qjls63J#wM4jvT$(l61xzhSfuB&!vY{*ze5!J?T$OcV8 z!P_-@)*>pjnvT&l8n7$vaW;WPE4BgaL#}IARt*g!mND#HD7vc<(LY*@Sz0;r!OIA7}i)L{i@2-QLK0x#e`Sj{jVr@?Ur24*r_ zzcED-`2@Anq_h#Ujx;}ImM>=0xIf@ZKv;_;KvC?42%ycY95xoDN|AK&UmVfC7X$RG zNHTB12JIaSx!nm{w{Qh?e%}l(iB`$0ottgAG-Vsm)0pnoquo|0X?*eqLcK{GX$Yxt zM-BN+ty7>+mB_M8X+cJVLhm$YOs>M*Xj>R1+ltJlhKrUDGXcC=~% z+ZJ?Co5=&XUeB~jN=+->*pz5LaC+bdB{c(@GN6Sz1%i64_)iYGn%FJ;*Q6vRT%}7lPBuL;)VLMY7 zgmg^{BK91$sNJ$yBMYujmS^{R%J*X1=CPi@&N;xA66Jr@(Id^KS34 zgD$wKIMl8Nb^P$|zO@UlZDW9TYe?5om%0{4^ejrX_XaN|+?YFIiIn&{>gITH)HZ<`dQW*)&>rU1K+!n1V!n zO;$V4it5AFriP5=VsMZJGIvR$CcwGPEW;jUUNFceF(|0~Ajo8u7HOUrQGolbg9Abc zrOyJ*0@aJbX%k;w6!v>Pq(KM!S*=!(zT%&R;kexOBFmN;yeCcJ zcHxHf_c-e#7XOtOtK_AF_8_jb+V?Z71VlgC;^5821Vi6 z4n!`c5@#o;K^);tD=SDx55~tXq#NKeUDvc-K?CbpO@=^%kOyDIEZYn3OPlC5CgICA zId{b=_ts6VW=iW-MznTK>!yNg`xdk%_jc>6drHiFGyTYFNN2m`v#Hcw=fHBqY_LJn z>J>u{w5GV!@(^|Eui3odx{3FV#8R2+jK68x+ZxbAQEL-LH{cT81<~D>>*_SkXK6}1 zrU~<_lA5BH_L&;sRwvXNj)$Ynp1`pdQa50zep_w6ut8rTB}Y*dBs~K5lNhzciHX7_ zl36xGT_E4{YzMUv(4rOceL0q_N_b-!NASvYy`nn-CW2zMI8#ni4nd6K1#pHm#iFOf z4e-lgffBVLu2(|`fLX5xB)QZur20ODm-FTPv&DiX z2}CYns7boMx;%x~N4<3%_hGA%71E=r3RBo$BE8*Sbo;L~pxw)U!fKw&{Z$z2rIqLo zp&;AG1=u#o-G6p=tPQUB#%NYu6N&MS%-~iNwLR&A%p0g+ivwCjIge4Kg2+%)UFn{_ z79OFR3AW~t9GVmToJlx0iw3Wx9)bc(rU~hqHLD?^&}XiN<%tOhXG;jghmKi83pAt$ zPY?VC-ze1^C_2QcwYjP5O5&1axZ+@gfK(3Y`5=-lA&o@*Hid#^m6nAn%JnMA7pr-; z&SAl4S+>rS5LLEJ$U!S?U4qj!Eyp%0Bf+E*E4E;+l_@+E>nm8Iwp_-sxC%}WeYk^# z8MOHvwwBDDk0yuf@n8b(3RN(LR#m}Bn;Yi!Hs4D1uO9&lcVKOXrrjjbkAT}mA@}uq zt&F;b3wB7{nZe{B4&wuj`||>SI-Lmv&SY&`SPajy6y62bsJAxSMiHi{bnI7ew+2ox zeDQJT|0)%rSBY}>ZWGpS5Zp1RPUEF5YSL-4@8z~*J7Tr%T(Kz{b;iw1!)#Ya_mUlL z%owp}L=t1_IU&XAu@Tn|PAS&qCG@H`68vx!!X-T>c;>cd-6bh`tcc>Qi75GFF~3ynjBcZ z>&D?KSuV@0=LNmdI4{<)UpS$E*c&0aUKqHhr*VL&6wWJ?gc~@K*6V@BFHsl$)lFvZ zd?LFA3g3~g)Ps$iRI}61)P3+R0Uv`kWNr0$(bneF@Y%F~W*hS2U8U-#gj*8Uxt;s! ze>O*Ja5DrDP|Pqp;H0MW*?{J|xE)$Ec;Pm)vSLkIx5&>}A^^^2hK+hd z%v{fxX_|+&3(l_ZMQ6zi!;LGM7CAQhO>V4{6?iEKKf%E+@@ci=%Va^U8F{i?;|PTp zVx`nZ?qeg4S&^)f(#G<`5kn0j+w>iaenM0^Ck-f8;%cXz4Ne_3h6MFQRc0{=H0vZB?O3$dmkL-V(CVH5C^I%bDTdnEf(Ze%F@zsY-E|m z8B(r!R7mZ;swO2V3wbK_Su8i9u7vs z2M->+``*`n{_~#)VK`ea&t_M!-4qJDLJ+>*Il42yTzbmrS)-mkIPea9*T=yFLpcsS zJw(i+OXd!;Lfl}P*BRDkVEH;r`O1;-mnbr|e`q{VJN0X7C3|(EEw)iZ*yYjMTcq9< zZ|}5dcI!sXaXRV=ooVUaPwyZZ+VoTHtW{~D1lHuAx++mw$`riBkPn}$R$IqeVoVxy zu(-Sw!ltR{GRrF$e22@G@cU7|UOzuO&)2C*WJ!jS%XEg(WoaZy0s-YZ!M0pgh||;a z;dtO$-s#!NYBnECMy_j5^XksAhsmtBc1;?a##T^?284Qi5WE(ZgpZbrLT=asz_H+W zqr$mlqBSXs3>?&AF*_U|jDr5-&p(5pHtvOH7$*h9wIVOmWRb%9M$o!rSFl^;`Fw#y z%ux_y=;-+>zE z&9la7ZX803dfep3jzH6CqY8zV{8l%S5B!s5o1OW`7QFVx7LKK9b6YC0^;I>_tzFvD zRgewAan}Odm7ex{S?g%+9%-juyu}-&RXXb-sr|EdZIl_k(59Xt)^oL%D6;Q>f05F% z41v=Xi1tF_SwK!C-)LJ(d~7Loo7{y^7=5^Fu>|!P))6Aaugna)EpT68yV| z8rl|WW}@si_$N49R*6j5V5~GW0vs^wZ z3vVvLbnGBi=MXBx(gAL%Tw?ZF4Z{W1KHE79XipG?f5O zmWmQ0dpMgEKbIgAS?>2D0(LcEvsKt>8iD{?Fjb;E_J5^YHhHW!& z0}zRc1L4@X=%;gJUYsSf`E-$2Yp}r890ha0$&?83HY}DS0$w-PO9>fQ=z6{%^0J&R zmcH*E9v+0gUqWh1=UBl`pE<(GzyzolR3Z5ZmzsqEr#UR7xG}+wz~e!@TM0|*YO{gh z8N&*~!s~}>mCa$N5G;x!*h;cRfw61~8Fc2G?%;SBM98O{Ri!9}?OUN6IeuVL-xXqD z2nT;LT^v8WKMcq4<@>>GHZSt(-otx&E-cRugP0Ko0UQ$IWSLhEk^~vPW#j@j?~*eI zLrf0YXw~7=}#ceWS+&Pm<+q zHak2#JUTcCnfc+zKlCOC6F(kLjt6&+{ZSnCW5;!ehkZytrLs&;U=W)3r>Sm@uUA?n z?NtLv^{R!?e;SP5ZW;L|6_TymY~$8C4zTSZx4}2va_j9ga(gFH+Hvo7I!YyA;|9g+ z4laUzHmO#19S`tEyQOY~+kEpz@>kMwP4Gih0;Nv9!Pe{5#q{#(;>_?}*qB$VRgtCe zCqE2aZ2konBD4hrkfc0W<={uk%;sK_&&>*s#;__&+c~yLF@;rLY39Ys$&Ctohe-G% zDomUtxI1tG4pw4`@Wu&yF{)xqSxCXF5*r)XTQ?3TN9VK4OcA5Zk2=YAm6PzTs9cf_M7XBRtq1K!v<`Lv=FkSCwzxdhX)zjnSqWwO97W(rvm#mMsbOMM zHm~qjSxyi|c2Q=Ki`u5KTrNdcU@5rY3%~Z>yKlbn+FS3$cLw3%kvEP}>lCl)8>HesChRA~^u>BGPAO975B)5Cf{TLu3>)?CEIojcCJB_@_kL?`=Xx#5s zr+ynA(IyJe+@`vpskukm=G~b%*<5o;B`?lS zA%^Tl5knjeq-LecR7S&IQhF$fSS7>~piGFLC8ED+%Gd^1HeJjiRs^$gP&^MDG-l?r z#X{@tmPPplJk;}N5Me_0IEZ?-VMY$W z@LT`vn=d~8gj@d6!QtV(BO`GAzzu@9kcH#;a4Y7O5q?<>CHda6)h*;*9RScvsgU-vn01<&Et9+67WazAbn3vZ zUv6Hk`;%yvXoB2ua7qb0fGl7gd+^A?WEi&`Y|MbeoX=;uNL|Z4Jv{|`fMf~W&Z=Cj z7E1^!X*5f!oMEs>q>rxed8RF;cyRYlKaNp5OctJnc$Z`~Gs7OFMzEwj+d)wQ3#V1E z)@C@^9yOT_{!P?3A8gF+xmj6Mc_FcIRgat5Tu0~XVmABfkADn-H`vnM<3rE(i#)-p zOK>hkg93L8YC5DKRm^5R9I7X&DbIT^vL!I;2y!Rm0H#xzeq zeiFqo+*02O8NvoC>K-doSdQaylik06|M1RaHJ!o}>-BoWuYY~|^hqxsjE;tdfH*=V zS67ce`xGwY_19lF2Nm2sxSI}BCHAJBLROAZlZNeOqS@Oi_*G|&`%1ny`t2_|yoPS< zYY!gq9fH~23+~WqwuHy7=Q-PT;WlY)JBQpG({?@@wVF*+(==X7<+bp-9mX_j*XknH zhW8pRn-gms75u5WsevPw3C4)|-E4XW`PIGSBM8Pm`Sj!Ed=9?l*|TTKdKm>h-}lN= zf?dJZPE_I12v%`#;3C=@>BAiqc{d=?g;Q~Cgc_s|?T!@j2#hiv=k)XpuJ+<$3Kj}B zGo8*B^Tokra(Z(9H$V8nqeox&A}`rXbMA>}+_YY5R&;~rYwLYP*K8q| zGc?V)v5|9uD0NNj&b%KF2E*QBI)_dF^86guy%qS!hZ8W`#T3^0$};U?JnYAPhX~c< zz&n^6-@SilG#rDq91aJNpTUPM=F92%`C_%Ka0HCmUa$reT<2Kwmo(cVAVRJR1ojmn zsw~GAP9Y=y-nQH%O_*tpM?-MeupNK{gcEjM_wRoAcc&N6AKrWY&fUAod2iAcXMgb@|KiX8tk>(`zk6qVJbd?!cP4j^z%3SKQDiwM z@m~nGkhfMVxl+i8S3%`!3kB6`;Xu;+RG7^ZH8vAs*zY@zovahMo2JLT$d?B5FpGCh zxSVud&aPg3^08SJu>FmO2RsNM|0yv#_EeSA21`u&?XLA^YWbI8nN(!DVZCiRG$!|X zIkvGzO(?}SQeRD8wn^5MY)Krb)+E+ujD{41tygi}?P_g%0&|zti|wP>YhK^(VeU?s zw~fu*aPu^Gw*5hS`=T44sWzWw;(Ufd(_y3!rW0z&SR}W~R0g55xLjmuItb$*Pp5Gh z9UL95=8I=%&p-WHB5mxUflVF{$7bM-!y$LTlAQ`mNJvxyL0vA3zzni-3CW~X1!Rfo z3g--yY-JWXgs668S7kb#U0hE2(ecC&gXLmo1r84nq7+0oJ6L2q0ZX(bI&Kp4Ar!OOY54?g($=g&@HWxx5>qj(S}c`;4q zpZ)y#Vx8C)>MGmK#3M_}b((vjuH zt`9p$F};{Qc_!XEGmejK1UW0_(x@5Df01gSCY}|Y)%8{8HeYej|CX4UR<+pqjk<+O z@^+R3>s8npZV$3=VT|u|I6LACyS76rGYr_YA_gJ%Y|8aS|BF9XHSNcqr<~NZy@s$j157g5SwyiU8L)jbkK2tl&{y% zN||#UIm|KbWXiQ5@Et=UxlGrHeV4)wBG)m&S3ddlv%{l1CN5kgmlYz1Ysv~HOd0zl zVaOy&jwZ+JEJ0z(Ao$?JPhPu!FY3oK$uBQ2eb+X*ndTV;i9h}DAD7AMXfm4Ie*o9@ z`0-=#@7Pfl2J8%>Yoe|*H9#j|<3?D?a97#Ygu0;#$jgN>qS zRV;Y1HVFKH0}T)!WXRx&(=P~87Oc*4UbWEjORVU;TJm|jqU6g*Ms><+-Gs2Y$r|_4 zX5K<0!nS$k8pfc#*xKtjS?`66cXps=?082MareH;I@N5|?GQeTbw|_n;**n07m2Lc z<;CpR-uuS$r=S1T_y58&?8*Jf{lkYQYUl7|k%+7)AQsK5%jBv^c)>Emjv(VIk@hN| z3=Tur&KJpaI)#81LO$3GdSTG(4Iq+K6+;9dPD1)6!|5#-g(jWAjpP0hC-;31$<2Ll zUF0q}w!ncCPK(m8o&WOwkHM4Nd-&k-Cm*v)Mp3l5x{PB4MSS|?iO9uh1ZnR?5pd;H|X&s}87o6gHZfjPm!VTJ$h@BKILzWe6i{op^(*VFHR|NCJt z{^XO7j_w>+S^h7+`}-Fsr*M^Dd-vU6|K>OQas0FQe+*ld>o{=7^JM~`>v#eD=5X>b zT_wyS;$p;5Na`F{U|ObbHtL86*}`l+0Y{Oaa3H32t>yFzZnqOhXOBPl?4wUiM}-r= zOj3wHJd*_xR;mgkN2YyCXTEIU z>x*?wZW^t&Zg+hhOiw#N-Q_OW3nbW0FL&=_doE1BR1>6^)IKq3ikeqKa<+?4s0l%k zMarrvXH`WBiAQ57c72ZMHZ4^PFq zD#@x8Rg+iKIryI^&z^z>#r=NRAGu)IO(#fY2Wl2f$>Ffc|K+(qX61PAuD)OeWv^-e=$Z z)^{>uC|{*(7xFmU!4WXXgqb=zzPC=7k3ahGaz2ZPqu>3lfBxW&H=QW_{K+$fEhx(k zd{L=`(HNa?M(2K#aPoAA5Cl`Zptin8-uZ##%YKy@(H4brt6E&F_rsXt;VVf3mdTTyJ z9D(0T@_rQ#^Ck^}n|PSN9GAjN4`Q+ZpMIf<3O$6)XiQ9MF-y$|o37G35Li~yb^WRo zfAiNrm`yLg^PArSYk^d;NT+30u}Yo3ID7Hg^L3Vfged`gP;9l;QA}b3zx}wJOob##tHuO+2>DSow$Z| zJUJd7jKR%3`Q+2-a@iY?;WPq2`j+ih277StzT>zMe)c0jEAoEa8;vJru@1wqABPa0 zR-#<37C-*sKO7w%Os_7O^nKTP`q|@q#}8qv2z}qe0hf86JAvcJJs&m-oNlF9QgdZ9 z9F8#!@S0kFMN;1fwRA9$%`2offdCkGK=7NM=Ze)D_Pf|LhTvCMY3k2mOLYtnw%K41 z4JLy*DsEy2DMFY8l~z#Zg^|vXLF2HdVuF2HhRYoYj<^M^^F=>GN0T=Vh3@TVcD^L9 z14V1^NwisHJ81Q5ul>q!q}%iC-MN-)$EyoniYdO`hkb?nE(A&X)#8MeE}bEM0CgqJ zUazOB^o=*(gb3=VAN(BL#UK6A?^k(I7=}wTWa$!&=j_?%&pq4w_P2iXB3Vw)7H3z> z`D`&y5kH2w8`lqG^WN)k7isqKFFyX@!(V*Sicwl`Jeyo)2E;P;UE3Ca8HV&n9ruZ z7Y(8wM8i=S2X64}^QQ;Lhg_&Ih~v=!_M5D%;9&0_zX><8AD5vQ8?KqG(t}})LsPQ! z9T&3HLEIC?`n3lSQV6V*1S~vF)YsnowZHh@e_XHE?>%}Kvf1h4GVnL>U#00P4&6dX*oy-~H~<;ob9#r?%l{MJjUS%DA|=sDwCpFnN6b?5Dr@(C&r( zaP;7vH;x7S?AZwK?(}M9IZl>W!%^_=*A7ljp2J?iaJ)l`s;DX#oUZLbrs!c^ zmb*xFz&y*sk)s0XJdxk8L|rm)oDL%e2S_XoW>sYwM}us&gcrl+0{aZha#=!vk-K3q zS5@ps+_tW+t_}|m+|fjN>z+SJ{B&pq)2EB0_X>6h5x>fLUP=@ByhuMw@vX@WZHMhN zil($TR$=ZTm~F-hS*>QNp`Ann-|D)xO_wzywi0on!Rzp4j+DFCYWQ~PY|ix{?6_FHeIx0RdQhYc!1 z1#TV>5#2}}&*Up5lf`_oP81PChF%Yrk?(rRx?0W`WnOr`vo4d5KYeU_eyVO~C~f5=mZzoR`GX*O{_M%sxht^1Yh!beaqxAD%bweVJtB%i&+);? z%XPL+S4aXRO4Qfhs58J-IK)F>84~_84dq$fF{O>Dz}!YHXN4@ALZr#EP-RuhBuhsJ zLx?!h5t_zyK7-sh45MVRaBcW76EePJy$x0bTY}@ zJHGd&J`-7s*#@Wg1>#cbZ^heHy4l8t7y-#hH%L5YMDofzf(_4r)S*dSPkP1g^5E=gCD8ca-lfuR)C;1+?T(V&S`8vUDK|f8(2pY6rn#Zws9$5y z_#rnp&^Jlc-M4CpkkjMVZSHROm%qd>`Ncfvm-~#GtlbpE!id>*E=tbi$De#sh~*^i zU(M!s4-Os2SuU5;>D9PDD6aJQ5yWZCbrC80*4wY+(g%y=4yoSBI|qY1lL@$&UTb1e8oN|iSd#&n`D&a8yvQ^v|O7wYnnKUou0jL zJuCDB%Qbu>gtr$E-y!XT%w^#nPi)M7m!*TEMrh+MDi(@LGaF#37hynrPjBNU!N`uA zX_z8A9FBxpx@POVwdzX|EN|SVp;y}Vs>7$kG&L?AS&))NU0%MOVtxbJqxnqUGQ0-e z&NtYh8fw#q;Lu4mVI;2glQXIX|qMo95&5kSw>%vy%y;_FNcu{26pq-F-M zsgRy8PnN4*7^kbH=OJ~7ZM)ez@xn0ZMJ2fTyvS#>Ac&kW#Gw;}=i8EG=k=2WUttN4 zVT}fL)wn@a)~1jqmqq3J?&2aDdPbI|vCTjH$v;}QeCyqB42MJ5B#Megy&Nm8M3S%X z-ha?PI$X{29I@<3M!`*t0LVtcJdwh_AgDnndFdiIn>IV!1J7=&-0q#{+}rtUy2Yt^W)7_M{|epUrA5| zW3^7oEbsUGclz;UGJzZiqFDpS1r5)zAZIc;xE|#=R$i9*YN=G^x&hMmWT|4P%aCX3 zuy?R75{RN9Eu5#b4?g$+X$lJ6wEF!5La$+2=IJ{@(LtM2_a_y%Uzf} zuw9HaNvVZ28*FS#xHe>+;JS&*M`YCsUI6a`V;=U0gI;g8xa{*Gs#${R7$_0VAqzxl zce4`cwKA-)_F+|fo?lcMS{`c+Gsp@Hiq{JYL+wHc2hJ5yK$gq-dNuptNB>Z%dtTD!;W6>3mXcjI13J zbH=`0e7rkfa|^nvGdfTMiXbB@ z9vwkU3Ohx4k#?EaEx0Q{xlP!iK8(PKN=-sa#RU^nY4f$dvtMe zKJ53vwO^H}ZUh;xFeU6euu-konHO*@t6C-)bBe>NS?D9e2ZCimv&>xY0Ez_JP=X$1 zs*xxVvtQgYAe)UmU*s##cb+~uIX^pzJp1!cKQd&Qk0FN--BAyG%o?&+&w`9%csL#% z9%Usf3LJ-pZIl8lX>OD;5V*!mrS{urdx~&3p4!{J_@c(iH_SD=lg72Y97OS`wNWp8 zdAmyXzTPHPfNn(+wd8lUf3JNb#i`fu{B5+64qSEMu2oX8DkC@bRzpuvUcak@8S3a{ z?#385mATplAsw`I5cyyY>%1Jq4!o`xdPqZ1LB1FGu6cQRdH3FN;0C2oLF9XmD`wziiV+xc+|#lM2wh5%sj)Ol6IoDLCCcTJLOyOlcnnD+ ztZ&!$Aa2a_91X_xku@E)>m;mH-}k{c9v>h6@Q44|wjuW$ot~afhNG*i*>C*D{poT# z9FNpU&90`!YOMwB!AKCiu0?)q+aQFFNu&)Xq=(X!$i*N{$IFqiy`)7i!C$4Tt`h~lqF5JpY|{hP{+J5G=)SQ&BuRAvCE0K_ zqArnZm+9Ja>|~aPmb0ABgv{e;AhPv3S^CBd{FiAPt0Y;QRc|=BD9>Jd_Zv|!E~ZI^ zQZA6da?fYa@f%gj=mB*9<)4YBnGDho)^`U7f7bbrd+Ce)5T> zgdMn16v4WK6lj^Oeai>aU91wu-Ab@znd@S<#qGF1@LV694+=%Q;FheCc$po~&59CM zGbBmtGO3E{aLnSc7lq+lZ@!tPsl#3HScr*n13W|1@El*++%r6gLWjfgYPI&=;A@ZG z`HMgQzu$P{jfW5Jd0?)q#dJE21`#++q~@5|awH%{TxY4wQJ?{#H}oBaE(xbWoDK+Y zTh3Q*7+lPk!~qJvu_7u3qZh^RzyEjt^Z)q2l=<4T{MCFl86T|k6?_D^UDGm95{#qf zvkMV|B?C7m3D459YJE}oXO$I!9b#dIAqxbCNtE6S+`CbS7cMG{V>LZ{^5n^#!#iLX zF0=AQ%I#9Q<@$1#+K_6x318U#UUhMPe0cQy^M6nkio_@gA&`^z3m|^9CX%4<@@8(T z#s?rX48EQi8`&Ptjpp@jlsL-4wbVd1$G5b0Y5;@Nh5@ig|2Pof;m2TaQ_eg@DH9o`|QDk2Mz>6rlztcAs+o` zkzpMh52Y6Nvu#X&dxNX1t6^^lnc=TJdhc>JHMn&(pLsZ)jiP|0*n+JcNPk=8*Z>*m4OU656y?w|7ff1(NMUb)?aHuSh>am8 zj|P2k#1LHLHRS7ZiBx&SEp0hv5HOayeD#n2v3;RA2KmEBVSkb4$&ZwB)W`nYyw6acxDq5u>`e z{@Yz8zv0-OSJ@4=r$KLxNTYp2T*Y=g*%{Mtuk@`*A3FWl9U1@({Q( z;W~kmnX4>ef=}~Nc$QI%fYc0 z_}U}aaVjQkVV4+7;&X#+6IQ8dI1;t4L{Y+Ln_(CpjwgAV^rGm~Cr{Mday%HHpTB@q z&FA2uIk>aDDqU&^!EsRGXPqKx{Bo6xiX?t%)Q~<-cP>y8{hCO`*MCY9HPnL3muM45G0cXr|?fukrd6HwqS#+pu{p^+`PUf8;rzkeF5D^A%0lXgSG zMtQMKsHqoxXV{~Ig6J5(e!KOBY?8I=rCfdCP_~WV(a0oqYnH1vDekU}-(J4Z%{Ix} zuVF3DAq;T|tfw(t3Y!ZSgo{7<`TPItzx%gmXHP>vfJJZFZdJ*2ofRlS2{EZll+Co< zaR3S{uw0}Wa#HaWmVG8m5=w#i3E4;-&y+?Kga}SB&3F)h{PBmO=R%N%7bq(3_+Tws zCSLcPxP|N-5|*OOA*TwxfK3jR6wjYOeR%iY|Mh2o_CNjGe>c0jgv=K)acN-$0R(%Q zB5rFFa#Y*v*C0?```bYqv818 zy?f|^i?V`D6*p_;*nV1+ri+0ecwL-b#_1pIuy}S}aiedOFq1(eJvfwwgq;rL)HSYMvR(a=^nQ*P$AEJ%JExsNQ=16xCcvc}?=#ihsvSi8l* zz$rk~oy_il@=eRHF^}CE{x0NTZvxo{b>CF)=CkO3 zl-ktMln#KoR`lp1Z?`_EetlgnYFpZVVZY`a2#mOk#cyzuWDMW{kVO$zTPcdy-gsj= zn?E}_?e+VNn&qMpIrtQuipW>Xb($=bbe*It_+gzai+o*VDZD`B87%Sjd|4JLPGBgM zWc55J_JUD=(2sfo1@^>bF#7DX&mO(?HaKkTtiS@U5Sl{`TJ$autrY^hi-v`L6r|BH zt-uQ=lQ9^xZ#!Rq=h2`2hyU;!Uw^wpAJX%YO1#&FzEkd?oFE{%g*z#bI(0@%{^=Fb(S6s zFc=I7E}|G>C@d-*A|3u>k@{eVBNQR~mpJ^vUljfWW=OWk5Eqc5WK*OGfPi3=00-NE zGnfHp?Y(Q?a=ZIEexLVbX5Grns_q^jXcaI$T{T&`+i8 z{Z~Hsxp7&zjgIg^6J#)T5mp1Yq5`!9zz7@I6*|O8kz33lpz@X#(NrrjahJx-rS|C+ zZP?3rc2@3BKCd-C6_K~hZ1O2OGJn@!`uQGyA%maGBGrFFp~RCeXd{Kl(}Ge*ssO+F z&2O&tyU^0e5*B_fyda`_uMsuL+;mptlQI2#K$Ymp!QlAd&;Kl=E?KmMaH|K@N0v-S1$Q8IB%q5L2W0(vUnhhDux zmF-nC#u`8s@M(O9+rP}diT>-H+r}_)n)vY67JD|g41v)L(p))vPzQV`>;NPw4x-^Jxj(<>@zVQLHtD}pUvh4tR( z@Pn1S$ci%m(wDwOA@Qwuev}s}M1d#|lThG6*lBm-7R8=MV3+(W@Zsg)flkUaC)=W< zPlv11irUREj;Lr=Q2@MaRg!IkiRWA2`W6{L9LH5ra>v*(q>a@kEt08%Fg=A=sAJjd z#%-5?$*Ny}{dI~gcW%EZit^9@?9aNL&e73P3DzO18B;RFc~Pxdw#dVG_>%#vs7Mtg zJ#i(;v3&M3cfS3tZ~f(0zxLn%xBp$g+e;{A&&F}wGzQ&Wz!id!-hZG%M*+i!CL@^4 z6zIuZ7izgYsn3dxl5OA}q+O8Kx_&PhJqUUOszpmJN)h@&F`P(0xOU?{8&9?vqg`6<27C4Jy&wJ_IfnQsQwRsqmSEP)}@RQM;O@tv1n z`pjqUeDy#6xhsL5@dFB#(KyY>R485a9kZD$;-|ug1thd&C8*A$u z>+4$^+w1+_8vTb3^dBjgg~HG2@nCTL-rIlsb3gY+qZMnDTfC0uvqiC|;xBAX1xi%O zI7y*go0l2)-BsK$RPBY~FaP~t)t>nIU;IV-1po4X{g?EcoesTB1Wi4zQ5A-w0R=iZ z22{G_q%I<_=ms$o$q|-w|?ulHnz7Y4IV)oqS@`XixQWJ9uHb0 z$0bYf$w+*_Yw1sG`4lHTxF?sM(G^GsPSpPKky*jRIFOD7kI(!~`ddX~mTipA}a zD}u9_vCFO=jY_%f+iX!QooQid$0wFzd^Xs`!Sns*I2}?l6-a&S#zrd$*4wRNa`xt5 zedq80%C7`od~|ZwZTBG33@th;emn|2&A1hSyQ3L38m)e>*=dv4-&o&lb~}2iWkeR{ zQvycT9)+^qX#U%O^Sj+%^EZCupX~4K^w-xY{WK0oO$sZX2VAQ{e^ok2t#N>)qElVc zrSc&gsJ+0Y9MTVaYaOf*1CZdp{iC<{_78nO+U)mK5T@w_5DtI_Gcpu(QaQ8bfhKUf zejG+Ze)r8czxlPVljryczx*q$ZU;iu^s25`uu_O5I|zIrU6}{e_ZsM~I)UpNPlnR9 ztxh0yb5xL+=aO3h*c`pt0*scpxfZYO@PDJ9=k!T~h$`De=@fI%bUZpc)kTsG233)S zZa~?>Svol@Q`3n3PO!dtZR`4t;p6?I{gd^roAK5S%v21Or7U>L^b~?sz8xOIS%j#R zQwng$QUzI_REuY+@g6U*mY<(PF$cB^;ap%9sh_cPJ{^ZOlciOb)pa5|K4XBGSs2DN zp_mO8=ZD!o8=g>33G0|}XKpM|4p?ECbvv2A`}$j)E8&_suGLv>jpu_}F=Zx@GZ1^7 zUVUNyv62b?3YwyY9C*&+HK)>Ca36F*3$RcY&A<;s_xeV^8TySz*l%{uPR@Sx-n(~R zdbPi~eYCsVXwpL@rF9Od9I^I9eUt8;kK|G!$lw)?A4Kjyz zqw#yTU*x_hdZEmD5;n z)91Q<;|9gulgB4%QEuLTDcsn|=+x57L!7VVQ34p{MU6vwp{IHo3&FA^Pdx{eV$KA8 zRaa(dyV%tFn07j1`Jml76ZCY8Wd<;?mZ@i7b5Qwwp2_Y(KGW3XdDGOVmAOBg`gkRj zx&Ad*v#yivJ6xue4zqMAs6o;LlX{RiH_h$`XR_ZV(vD)MePfckFAN%S%L!d3yhIjUe{gafwAxW4vNpHDgaa#fl1mn6%6|onqyr`e zx*1ehlV4~>%EYGI?XIog?&qo3c}&li%;`f#%8gOxZ+@33WyaeBqUcf9UZ&+yBsR2<@9Z3Pjq>RvWUPjPqWNt z9oKxSCepLdg2a`j|4O<}Q4=ydha+0Y>B%C^FxwCq3{uRDA|EgUUFGGZ5bA&Y7yrXo zzV;Wt{oB9&`Okm;_0PWg^2;xuo}OagLQeA#dh(Dm%)p0GY_~%vkq%s=(I9(x=bd-H z{`Ieq)8y~{;xBA(ZrTOTJV(TESkZ^xQc+~ex1}%dX^^FZOdf~4-jsC*AAYn>VgqGgX-tDOnL!xk`lE%7=4nkp4Is(EB%? z=2}d_GK@XX<7F4c3`suE?&0!r3D6?tONwEJ!y0B=B$ zzfMaB%HoY&Qy6S(8z$!~Dci1t4VF=5SBQnpOPdANu=$^QDIPrIcF0`qs@6jh%SG98 zz`W$ywR)D^b%Ee$3bhgD*$vEh5K>7YXVkOQ^aL)!ud7hfAf_tH;nPx7Id>N}ag>;`DjnAm+4MMM~ zipdE38Lf7s*J;y7rYKD>9rzgIxE>jn44hB~epwdTcua03ioz_nVgfOhbYfb+oelKg@@3fhF2F^OiU|8feN-&KRjbT`9BzGgHrF zXW8thmSC{fILrQ==dBGF-k$n&AM$E8_{)Zz&dkA?Rct0J=RB8H;m=M1JTEZp>IvsN zQYmC#ykN=baL~&-MIkn}HhtwiezMo3hZGvCS&YNPT@Npr&tu52y~7FPx;9yMdXJfDznrZ zj|N$)fIY$j88)HMP7bJ&Y&P3ruS2zTQshZqn%p%3#W1e1J(S<0$wav!v${zxE*&$K zhB3DNI7RYIRS;OzQK>u!y-DJ~JnayZ4YBrncWc>Q;Z@m7- z7k8iRJbLhOcX#*Ug9i}b1EkCZLXOKc_8Ql=*EcsmA2ndHbbOBw4*UH+J;QJ~?DpC) zugRp>>yg<&^R^6yEKqr&gQ6qVC@Vm4vD-;yY+lmQPm&CLLRE(7>d4Rwczak|FGCCLmmN1#;t9r(2Fl{t1u_|3A;8|C{ zy}Ctw{#vt=lJd+swM1Q!h2kYPjH|5M={t;)QyMyR6jFmQqLWndiWZfN+>(;HRNGRhY;~6KT*pCF2K{>X%+WlU@AuCNYiZ)~|z9PeKOp?q+DceYs9Gb8W zU_+>$G;rrs8B#eJNHKvr(L}HyQkDaTfaZ|nMG=KFg)G-mL4$HpD$>SI30?(yrIXXM zBG2fx$~;AvSeKNx#$FUR9M_MK{1sJ-qJ<1O3*CaEBNeFAgM4 zeGWJ8#@h(Fz_KSUa&uWnE_pK(JEi8lC2>ig#!fJKn#uZK%c{!DCTB&tD zI0YNVGseuPZ){!ZJm$0Bl>nfpmfDvAi?4Q9bysr!aU9z^ryOaD#+D#Y1dpBPfE>Vu z?39++f8Yix5ZxZstGV|<0SKsSPv^j$yFLrumboq{{CeD!Q=0CPo`HJ$7r%hk*- zo8w@mDS{X)P10)wDzGcw6_s;9^f&>t%NH6g+tj3f33>qKg%k||iqQcAH$}J*c}_bBcO4yYd(3dsKDHoClAmTjp}SnstCK>}p*$b-eJqmwcXO zn)(}=rZ!BpUJmKKL31KzbK6QMG)AfJdf zsINj50%1fa9w_nH-L%MgfXvkIm6plG zm0>OR&%!g{`Pjk-fPdC(eb%K+|)Co}1^6tH4j|c`+4E zWWXF}z*iDc<_Z80#f7p6J(UAW9)R#pLN(AMI}V`f=XePviBv9zVY{=2M2S)W6$;Ua z;w;UE`M97OwU|tDdS6BaRY>t$`9?JY=+9d5HRE_D@S06fFfuTMUd5wo7vOY?RalA! zuPg;_F-17iRf@`pja61W->i5Ujk4ov+LxRz-C}&=S{x)@XmoDrNda{%OP3-16y7O6 zCLfTVjhd?6Xf&#=H9c~pZcp}NLH^6Q=;)F&#JE|PIB=BQ20+JO9Iel&FU}9Qjin1? zv#XaPo}Z*^Q7mYQ@nAJvVKIbUIH=Fu0lEwdI|CR_YsGp)XIZ0mMdE8N?VepmQ}DcI zJRe&vV6y0|WRfTJv{}LiG>?kWk1Uw0!9-#_*GcHsmX&RmVs*lJMU{Kn(>jA}cct@m z07MjKvEpyy5wbu9py^WPZw4;Kz*y zMPxd)6^DXSw-j!12-p!re|fD7$UwjAJ$y)S$xzkjD5uC03dv#6^8Cn8RqAFcfTSPA zs1a5w(Wz;UGEfSMblo%8<>DVgn9b1qM8F*eJ_%UD^n&zd?XxjK!<!*-7%GW9d)VEEM^b|xuu^M06&%RsXCl>*>L7C8!Sh8o@6Nqo$(o5*8@HdRjTVZT3*};ilV|& zHD$O(T1@gRFJy`Hs*2JJD49o0mu{+JG--{`(xNVN=0UcAr4KYda+c({GGIChoeTq2 zXdwZLAj#4wXll#&P%+EMT(81je<}`3WVwdf{m7R!LRc9+vqWdvk$I&vSiaMY94C{V zNP9r;f=XHd90d{x4KM@en~Y6mwcG_TNxe^LZL&VZUTKq;jVnG~%={Dve6_RqG;AY( z2VXz^i!S@BI`^N3Hjce0%wx*abB$uD^XT9ht(fU8TYnXuVUsc)g}oEqm4 z99d0z!-kY~~lnsBX@JourCyrA-c5v&4%-*KTTgqZK7pf4DY$1pwaU3*r|gsF60LHPoM zF0zD@X$5m)cgX~ioB}f;iQqCp8_0E*B{LW>-+`$-kmyO*OglFNst1Z3iM!>k1Ra;C zu*3o*WiWtn=28Ne^eL=gX4VB$@4?o4wksQo3%S^(58#ECE832Zqd)C||5Q_xdXLLY zL6MHRe0sUu(XsuX=|4+*&&%E9Wi_E?n!J^iD032nh4)|o37Lt)GM2E~4w&5Q!sCr< zL*auCX^~rAObzBXvY%XImp_HEfTDzHTanAb9?)^gvI0V^X#8U73QLQaY=EijkE0dEXmzbOr@`1h7Pl_a9&fn zjOBG%cpj^p*u&xuthqWg)$`lS+7oq9`!Y&=x&3L2H#;n;ty5OBpXfOl=zL`IG~I-i zF5o%lnJbx&7Pn$9Vdybpu9$G4NxM=m=8B^E+25)KxZ3ig-UqV26YN-Ma!@Fw!wT&L zlbf_mVz3aPcS`ZommXbJI!tAmP=%)Q3?^BDrw&Mflw!bGy1@YS>4{G{XB2iC-DbNp znw&M_u5?{3asS zIX;l{RCT7-ni<${>jmeWJFoV5^rKwXWU&9|2M8{Zx%I$*o*#a@e6MYd!t30OqqNMi z?5%{Hav*tvsRR-GS%t_&#fiqs)>?mH|BAxWIst4fqs*!_&bbbT5rE9$l2UDzm}pY?5SaTRJ3bHjr&^ zb^5KIqPmb`paT7$5+*|zx+=l3Vsc-36#KI(sL10~#`-xWHT|4Y42pkbZ}v z)Rrfl%UVP9s)%#%ugx^~~318m7mvJT~+B@ibj7utiaGgNML4stq>k4%b$QrWD6 zC#`fw-Z2inPOBBQVmd363eA!vA4OT5ilO0_Pnu=?9;o=l1T|xJEBZ+kMZgzS?l_w? zgX(B-kepw z5*Uu02R?xLP}mN`W>KY--Fh;hT$a-0A}Pk{iP+as2lE{xhprdI?UpccQ6`S-V^5Pz zi_Y1RY|+IDJoWj)_53NHogEmFT}{)4A&ZeuTTTKO4HlVok<`; zmD2Ba!_AG;{him-{`^Fzy5G^2%M- z0yF12uJx$6KwxQ0P;p^*UopJbE`VLo@2IM(-bT9E(d1BhvF(p01E z5PE$GtkZZQD!qD9^E_Y1yy$t`D$d`P2>i2b;yhyysczV~R9sI&-UNLO9`9i(dOtm-=sFpu zhbM=F;h2626sk(S{K_3x5D7dLcy5sb#3s#9wfk;526}z_#&+aKqhv&p@b>MSt!A$% zGqOwyvy;idW?H9%K^TUbCxp3xG7o|zMNhJw(4*=Nbaj$ZGL;L?*~+v#t#LY$r zgCrsd=%sh|4!SyTMjgM273WDZrnv7l8aR|_BvES0n`=m!bG&WOlRVfIyz)&453RBc z)iRaJAO+Fb#dLU1PFW|pFszyxDD63+FL{CjfP6r{!0m5uKRi1A-~R3I|G|Iw(;WIH z;qdhA?%n$hzj=LoeRHEj^=FKAIdTwE%M$yskUw>7SE=N!Fb0YO)}W_}=!$uXSfbIm z0Oe!&{+!eO>L4)-m9;sCRa-fC0Tt89*LPxOZ1Fs-)2jl{bN6PK_rK23urnBtb2$uX zCFJ$$J(^{sT1j8964bsBr5CzB7lNwf%m+E^YW?Fg(a$p-s$aH4)7y-nvY*L08aK20#TD zgw*Z0TNEQjpQ@C8t4-C{M-LwT>0f;H55D~GMumClwa@RL9Bpjg98b~@?|t;e$FJSF z^HLlJj$IF8Fa~_gY_V87x~?a!jh=_f`l(wGHCbRGwhyD!z_1?abQkPt?TkeME>joj zuO$Q%2Wiy2|CI_VW&l&l>{{Cma&2BL!eS z6?b6^v=~d7t0XAH%Hz9e(7`Fd2v<=KY&l1$I0O!*LVxmjFvNP5@EqlY9)|udWy=u2 z!(ZAhHwe5|s{`p2zW1ZEWH1;gKc;Fkj$=j;xHg@pr=?uzllvd;?;gR;Grc8~waZp_2gs3|;VGk426i1MqTF4Ks$DV$VZR?I@j`PG?GGCY0o* z78@HI$7jcAS5R*`Vl~1O4wHIdm*(p5>ba z_SS{KkZqx$vF*|a_XR#(ccYh%RD%+8of}8pd-C|(-+t?xfBh|TabX-JSx&)*@&&4= z2WR7tAKiWR;LZ!TZgUC(AB4R<-UhFG=XyO^pNlL%U;!SqV5L7rYPUQ=;SyUc#?0rP zcB)VJex52%@>Gzzh4x*s6tj{7@L5_!^NL~tT5*o<2fPGBaqcSuS5ko-oXaITZrxfDJ*yv0+s=wz!}>B5Ut`-xg;DdSnlb*owP^DZgMF>aVihP=_M)&=wnnEXM<-`V zmhPVpe)Pdd|L>b`9uJd^ts6UMXTx#Q?XOXyq2UvqyPrIM_q`9cH#c23w$TUFfz$2? z!+^wvoy5!bxt38l3tV$HfwIVjIneLvLk>sOQ`seCybb5mCQx0?dSP$Vc4!vrSo_=R zSz_11Z_XKgOpQqLvWR!->htA&rsY)j(jMmZOoIYm)<>mjzx3z5*4NkX)7?D?HLs?sQa|dKg;T z!MdWfj*L7Qj}tn_MP^P;PUtiQaYT`G&FE349eOS&VC(gd4-P5(qG2*4=Ajr-i!m0cI5|TiogbA;;NYR5fgmMw4;cs*rxK` zB%Orm7!k54goo0La@yGR=&5z=PZcxgQ_5LS%PdHU zE<$CU*)Pl>$c}aBmkfXBo~AA~`n>rtMF#uKR#n=bEhW5MrdkJNheQANUkW0y#=3^P&t3fY2~#3AP105aXo^k>)AIM&Q9^ zgvLll6=r40(16GWs#{R7OUhhlI4+Z%l0%>J%vKQ5p`?f&$06wS0?GlyghFaklJ&%K z6Ma2OyYsZw=~{geB_RrAKW5~6lfek{MBj6O3dFt)vSk+wqJ;vn*CfSaHb~s50<{cq zCeMm0WF0FX$RV%;(r2gF0T`#!?PfR_pvg2G4qH9+lj#+#QYgi9; z;(MIwLVT5+2t$7~7*YK1G+X3@$e4Pa z_U`^+o}^@$Svqcn4d4uLlpq^s&i5W&>wN7R_ey`gQg8A?uo349T+3^d^A7E^970=N zOF3=NPSJwgo+!}yIzQ)A0I9h&4Sc2!(BhT~D-O@h0p%e?l zkA+$2`2%W>t$t;%rX!ipGv^GK7yv-n-K!*~!<3VjIS!v|NA<2MvmAkx#R7Pc;$T+K zKv{%L$^-=t0XjN72uX@Gk|E=8d8Le-@(!p2AZp<7LWtF6E_5LsMFv(D%$!3}mrMeZ zM7%rcA>*X#k?J^*;D(`Oeh~}`TuA-cq>+39)t_Dn^-IYtcaqnOP&5i`z^rd?YCt1O zlVxe1F|!sHwTleep6F>&-cIj7%Zx5OvIq}tA*${?Y>O(Jh$=V|hV_Cdgeq+_&p|L3 zAwNQImmLwj49LRpFjr1RAHa~O3@974WyF$L>Zz)|y}iTZ)BWQkN?kvC^7v>tfKVcp z70PEldmQ4tNaKPD z#)Ts{Z$!zBc67U_QpA<(mlev{*l0jKL4q9Fa&UExz zl%mv4@s1fJI>5dqq0X*d!4w&QVoOz2X&iM5l|qcCNQ*=iMMm*6lbKX8*-|0gJWX_? z3R$Eet4gIHq1$l7$^*U|wW3h27o44*z=j=9gxXh_JqDid2HxrMu~i2mD{K3Wk`Y2< zGa>Jgm#Imy9Y$XTk625q66tP;D3`>~bxIkrK?2+9Iqe zLV`la2`SgKE5aDrP@g`6rZ z!5;_^`kaFVp;8CngG2{O8m65sm|Qt8D1&t|NTkfvEwGf~$a+?9$ zmf`OU5ZtGAWIoMG#_Hbe)mzAQj^^{*!>N&wqNX5v>FY7!{#19u5mS_so{VXJVAhgrQ~6uvLBpE zV#xAEK_Gn7Y%nueN{RISk3ZSj-%C!1WDD#4UK~a2`6!vMP+kTxRKUPP3*v>0GRla8 zYOHl`udD4sRg8BxobIC4WTqmsxCBUCUE7U>BZI^NcWPjkWJq#wIy^c)9Sq0EClm-z z$sj2u2417oN)(<4F>lW2D(tavzmy8;gM$Mu9NL1_E9lUH9KkGhedaMutfH>GAeVj# z)%ZM{t)IsEu~`9Yb1v|EERQ7SF?0D>u12=~RM~7@v-;VaMN@vR=Nuh5Gj(c5Jl_Ws z7nx@3hKh=d)ix-SqQOI@WuPj}Jj+O|z}+Fa-AUmHy&Rc)x#RkUaY9bwrp$;~BbF8w z1O5?)Yw@CgUU+^)=4jO>>&OyxJ{_D_UO|B6^O4e2N>S+>$gt6F(9`6)0=HwULB(Fq zxHd0>DgtZ$xJVgsc7+dZ6{UQ{ZJUK+=POB0PESwx>&f2kqx%mY?K~mt9t=(**T3`9 z?M|l^gwVql%o$1M6MC^9(pw9;Pv#WZv*FSaSwm$vw&iT))sh3+B+cfN@KdBMc{r!Eiw6oLGagwTLoK?j{yFQh)E_nDj)|SBfxagX5tSisX&ML6= zIuhtWAZwVidMc<}i)MOd*JokrcOlFy<3ndKoyFaz1Q03sj+e!^m4Wkf#w+5j|$T&bv3M? z>oMs2P_zOrN2laWz|=JwQ0$}QPC4*sGJf#r;YUCD;Na*GIGwUOJR0tepjR0P@#2<4 zX4wT_ZUn_6I^R^k3hjHLH8#|fF7`zg4=KQRja@gC>+}Xn*j?qZz!gqBC*~47S~+qJ z%x`A0nGOqvF;|dRDSD2NAAGXEe|T_oJf5V=g**%yo~4M_?7yrfmsKGPqm^$5$qALj zm8BN}gZFe{$+3HgwM}|qk%Ei77Rv^XqgB`6l3S2RYdlnn3?|+$j&B<-NXI~VV z|EEtoHF^H%_o%y(Uz0+>NhG@D}drF(RAKqn>Fg@T-ONm=V4 z8I6<4Cm(#gbGmoBa~w4M?X7D*J+F6v{K;wPn@-RD{Hw2h?)A@K+gNvWm826iVku{r z6a+ox2Ix{zju6E`7y)Eq3RoE^S0po3I<8O#SBl&@{*C2QPf@5h7GDg7K#hoJYiSM4 z1*9L8OZNSkV&>`T!C*k%EN->IKZ~^}q#Me0tROs$pYDST=@_je%1g!&y3ev)M_8#7 zyitwyubTcGl4~r7)o5&{lo#a6it{$^tjsYNGR!GbXG)0x&9T|=b7e(Z`&L@@Ycv3a zo~@h63*1~B$qnzvRL-*&nR1qLsAuVOF8iK}yN%P+KBE9zp5b1|BwjF8K12O|mfrk!UnK3S4l$D!a0I0Q70P3SO^rD@S)S>ORRuB{l zDCsTq1bqHFPm)P7F6i&B@w6=CcrA`&I@Gx({i5`b>O9ZM(xSKCLELb~XfVbIu_&o( z^@A{-6l4e2*80i|PDjVxW}6Ob!}TX6cCr=IV`hWF(P-3L>mTnN(oyTS`$s1S*(eFy z&C#T+RJGPy?_KMYS&r1WyWaWWNty!%sKEN?YXbCEMQHJNe$*?>sy@np9eajfARML3ub@yW{bg z>UOGomFs3jO3@peJ!RoVVUed^&`@4P1+XYGa#dwQJ~T%F=P@JbVNr$`4BeA-(+W#v z0BR>$8cxT7u;l=Y+M?2&p}VGzU}FwGD%7jWnVG0S8Wo5)1+dl!Lik`*kIzgBFRF%Gkyz$(A%o4fx)hr>-#YLBA zjGvBOYJhio#(vjj1ogGSEar{87Mo#$r-Hn6(!6j(mv46|i(Jagb7JFUE=3YvucXTw zQRU})NuE?_$H>uSP^Ibq;oiaEtQb|Q*}QiB*7mJi{k09uJSeVuAQc%7$CSXjKE@by z$SG$np^h+CWC)GWLp4$b!cE2#l zVz1Y6sOST4LaqV)buvTAI94Wwa#^w==gySJU_HuYb8M$TR@&tzPyoev$(q*MN@+5c z^pp$2Frou#yB+yfj8BwWe)oqz`0(TVqcroPh75vnmcpr1_3i{ro>}3OlBs$j$FOpg zmg#6r&rEiEaCCD0Y#e(OW2w&PqFx28EWEO@#9*`)v8k+1nYO5S zhbyTS5Ta_DB+phjmaEMr+fcIxf|YAFE8%DWd(cPabwl2no|}T1@ei*cQs$!il6d`8 zaQU;On5$p_&(k*&GtCA5w|RzMbG^v4qmlVz?CiufJ{ceR$O^pUt`593os1_GOWK7N zijp-4yM#uhzT2dmdS4%O-+$?T;_#TkVsdjQf{pKNm)KR+}nLT_{n=u{N|hM z>znO%^Y)F~-A*I&gUNUpN4_sHju=fwu^-Zrjr=eU8=%kAj^{>F>2|xPv+8hkT%;LG zhJcws#bkGlz0-ZqP^5{lne8~uwRPETH~SmCm#%GYURM!?Z6|K`wr}1T=Ogk3MVU}+ zX*8(pRm1VgMrWq-CJk9JG zWvU)&F)34@~#0CzzFfHif$}G=vq{`_yKRF)IJ&D4uP=zRD$+;}dO#0+N9O(Fw zU#)~q7#&ooK(xUKri;A+f$#|T18d#x(fJdG?rC++t z7{Ev#@V$cZk<76$4JS!44xk*Y6H{W^-&)(~?)~J$4?cV_cyu5hAM6KW|DEuY8#mWp zxp}A5<3+|rrd!+FJCAm#z&hC5ABU>dStAFs zb@SThwe@zdP3PabD#^WNZiG6FjDy9xyp=L%0|f-1Tu=#`BYXB3V_&+E`7}Tq80O>lEu#OF=+5c1}THCGgi@s|9 zo)=!|3c}kNQUaYmd$E9w%@gIht!6U{Jyy-rmOuqQ9V2u+paOOi1iLrRAyc|jQ|Rk7pAm`+cc zXCv|olQGq;n1teVNWT$x`u+C$TGVPC4Ne>Vo)^W%aoQ7sP{EQ zRlp(?1QGcO+%oA?Mw({DB$-D<2X|vNMjL>40dtMbRR?7_6+}& zg%Y8D#9$LSoi$uMQ7ewtg+r}tgOl@PTx&W`rwmh8DtV6C(7K-1%}O>7n(At1kU8dK zAyT^*b7X<(44eS7#eB?EOJe5>=2mow-!!(1=JS?0?#Ee(%;$Q3D+aZfLHU+J%ys(N z=})jff}x_Qn3!2ey30`hYP4KQ_gFxo0F;*W@>oQ4z%_?%A}C>F@&^sjRZ5I6v+;<$ z_({o|Zrr-Pw$|I&SlilOrx@#jq#E6(QZr>HW-x^kv+3n@E5a;J zO3p#x)r{feLUwvtlV#PHykfH8Fru&$=VJ9Xr&5K~-0TV=+W$YkF1)T0riN>O z#`Wkc#xS*+CkAthIV_+vt9&X{hQo2M6QjhEp(|OwDo2wL@B_C59~1;LE6^<~88u?c z^(IM@4pXZ1=os}o-8{)p*EcD`ZYI^ehx@z3^7Qy93cTyrulM_1#}tEP_{Qfx|6un) zCGWO+-Jkn~UugGwgTWcyk?;T24|d)^Qn7sbwbyRF_(FGcGo%1prJsM}i$8w&Fd3(A z7}J~FKRR9C=)d^Nox$J~3?M~X$?&c3eDBf22Y0UD`rw_PJi7aEtx4f_Yn}eNex0Ia zp{pQ_)A0yMc3}UwKv2j^nGPFy7nan};&G=;=Uf6+6w_GRowH76EdY#Ag5d{sCcHEG zV(<|OktSnGA`E4l^jpB$r4JrG{G0E8_j_;skVTZjaWaxB0*6o#VgXf9=^h~S1be0H zL4y!a74d&1%P0_h0dk;k@7}%Z0P$z2;twLP)9JKZjc%{EeQooXe(4ultp>JOBe&6L zQFV>bh*4~X(viumH8)z_X>j9IOIzp)YjPUlTxg3TpEBk`=D2{HU*`8%2Bp(_2w+gR_@wcEOW?e@zr zr{o2)Z1dW+S6+WT?)JTAym{k=mtJ}G$!WlM z_~d#rN{=WPAQQCIQZ{!gSq#VbBR63DDsRkit*Ocq6Wlq_7t>4!qYf0Rf>}Y4X9AUI zbdGc%m?vkHjPkrZ8JxZO{qO(TUwm!%aG%QEQI@2M?yRky34<9{w^5o6$y!qoFzkB!YgOlUq zs;oR8*9rA7C$|g@WfzM zY*sQnXqM1c;-V6+)w_a!crMFaEJUB*GHaQxBQ9zVIu|zv@L{;PkyDiZ6#TKAey?2b zw7C%PjGU+S!m3J17WL5@twO3Hsc7UvYM7ke+}NOl?-@@33dB$?EP~}qg@w*s^f5dh zqiF;O0EX;s6ID)$pAI20ue2#*KU~}B9338K$;m(Z)qk+Ly?J(a8pJ`StN!LjXY;z- zh#TE?ztc{&nB-~P>Xe={PSQJHcntu9Fc?fGUL&FyLVxMCx|1U9gbn0b1>LyvFaPBK zdilon&F0#F`m_K0ufOrl7q&NVZEVrk^|ft33j3Y4&U(*p#uOp_C;&TARWhBPYnEJQ#I&*%~ zjS=%D`1y&qb!KgLP|f#6Q9eA?GB1mJHOqA7`Si0`rkI26q%++wnE`zk7IW-Mjx~Nc zU%y_ZYqTUP@gj!zk$k5w+vQ!-{p@+ydChOP<($RPWpI?@kg z5AA1cY71U}ODAI(OO|3;#Y~lmL1hf_O|XmL*DXIBC=wj95c=)B(hnX!`u-2!e)Ic3 zxc~S`>Bxj-&xqICQy~+*FpKGDxF9KfiOX2rCYgQO`n8+FXyH4#C+U=)2#~Jgz9X-tD$IAWKGIYS z3DU+xZtLZy^xaU%DkR%Ox1Ptw(A6xsV$m)(xHNi;%89|v<0vhubXx0n=p^s%K8D_A z7=LvCgHE@HUPVR)VujvgX(r?RbbLgSPzK`e-TSRh(*yB&NvZK@e74@`?(XdT#sB%I z^kPSQyYGDOt%vV_{QvyrS8w(=`?1$=_HJ%nd*!7UuW#RM$KA}7{my!$6L-2>QH#tn zfcs{xC&+s`mDFx7P{5Uru~H#^5Kv$aqXq^iWLv&xDksUt8U&shJ=U<8rU%|Xk8YI* zT0OWcaDUm=G%#oxofV83JpK0fzV}yu^PTrUzB@^>5(S&g1>2is%|FiUDgji0m`YA* zKDP`CFjRa>jB_*(bzqHPQA>e6LQr)mdmOP$kQcHD}56q~TICAXE$t;ax7le9XI zflD?iXJ6u6zE;$J?#vg4Z8|t>ubwx4SH#RKh<27IEBgHL&itR{D8CqNFIXm;3H4O= zJZ3sIIY4nCW?qeFSl-;L51NqGXu6}|%0ZwY(?f3^7%PXpvGSdE)W9j^(I2o0751qi zw*@Gy%j}0Z-jGFL4k|Sxbl{O1-gP^Q(llvDaTGVwWSnQ&;P~us-+sH8#{{TZ4l70Iz8Gi%j{@phiteVN1`f|!Jy32Pk#Kt2k*Vt3tRU;{`mg;AAIkxzy0X` z{RV~ZJiUGM)(h8P$jiLh>O+4oZqVUw_xfQgj@y7SP`CqUHS#{@!KLg}Qu@ji=Uh(G zfeyluvfs#$!H|tI*`+9*OcDnw+&Lnw!~g?*TkN12AFLwuXULa<{7Hq8XH}9zdvbK} z?%%%qH{boC$9#lunmse~CTPFvY z$4n*ubNX5c&KFr^FRB;_uw8OMXI;z>0kv?rcR_isl7l2^C($U)nhmBZ zfbIynMigJn>Y;_3Q?%pDH*F-)xuc_HNht9ur3filIl9$oD&t3uPUQJ%o*zDWk{+LY z^!D5DzxShLQu?v~g)e;Z=8G>p-rf6~AAa}F3%7e~YgDIwaDVqfBV1csJ2@UG&;8L` zZ*OdDz47X6_Xoq|79z5)4(FPM)~+Xe*W$^flE!pRjStK6gHb_yO3LBg=8EbT1( z+R54Y$M62+`)|E{Z)Yb-Gv)ij_Y=x)shq?a(@e+$0vF32&wD8#`Gr;@TD;R(!N_(y z;sY}p-#9B^s$FcXmlQAsWfx9mvlY&@t31uAn&iL%%wSrU-A*eh%AK9P!{eia!_&jV z!1S9iWA}_S`lWiqA_R2g_*KdNwP}Y(tDW+Lou3;2N;u$Pt%e zA(m&s^#{xcNAsx(b}Vt@T{#t9)66uCwP(5)o(oBKAA>xH)fzSHXlfst0-!Ay(h@Y# zagmj*A{lVS>Hziz11-qLq9{@hc&-|L9EMTgd95h&!4jRRQj<*#j*lLE^x?ht-aFoT zM9;XM{L$0x@pr}WRs;latl!Cso>*m3a`)Mfa~M^DIRK%5&o7ckxn8VYst zngwd|`hh$TGXVWf&mmXZ=Zt=Jq`_QKmSd27j+ptg8ge?%{1V1o3W9&?0Jd(cOX$Lj z9!tWYDBY?AolkF^9VD+@uf%8;ok!|TivtnW;c z=2IzKAS}S71F3>Ws~g1et@!NZI8SnPjZmuTY^`&atQQqX*d38X6#yXRXw(FAPjUOs z9oP4&NwKxHH5?AB0Fpp$zdVnEsI_tJ_;A13?@_$goHB_BiX=%U6Ug;SC>ClL6txz%VhXFTUY5S+3&#FC z3>@D-Jid4D-gkfS&WE3Td~`O+Aq$FmiZkV0#_)+sITu%gehPdC_E^E;QZkTy+F@fR zFl4QkdXKSyU=!kapcy__I3^GhGHuS36&earbPYilp(r31tTP4`;w(uNiq_C;1muhM zb`J)l(awIO-|72-yMJ&g(|4WIBO!6zCdCTq48tJXLI03m-z3akm zw6OXx(@)~m5kE^XdO1ta9cM9#U5UtB;A^VOn{u7WLUI%tskE47DNH@6(Jxd1)rZ4T z635}VQ98Wz8iKkGMHjHtELG_M+J?i!R0ddCtdv1Ck%qF`!WZ4BdwOz0FBV3TDKu4# zni0En+_x%=Jky!FYG zot@($Mk?g%>A8Z?^*l2wcp=?NBJsEcTqMwPH`3~&u(B7^GU1JC1}Gu|VhUMV1>6z} zH27U$97>ou;8)sqh3V>*JQ}qEG5D}}*K%WfbAXx3S*atB0(+Sk1H8z=?ony-UiT2qX`SHp(*_-K~uqTWg)J3VaN}B61+UK%W7C3?SD~0kOs|SWoCk@Ro_C z%;N!))mE9;nNQhL)YBlb7-l#*P(Q;O@u|DiPt{YnIub7H|6Lt3>yzKPae9u&nhC~t z0*vIKZBkhvG$l+)p}n9sSY*A%plIK-}%rH{QC@=I}p;EN< zB?=zohRk`EW|dpUy;x*oI6f__f)(2_^-i&VL8)k1DL;rK3F>smgl?f-uN8+apaPW} zMqNnR(IM-0yVJBMcMpz_3s7&kDvt9q^C$v`E$}2zJz7$s>lTxtva^MfeEI~G^QLmZ zKx7bj>El6jo)X|J%f~Pc<;5z@iwRc0vpi#MB~|Fkn>a=Vp#m7F$}LD7U|JPYYCD)D zs23@@7$@)j_=7jU`}VsZesVm?O0x9O;~nBM%?kmBA?bmH7Msl_{m*6XK;zJxS49N= zSV`d*{C@UsQm8UKtzrayWjCsUti*Q}D_zevwqTwpU+H#mDGG)Pu%fsuBW#MZ<4U^~ z-QqMElYhl7iHqizg(L3o9aE@}zXHDJO3d?M5TG&rI zjhsu8dnOCE;)JYOEiw){8?(})`6^GitHqVR-H zGngE?YV|R$X1)z(TXaj_zi>=}r(k9|I)7PFjMH&}wMXy;Ai%h);wDR`F`W=hdp=bl zKsVoH1qODb2_=~mvTB4lU-?+vQbF8^A}T*apYJ^=G#PxSAycV&QdT*$i~tyOmPpc! zYOg;H~#R zx_2@fkf9$YK9M;3EFX^h&c@^6Xq@9Z(j{}5okGbhFUEO73A=*}4oyCs4oiGZ$45e6+s=R-!wwM= zF%>iYYP*AtVXCnA8C<7ywXFo1o{^=@8^2HIH26F2&901-<~d_;y&Six#re0!%|#YT z9}IVxN&*AKzoP_8(-r7^;3y}<^uRe$j-=8fL}|ogJe1u4Zp-6vfce4ygPFDB-F5rk zC~wT@tV}#N@TC(CXi-!$KyR7XHVS$XUj^hhsLaGPk5bE!-UieAFy%=>pEz>{!=v{<{Nx8e zeCwU}f08AIj6#g_0i^I46AAzzw1z}!HS6Sb-o^Bl(x6Dlcn+#L8tZc~_k!-5J+Q}86un+w9t%l1CZ7u$^)D30$iRMCKE(>cjUvJ z-H$)K|F`eG|H*?V6vq`E%)lR~88^DI^(dySr%>Llcw7mW#j_jDty#%x0+j&kGZW#@ z#D#0ywhM>HrnTIF!DU7P&CQtU%H>SGX4*}mv_Z3cs0^8JkGri)UwNrzZotXbW%_1BeM^87h_@?R7_Bc5C)pfc%_p>)|){%iL zXEO6){bEpZW{n%?=~I*C4$zcHXxjK$v9u|~IeD}cQ#9Z5&ey^=%^ae!EN;uL2*gwX zi80HIm`=frt=?HhK|`-V!5O0!i|Wbq$(|id#`7$pCo0R_pz_hS%VL3QK&j#QTHFk; z#!<<`I1fvqCNOU2yv_^QV(~?#pBI56mR`~2lj1S}f_FShfW{T1{wRl~Fo#7h+yush zqOu})!3)uvNdzQIe{!ybwHUr-oNq~lCxt{4EFBtyelP=){{}f1#Zm}4z#w@KjEVjz zoy}t}pleb>aGfmU8Q%ZCXcXsyf-MjbT!NbQpJM?4+dxvMkNyfSq zy#A({g~(+}PR{{Q%;5`xA*p1PFqg^hjMJQFw(u`jM+yH2L=YdZ{Lc<)@UvYgTgKLM zZx;7gFh{CjT5(_o3=`T&bYAU^R_IhxZYC#bb#OL_R3`{wc64ib$i@j0Y*j%6oP(q% zJU&v~zLJ%bmsXwwZ3{G^uv3p*lL>2>4TC!x>~tU>l4uH{ zXp?$D0KN|~ZE4yPjH?ILTt+#*F)95p&5OtThi`uGhaWzC@bJn0V3HXMFLeGe-Ko%~VH#M=q^_BVYP#8r zs%t8zshOh`iqXnNnOLw$qFEqNUeW|$D62CghqS}SDF>%g^PqY@;o#xP?03(ZO_9RR zgo)Uj!9lY)w@q=$GjiJO6n3lzz7iGDUCGFA<;m#(WA44bC0Vj^v4}`pbzEP68N&?Y z5J4hKYRbfk3ab1?|lD{zW33yci%yaSncXX`RuBw86=F! zFRweS16mHIq!SN`NStBGa_V@^2qn!iA(n&3&%XEFSu^u320rIw=ENL17c)=CuZNQ` z8LY|_>!@SI|BWUsDvg=5eou;=nt_K-p;dyQ=iP%Dx>at{O4C~Y59uyg?vOu*p_jG8 z(m+omGbMN%3MLy0M>|knkRb=DnEsNFeArA*P=R213%P0ZUg(u6AeM&GC@anP9FhXf z(0WvY(D&d!<03oY|9XSu4TA+s%L5ZXhvb3r*q&oM3pB9(0z+xgMZuzTY7$4*EOUrf z6XJD580Hzkq^xO0#|OX%Y^n!w8s<-NM@d(UB_*4!#n}G!n}@&q`(OO+&wugi&E2|X zQ>MB?-i>Q1w2)W0kY`y-HF-UVZXNE#h(3c>4~|5~`SKh$aw-tR&nl?V$V5HC?~4l{ zQ|w1kDmid9L;C49V$t)PB1#I`CQSQxLYy;VHUn6MlK8HHk0^JpIN0T!%peTEdG-2n zv)im8JpVxdJ+ANXA78(|-8Fk_T~!w~DB_e!LzR@?ACC^FMINt6iD3w^7u@?3jB1qE z0i&EwiJyM=JL;`Qmv7sdLjQhxXQojr1>pbwJ5!DJ8nd05c3)?wY$|^i5yVGnt%#|4 zfhQVKi-{9e4yRH70_9oBR(68wDQFMoIi)5B#w8F}r*Byxf@L-HIaL&DK$hCUY|C>C z5?xt1>IL*yI_qeYQNY}Hdyhqps;O;Z3lCvvd>_*gh45f8YzOrt%eZ$e_mED6+zALH zdYLnmDdXvEh8D4K-_S9ly;tC4px5J3>frB1L}^!51sI+kC@5ol$5M?h!I$~MDw~_C zqr$qgZSOz*;>-W@um0-I!)DX=^jGL#tK6dtja!Mhj8YOMy3+@VD(<*sA#pU784O#O z3_4Nwm@IwHF_ObUs^ggOF-1$sfpvsB%fGvWnJVkFr00VEed7OS`f2oLx7uixBJuKV@Z-#mZ*oC2lw!=k)|QZd9n&{yNq zm(Dt5@j&Jo1Xl|*AjCBgE#)}l$0=%J2d>Fki}kZ)t8_}@$K)YQp!4H6nX*Vv8%P`t zBV10aZ{L|gzb#)q?Es|@FH!QOe8S~QEFLiii*$V+dvGcjSmv2A7(nwNx|m?^SC8NM z==;U8+_$^s;*wcSC<5t&EfzIf++ozAc_LF6S=)EE>oW~kXymPMi{5qne$}kF-U0u( z9r~6D{@gH7keXsRTwnl9J9O+&M!62||^=gn1Esalz!EBY)rVSfX6~^QWKv z<^S{le)iRiUDqRA7PX=!4&15C0>JiA0l75QNIcJr{g}G0C*ISv05DQw7lgpkLj>AS z(c#l0_Tvy`BH2z@Xf-0>BPSVBv?vS)3iBQuy^&>{+OQq?UxN5a#1xFo!Z4DO;#~CCvP?P+EOs5(s;=+a zmY`K|e#=sQ($otGcYI<{aY=!Wofoo_;&^p|D>qC6=W20+NwKvq?jV9gOdT0C z!@?d~YFH*x;Ebl|>}=CCN+gNVD$00TPk)E}hXrdqJpcNezxtcM`}wbbLyOD?w&A9I zs0*Z|lM$w&rEE#*@y-~} z;b7SznTDv;>X;}Wp9m9mo5?HErwQXJ;ke5NP)f|xxvXX^kJnDGcI(H7)r)UleE!)N z|Mbtl`R1FiIU%A<1y7QmV^&mBb)$A|J85h(+z-PkR4!O8c&_JT=Gi~|emiq!#`4yk znQ9KkXJ(GzwfcZTU@&1gF+NdeW%8znD%s0~mSPV^h}q|uJ27|xa03pR{AdAELv#CZ zSa6KxO0BByZy#ylzW3fc_yRbdmJM(nticrOQc0%hjTc45ZaZL$u>k@2$+Qm3Y)Ls_ zfU<(-<3Mrj#BhRF1D>HV1x7dd0$o#Ra%6OJS9B84Lj@rVD!GPUtx!wu=;n|Dph(I& z;t(3UgJv=g0^oVHXf+!23E@@}qLCcM-^TVi<0{aprdWs8B-Bp5tEJAo#16U6i6# zG4E0(oj^upw#gAihiz6;SY~^17-E$~EcF1GAxk8N>uW&&0`-XQ7mG zIv=$Q@uFF5fjE_yb)A)2WlCo0$Kc=~O+-(p(2z`H_e#AasgF;xo~>J3BK^CShk{=2;e$blfNvm?#~oF#w+=(*HC^ zCMal~LaQ6t5r)W3V@T!)(oKs ztUd6PLx&w^)L`(V64ElGgfg<{hy#vRDPh~704mVD!O#Q-McDR5QNuBQ#487;cOkK+ z1ydF|42#gEq*Agh9aNq1ZawywP~!Q!A5^4a8bBcVU?&AsbHm1sPNK4pYjYj$`{5YV|;IbG@dbK>Ez|d)p2kN=v1fJTvIo!w`**mI-Na z`mx`>GY_8Fqh+l=7c<`?@O89@-@Y?L^h`W?W^)zo>G+JreWE#%^o=}Y3Z&`B-d}=t z@XVlMilCAdi7CER`R{tyS^w2nU%z?t`ptc#?evY`ND>5_h$b{}oN3jZrU`tUo;Wqo;hT`wxZ0D%Qj z*X)>UI5Xp-jZsV%_Da~roRf07Mtzm@(e>rHM=F`rJD!W;(4)nohm-g z5}dToe5whOW=4~;@faSD>r$^IRTcZ7BWk4)la}a=Fu5`Fm1dItvi^I)9R(o&7`NmZ zt>H&G4#jFShA|#Wy1VS2ti+75v9{eekE`{5y3@m0C5b`r|Zi<%nz_Lr%J>v|NVoEdEZ9ClW_IEE{bm-lv?c?ThY2JNjc~LD; zZ3gbz7)fajKeG~SEM$3F28}E_o>Ly1K~D!3k`xkwtbs%jO3Ew9rQRSgmi6-D@^W!O z-|)c)AAogT!HP>w%DWUgIg3v;z_7(Hk;|mx$&lpuqW{(wD6@ zv6M4W3c$ua*@B=Ygj*bh1ci5(1a#t2$wwiv$#R z%v2atn~p8CeY08dwL5td!Fks#1Rc90!D<%1y~U4eo@sxy;qXetL1er0wt6q?xuIa!6(=(Lx{kKCqApRzDi= zdQmO_KJtcjz6w~&am$FcnDb}XH#JCM*lJZ(QKVu;4G|tkhkh7(AQm)oGR~!dgm>^P z$XT_~GX}yJWnC5}Ft^&2i<;hwuC41Ee4Q#P+c9U;iHZ(I;BM&X6I-kzzia!iUcCIL zPrvx<|MII>4?R0gXIrR-cvQuX?SSFR?W3BF7MXrDFB~f9%!_*uS7NaW!QRc}S)m8X z5O>8znvQMTpw9tCWKDW;8yiW&6mnC7!srPI-55iu?|S^@~lZ-g;G1CbFU4A&4R zrJxQCUO-=!v{YxSHJdVRR#VIvZi!k0t{gl9#Tce6?9i)R)usYsa~Rq^{_EQ2)yo(6 zx3~LUyW8*gO@pyF=TZorKmG1^PR&(jVt!9fJ7aq$?#vnR_55Z&aXNW&XNFN%FB&9` zV52A6$m-bYQ|_zbF}D`5L9E}o>CGCCqBo@&wiRTwNe>SI)q)!{=nVAMKeqd?Uf*u} zwr!doxI0y4s;u+RpMUk|fBxTfuo%eT6>W#ipu*U#H>`>QiDfS8K_;Z9+xA$FEN@#u z$^_IUIKgIok_T1+7=ZA`7bN>-<_ZJ{n2Xp^8b#SJWw{ikX?M|=>#{2H42Opop^c^G z27FPJ^p}QeV>%!VZP&%Xn)4Xz0DC_Km={3?Co5pq*wB}4c>TEk`LF-^FaPH6->h4p z+Oq(}6w~?fB2iK}MBC z@8SO$81-m@Ro?IWyW5A?0C4Qv*3;2zbjft{L<#zgXyu7ez!QzrL$memJM*ju(c5CC z3|4%!E>q_^I#0d+#6LK74Wknhg@stG1ZL8M5epQH$qVZ#wP2^3qV9vceYmF^ce`Hi zo1Mn!k*fUMY0vxszuBz5_xp|DI#t+&+rRs0KQdCVek^x0IUU$dKeroq6`h4>~|6ng)87PDjw+9Gy2i4o^h{b7;D`)V@BsW}aKZ zrq5x%%)nI9!bABpI9(N0UQuwlyLXkgw673m!?yq0Hc=zV& z<3IYCaueIz%j;)U^bZcprJ@x%D(43d&8$lTS~Nc#yaWY800kf~;3H{|lR3tp#~KFH zXU%$FKu6PG3M)c5f%WD<#RSPPWJW~HX{)zG2b%%95NuoY@VkDkA>1|aD)Wpj${*I- z^=`LX?Z3Xg`{awSzxe8_U8l;0?l6Cs(>h}vP`nWbFi|L+h~1!( z5gKn-tJRCUS9kY!Fwx*tB%sc5C0aKHZZVAd)Wt>d;(qx1U;O<2_ns}Pg3ikZC7;S^ zTOhpg^g^?O9=KTpm8jexZ6IRd+7bI9{~K!$6^U=$fuk9T1uY=UTQv!*R3QQOta}Yd-z@ z%P+rrvD&c=rAKqhB0B|ENO7|)hPw=SQ09dvv#29sUj&6yskgHr=aXOl*tg~g55OWN zhrZ1O13G0CPsNK=G_yrJw>;q8g~!3Y2AN(88;g!4Ah$-0ku-kMD8w+z>R4oI9z$wo zA5rCBa&|CqJ;nMonn!4SfsFUktcoURVOJCttd@FuURq)g?Mx~(KG}VipXw(6ds+0I z5#7^M&Ksnkmfq-;=XG|6&y1OpVHb;qP~`NnR&$Yv59^K9cJsK}tX3OJWx z;bq;{Rl#*F1x%>9@>7IWUcoC4^(m_^u*WTq*rF=wFHP6dQ(WYW93zln*X*N44r|fS zG6cwmkts~+;%bL7Sza?dOY3)S)AmiyYX&(tTn#Wv*{rs!wt4mX{>3+M?jCp8mQ^`` zh7I<=VmcY{ZQM|0LEX9A+Kf4n)@dm&a+Fu~PZcBQPV-~OSgB@?rITj`=UjF1LV+cX2;f<$XF@qT$XuNz?8Yn<6sM<^C53Ldv%>}9{g{9 z`{voZpZ(d-e!^)<@ywWk>dB$&1~C1F$xlaPZW`M`yaOVbc?1VCNeSR`<#wFyFgl*q zATxMp1v<~$!SCH}@C60q?S6AnUSi~xm#A)I*8!iJQm3AG59TYJICXZ|?{~JN*xzQl z)|uz>6M637-R@oLsTk-YST*>@Z)^?~H&M=#lQ8Km3t zDG%j|<$StDWqcT%KTYUnGqbdQnRr|2H>aXday=7CE!M;7m8(q3)JYg*iL!c;x<$@GEy=BQS5IehdQ71v~6fP+}+((7dRGZ9k2&95TT2@-tC*qd}+Ba zO9iFwI*|TKB+*XnJ|HiG71IGz*ib=33H6{m%G>G6_q17?w(A{z?e6h$O;_C0MrA!X z78V%~%#8WRVPdbQyX&ma_p}eE2Q>XvBG}CI?x_^_v_70Xc~bjx>HUaAP)P8Ary8iU zq&JPR`iZ43@0BPu>xd=FG^Ic6w<)Vj$fH>riskf<#208I1Su@h^x+5@wQurVHhKkvy;OkSdTsgHV(`OFhB}nRZm%J>lA%^;AbA%W--BtI1+y> z0R5ChnVJ~T$Ja344n}KOU6z;uLq`o>1nzP@{WcxO~E0teQsy zz`0@QZ`tk!Ne}JJ0cL)bb_U8ZU%h#mnexX!{b6(eST73St9{#)bxqN-*)@5YK@<~G zqZQh=z>%Y)1T+uu0D{s~==abr>V{3TxmaAa-QG4Z=Y-}hAL?D-L1~8FFZ%)NdyB<_ zw&G^J2L0MvK`+o9*fx7QYqtCKZnf#Uw&cE5PkR#o+4XvT|JZax-!x&h+x1RCWHzrm zYatX!M+V}(DYKk@0<_~2^09O*d&u*@+-+Slw#jA_u(kfH2G?M~7lM==OX!fnk>H=PX z-qNG$hnBAIqN+VyBDuB0VpD;S*c^hCs%dx1YHLC{WZXTDkBt6Y1c=AeA08ea*4vdE zEbT7mF~i(<9y=2=?gq?EKIMgXFQ?hS)6&4SIH)XGESkllc9v}d0^|6^!)??qV5cBI z+lRCAPkoz}`s~{#(LJd zC%*1y1k@9em{Yp-A#<3Ta_Rui`aRuN(s6SNug$pcLZQGqoAW@8?lo=7yITq%x0J%# zpb%T~e7RUwb-{e)yzg2lKGHs*;5G=l61ue}tAlMAVwv@s&8&L!@^SNc|HmJ{UuS-C zRodPRp@T?WMkO53YaPHv{otWl5_C^hiVYoA7NzTS1?(&H_mJ@P%DAQ-%uwnIpq8vgclX+Sk$S{6wca*No*#maG@;f4$jhPDqS-PNYGZ|?3MR*%3(@pu5z0adlA3M~uEIy_52s&d&g78p0t-ew=2 zPz<(#tcy`pTkc!+pZ@x9|Kfl7?|V;WUPIaPYPY9IczJQ%ceE9U9#$fQH>H=}&(Ifj zz1{Y^9o3~UgQwSYJq6FE%7CObWnQ9hOZ!N(#05mI@O%Rj+Kx*X%3^yD-Erv08cj*; z%h#{y_lwKRXV>pR{n>Ti(UBkkAb6IsA|{1<%6>zcn;veMt;wp)_n8^wY9YQ%}2??1V`fIN7d=w8iPMeyV*T zo8hS{bUH64NdyUKCn=bhZ5fdg$J#f36h@PSCfX$lSUYgIWrD37A5#%4h8fl7X4;Z%7s>N`>l%0g9EhWR4Ss2=y)^4zXa?C2E+hI9v8 zvd~q~?jL{q=7SHPXMaMkV(Y9JTuZB4g&L+q6c-`LrY-gq2%%I#Wy8=6Jw28Vz<2cV zdo)vBpKpT$+PWJ!<}-4SQA3JVWc%%Qhm0Cdn0DW{pqNm>+3xqN&9>Q5z`we>Mver@ z8Pmg;r56N(fa+Y2r(EEuswIvsn=teMjVGiATiTMHv%>M$Ma zEr?aNIkuLAJ(JQs8-!te5|iyPH9KTTcCgryfH}QslsLiSa#Yy-N7S30U{Fe{w;0Qx zYQUeBoITA594q6ndui4-?Pj;%w~eR27kM}ILT5tr15uLID^=9aF}e~tWt@!8G0DP7wP?*6@t3yM}1$~o*|)OcXl4(N;; z(YXrkzJc30!%o;b#ruwR=GclPhq|U(E{b3goj02`J<#fM5$QDnc9FYgzisxkEpKja zXgjSpn>wSt-3N-8EXK~Fjv1p}z#l0TWQZUxX;Y=OD_X4EJ*{N4G`$xlXbx88j)A2c8#Q3@_o&RbAUU+6&7e$l-Hhqzc&RILeu zoF2~ERmto>G?((FbH-hxsC zxx0+L0HfGs$~xLDJ#xNg-?OuMaATtg(2Y_@se803H*K?obAjVKtb-NxXi%|a%~a)r zM~<{HU^WD9UZGyTdi?sm7a#oTPuBa#cD4HM2OltH40>I(87T^~XM4-?cJSxxLj-Wr zTUs{a>GnI>jk&5E=Gd-p_Ox+eE4JHFZgDHvyVUic-QV$k1ZHB zb(y=8wtdbRHN~Pyr=8WaJ&U8F4kbEDa>)jhRW}t-j;AJS?tm(eAV#M|`B~dW0DhVk zI?V|%D9`h>z>!8#RI+6-1M5}#be=zhUh{O)F{uW28u?KqaN`(ra#+cx(0n-OcfT7@;Wxel8m6ugkWhGReP`fG0H|gQ@j*CU5mzjR=X7TP~p>4n2 ztm(mZmVzV|V4(*hz(e07*t)*#L4Ze(4yGeznH7s(+is93Y%Da0-InUd)%x-A{^4;& z3wuFfR-2}6D5s^3x^H%L23=iUTrHNM6*^z#uwvcscinbBbo5^jC_Fp604?ad%oJVU zhQU%vOZjnOToI`r=JxJ(QDi^(lkeR>-dffx3Ce7(4PXWz@9$TfCke@#Y_gCM!r8pA1JVODmcLyu^tg!Xl!h+MlIBABBlXz zuT=#q95mMCnljN~3+#xf<9mvAOeTi}AJ{UX%_hldiMNOHiVtq6kZHTjcvdO~ zL5PtnqiP0dnrN~zKGIX61f8M4#@JfqOQG?X=fb^~33MjP;DC0f=)AWulHW4q6yDDt zFlHuT3qRryrY#s|UJP$}5g%2ga5BRtoo)f&bK0RzKRmp;TQ}R?#_lO6sy$W^h2D%z~{B5wGQ(&V9SfablPWtU(w&Gh;ybH`t@eF zUahWj_##);)xPUc%vpLVUl`ao*Hw10xZ

>2$4j4QIo90G?JJ+s#_i-!z!z5qn1 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" + this.options.dictDefaultMessage + "")); + this.element.appendChild(Dropzone.createElement("
" + this.options.dictDefaultMessage + "
")); } if (this.clickableElements.length) { setupHiddenFileInput = (function(_this) { diff --git a/static/js/highcharts/highcharts.src.js b/static/js/highcharts/highcharts.src.js index 9e063bd7d..381650b0d 100644 --- a/static/js/highcharts/highcharts.src.js +++ b/static/js/highcharts/highcharts.src.js @@ -180,7 +180,7 @@ function merge() { } /** - * Take an array and turn into a hash with even number arguments as keys and odd numbers as + * Take an array and turn into a hash with even number arguments as role_keys and odd numbers as * values. Allows creating constants for commonly used style properties, attributes etc. * Avoid it in performance critical situations like looping */ @@ -448,7 +448,7 @@ dateFormat = function (format, timestamp, capitalize) { lang = defaultOptions.lang, langWeekdays = lang.weekdays, - // List all format keys. Custom formats can be added from the outside. + // List all format role_keys. Custom formats can be added from the outside. replacements = extend({ // Day @@ -14895,7 +14895,7 @@ var AreaSeries = extendClass(Series, { pointMap[points[i].x] = points[i]; } - // Sort the keys (#1651) + // Sort the role_keys (#1651) for (x in stack) { if (stack[x].total !== null) { // nulled after switching between grouping and not (#1651, #2336) keys.push(+x); diff --git a/static/js/jquery-ui-1.10.4.min.js b/static/js/jquery-ui-1.10.4.min.js index d2da7b53a..ef3be2396 100644 --- a/static/js/jquery-ui-1.10.4.min.js +++ b/static/js/jquery-ui-1.10.4.min.js @@ -3,5 +3,5 @@ * Includes: jquery.ui.core.js, jquery.ui.widget.js, jquery.ui.mouse.js, jquery.ui.position.js, jquery.ui.accordion.js, jquery.ui.autocomplete.js, jquery.ui.button.js, jquery.ui.datepicker.js, jquery.ui.dialog.js, jquery.ui.draggable.js, jquery.ui.droppable.js, jquery.ui.effect.js, jquery.ui.effect-blind.js, jquery.ui.effect-bounce.js, jquery.ui.effect-clip.js, jquery.ui.effect-drop.js, jquery.ui.effect-explode.js, jquery.ui.effect-fade.js, jquery.ui.effect-fold.js, jquery.ui.effect-highlight.js, jquery.ui.effect-pulsate.js, jquery.ui.effect-scale.js, jquery.ui.effect-shake.js, jquery.ui.effect-slide.js, jquery.ui.effect-transfer.js, jquery.ui.menu.js, jquery.ui.progressbar.js, jquery.ui.resizable.js, jquery.ui.selectable.js, jquery.ui.slider.js, jquery.ui.sortable.js, jquery.ui.spinner.js, jquery.ui.tabs.js, jquery.ui.tooltip.js * Copyright 2014 jQuery Foundation and other contributors; Licensed MIT */ -(function(e,t){function i(t,i){var s,a,o,r=t.nodeName.toLowerCase();return"area"===r?(s=t.parentNode,a=s.name,t.href&&a&&"map"===s.nodeName.toLowerCase()?(o=e("img[usemap=#"+a+"]")[0],!!o&&n(o)):!1):(/input|select|textarea|button|object/.test(r)?!t.disabled:"a"===r?t.href||i:i)&&n(t)}function n(t){return e.expr.filters.visible(t)&&!e(t).parents().addBack().filter(function(){return"hidden"===e.css(this,"visibility")}).length}var s=0,a=/^ui-id-\d+$/;e.ui=e.ui||{},e.extend(e.ui,{version:"1.10.4",keyCode:{BACKSPACE:8,COMMA:188,DELETE:46,DOWN:40,END:35,ENTER:13,ESCAPE:27,HOME:36,LEFT:37,NUMPAD_ADD:107,NUMPAD_DECIMAL:110,NUMPAD_DIVIDE:111,NUMPAD_ENTER:108,NUMPAD_MULTIPLY:106,NUMPAD_SUBTRACT:109,PAGE_DOWN:34,PAGE_UP:33,PERIOD:190,RIGHT:39,SPACE:32,TAB:9,UP:38}}),e.fn.extend({focus:function(t){return function(i,n){return"number"==typeof i?this.each(function(){var t=this;setTimeout(function(){e(t).focus(),n&&n.call(t)},i)}):t.apply(this,arguments)}}(e.fn.focus),scrollParent:function(){var t;return t=e.ui.ie&&/(static|relative)/.test(this.css("position"))||/absolute/.test(this.css("position"))?this.parents().filter(function(){return/(relative|absolute|fixed)/.test(e.css(this,"position"))&&/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0):this.parents().filter(function(){return/(auto|scroll)/.test(e.css(this,"overflow")+e.css(this,"overflow-y")+e.css(this,"overflow-x"))}).eq(0),/fixed/.test(this.css("position"))||!t.length?e(document):t},zIndex:function(i){if(i!==t)return this.css("zIndex",i);if(this.length)for(var n,s,a=e(this[0]);a.length&&a[0]!==document;){if(n=a.css("position"),("absolute"===n||"relative"===n||"fixed"===n)&&(s=parseInt(a.css("zIndex"),10),!isNaN(s)&&0!==s))return s;a=a.parent()}return 0},uniqueId:function(){return this.each(function(){this.id||(this.id="ui-id-"+ ++s)})},removeUniqueId:function(){return this.each(function(){a.test(this.id)&&e(this).removeAttr("id")})}}),e.extend(e.expr[":"],{data:e.expr.createPseudo?e.expr.createPseudo(function(t){return function(i){return!!e.data(i,t)}}):function(t,i,n){return!!e.data(t,n[3])},focusable:function(t){return i(t,!isNaN(e.attr(t,"tabindex")))},tabbable:function(t){var n=e.attr(t,"tabindex"),s=isNaN(n);return(s||n>=0)&&i(t,!s)}}),e("").outerWidth(1).jquery||e.each(["Width","Height"],function(i,n){function s(t,i,n,s){return e.each(a,function(){i-=parseFloat(e.css(t,"padding"+this))||0,n&&(i-=parseFloat(e.css(t,"border"+this+"Width"))||0),s&&(i-=parseFloat(e.css(t,"margin"+this))||0)}),i}var a="Width"===n?["Left","Right"]:["Top","Bottom"],o=n.toLowerCase(),r={innerWidth:e.fn.innerWidth,innerHeight:e.fn.innerHeight,outerWidth:e.fn.outerWidth,outerHeight:e.fn.outerHeight};e.fn["inner"+n]=function(i){return i===t?r["inner"+n].call(this):this.each(function(){e(this).css(o,s(this,i)+"px")})},e.fn["outer"+n]=function(t,i){return"number"!=typeof t?r["outer"+n].call(this,t):this.each(function(){e(this).css(o,s(this,t,!0,i)+"px")})}}),e.fn.addBack||(e.fn.addBack=function(e){return this.add(null==e?this.prevObject:this.prevObject.filter(e))}),e("").data("a-b","a").removeData("a-b").data("a-b")&&(e.fn.removeData=function(t){return function(i){return arguments.length?t.call(this,e.camelCase(i)):t.call(this)}}(e.fn.removeData)),e.ui.ie=!!/msie [\w.]+/.exec(navigator.userAgent.toLowerCase()),e.support.selectstart="onselectstart"in document.createElement("div"),e.fn.extend({disableSelection:function(){return this.bind((e.support.selectstart?"selectstart":"mousedown")+".ui-disableSelection",function(e){e.preventDefault()})},enableSelection:function(){return this.unbind(".ui-disableSelection")}}),e.extend(e.ui,{plugin:{add:function(t,i,n){var s,a=e.ui[t].prototype;for(s in n)a.plugins[s]=a.plugins[s]||[],a.plugins[s].push([i,n[s]])},call:function(e,t,i){var n,s=e.plugins[t];if(s&&e.element[0].parentNode&&11!==e.element[0].parentNode.nodeType)for(n=0;s.length>n;n++)e.options[s[n][0]]&&s[n][1].apply(e.element,i)}},hasScroll:function(t,i){if("hidden"===e(t).css("overflow"))return!1;var n=i&&"left"===i?"scrollLeft":"scrollTop",s=!1;return t[n]>0?!0:(t[n]=1,s=t[n]>0,t[n]=0,s)}})})(jQuery);(function(t,e){var i=0,s=Array.prototype.slice,n=t.cleanData;t.cleanData=function(e){for(var i,s=0;null!=(i=e[s]);s++)try{t(i).triggerHandler("remove")}catch(o){}n(e)},t.widget=function(i,s,n){var o,a,r,h,l={},c=i.split(".")[0];i=i.split(".")[1],o=c+"-"+i,n||(n=s,s=t.Widget),t.expr[":"][o.toLowerCase()]=function(e){return!!t.data(e,o)},t[c]=t[c]||{},a=t[c][i],r=t[c][i]=function(t,i){return this._createWidget?(arguments.length&&this._createWidget(t,i),e):new r(t,i)},t.extend(r,a,{version:n.version,_proto:t.extend({},n),_childConstructors:[]}),h=new s,h.options=t.widget.extend({},h.options),t.each(n,function(i,n){return t.isFunction(n)?(l[i]=function(){var t=function(){return s.prototype[i].apply(this,arguments)},e=function(t){return s.prototype[i].apply(this,t)};return function(){var i,s=this._super,o=this._superApply;return this._super=t,this._superApply=e,i=n.apply(this,arguments),this._super=s,this._superApply=o,i}}(),e):(l[i]=n,e)}),r.prototype=t.widget.extend(h,{widgetEventPrefix:a?h.widgetEventPrefix||i:i},l,{constructor:r,namespace:c,widgetName:i,widgetFullName:o}),a?(t.each(a._childConstructors,function(e,i){var s=i.prototype;t.widget(s.namespace+"."+s.widgetName,r,i._proto)}),delete a._childConstructors):s._childConstructors.push(r),t.widget.bridge(i,r)},t.widget.extend=function(i){for(var n,o,a=s.call(arguments,1),r=0,h=a.length;h>r;r++)for(n in a[r])o=a[r][n],a[r].hasOwnProperty(n)&&o!==e&&(i[n]=t.isPlainObject(o)?t.isPlainObject(i[n])?t.widget.extend({},i[n],o):t.widget.extend({},o):o);return i},t.widget.bridge=function(i,n){var o=n.prototype.widgetFullName||i;t.fn[i]=function(a){var r="string"==typeof a,h=s.call(arguments,1),l=this;return a=!r&&h.length?t.widget.extend.apply(null,[a].concat(h)):a,r?this.each(function(){var s,n=t.data(this,o);return n?t.isFunction(n[a])&&"_"!==a.charAt(0)?(s=n[a].apply(n,h),s!==n&&s!==e?(l=s&&s.jquery?l.pushStack(s.get()):s,!1):e):t.error("no such method '"+a+"' for "+i+" widget instance"):t.error("cannot call methods on "+i+" prior to initialization; "+"attempted to call method '"+a+"'")}):this.each(function(){var e=t.data(this,o);e?e.option(a||{})._init():t.data(this,o,new n(a,this))}),l}},t.Widget=function(){},t.Widget._childConstructors=[],t.Widget.prototype={widgetName:"widget",widgetEventPrefix:"",defaultElement:"
",options:{disabled:!1,create:null},_createWidget:function(e,s){s=t(s||this.defaultElement||this)[0],this.element=t(s),this.uuid=i++,this.eventNamespace="."+this.widgetName+this.uuid,this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this.bindings=t(),this.hoverable=t(),this.focusable=t(),s!==this&&(t.data(s,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===s&&this.destroy()}}),this.document=t(s.style?s.ownerDocument:s.document||s),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetName).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(i,s){var n,o,a,r=i;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof i)if(r={},n=i.split("."),i=n.shift(),n.length){for(o=r[i]=t.widget.extend({},this.options[i]),a=0;n.length-1>a;a++)o[n[a]]=o[n[a]]||{},o=o[n[a]];if(i=n.pop(),1===arguments.length)return o[i]===e?null:o[i];o[i]=s}else{if(1===arguments.length)return this.options[i]===e?null:this.options[i];r[i]=s}return this._setOptions(r),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled ui-state-disabled",!!e).attr("aria-disabled",e),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")),this},enable:function(){return this._setOption("disabled",!1)},disable:function(){return this._setOption("disabled",!0)},_on:function(i,s,n){var o,a=this;"boolean"!=typeof i&&(n=s,s=i,i=!1),n?(s=o=t(s),this.bindings=this.bindings.add(s)):(n=s,s=this.element,o=this.widget()),t.each(n,function(n,r){function h(){return i||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof r?a[r]:r).apply(a,arguments):e}"string"!=typeof r&&(h.guid=r.guid=r.guid||h.guid||t.guid++);var l=n.match(/^(\w+)\s*(.*)$/),c=l[1]+a.eventNamespace,u=l[2];u?o.delegate(u,c,h):s.bind(c,h)})},_off:function(t,e){e=(e||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,t.unbind(e).undelegate(e)},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,o,a=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],o=i.originalEvent)for(n in o)n in i||(i[n]=o[n]);return this.element.trigger(i,s),!(t.isFunction(a)&&a.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,o){"string"==typeof n&&(n={effect:n});var a,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),a=!t.isEmptyObject(n),n.complete=o,n.delay&&s.delay(n.delay),a&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,o):s.queue(function(i){t(this)[e](),o&&o.call(s[0]),i()})}})})(jQuery);(function(t){var e=!1;t(document).mouseup(function(){e=!1}),t.widget("ui.mouse",{version:"1.10.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):undefined}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(i){if(!e){this._mouseStarted&&this._mouseUp(i),this._mouseDownEvent=i;var s=this,n=1===i.which,a="string"==typeof this.options.cancel&&i.target.nodeName?t(i.target).closest(this.options.cancel).length:!1;return n&&!a&&this._mouseCapture(i)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){s.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(i)&&this._mouseDelayMet(i)&&(this._mouseStarted=this._mouseStart(i)!==!1,!this._mouseStarted)?(i.preventDefault(),!0):(!0===t.data(i.target,this.widgetName+".preventClickEvent")&&t.removeData(i.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return s._mouseMove(t)},this._mouseUpDelegate=function(t){return s._mouseUp(t)},t(document).bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),i.preventDefault(),e=!0,!0)):!0}},_mouseMove:function(e){return t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button?this._mouseUp(e):this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return t(document).unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}})})(jQuery);(function(t,e){function i(t,e,i){return[parseFloat(t[0])*(p.test(t[0])?e/100:1),parseFloat(t[1])*(p.test(t[1])?i/100:1)]}function s(e,i){return parseInt(t.css(e,i),10)||0}function n(e){var i=e[0];return 9===i.nodeType?{width:e.width(),height:e.height(),offset:{top:0,left:0}}:t.isWindow(i)?{width:e.width(),height:e.height(),offset:{top:e.scrollTop(),left:e.scrollLeft()}}:i.preventDefault?{width:0,height:0,offset:{top:i.pageY,left:i.pageX}}:{width:e.outerWidth(),height:e.outerHeight(),offset:e.offset()}}t.ui=t.ui||{};var a,o=Math.max,r=Math.abs,l=Math.round,h=/left|center|right/,c=/top|center|bottom/,u=/[\+\-]\d+(\.[\d]+)?%?/,d=/^\w+/,p=/%$/,f=t.fn.position;t.position={scrollbarWidth:function(){if(a!==e)return a;var i,s,n=t("
"),o=n.children()[0];return t("body").append(n),i=o.offsetWidth,n.css("overflow","scroll"),s=o.offsetWidth,i===s&&(s=n[0].clientWidth),n.remove(),a=i-s},getScrollInfo:function(e){var i=e.isWindow||e.isDocument?"":e.element.css("overflow-x"),s=e.isWindow||e.isDocument?"":e.element.css("overflow-y"),n="scroll"===i||"auto"===i&&e.widths?"left":i>0?"right":"center",vertical:0>a?"top":n>0?"bottom":"middle"};u>p&&p>r(i+s)&&(l.horizontal="center"),d>g&&g>r(n+a)&&(l.vertical="middle"),l.important=o(r(i),r(s))>o(r(n),r(a))?"horizontal":"vertical",e.using.call(this,t,l)}),c.offset(t.extend(M,{using:h}))})},t.ui.position={fit:{left:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollLeft:s.offset.left,a=s.width,r=t.left-e.collisionPosition.marginLeft,l=n-r,h=r+e.collisionWidth-a-n;e.collisionWidth>a?l>0&&0>=h?(i=t.left+l+e.collisionWidth-a-n,t.left+=l-i):t.left=h>0&&0>=l?n:l>h?n+a-e.collisionWidth:n:l>0?t.left+=l:h>0?t.left-=h:t.left=o(t.left-r,t.left)},top:function(t,e){var i,s=e.within,n=s.isWindow?s.scrollTop:s.offset.top,a=e.within.height,r=t.top-e.collisionPosition.marginTop,l=n-r,h=r+e.collisionHeight-a-n;e.collisionHeight>a?l>0&&0>=h?(i=t.top+l+e.collisionHeight-a-n,t.top+=l-i):t.top=h>0&&0>=l?n:l>h?n+a-e.collisionHeight:n:l>0?t.top+=l:h>0?t.top-=h:t.top=o(t.top-r,t.top)}},flip:{left:function(t,e){var i,s,n=e.within,a=n.offset.left+n.scrollLeft,o=n.width,l=n.isWindow?n.scrollLeft:n.offset.left,h=t.left-e.collisionPosition.marginLeft,c=h-l,u=h+e.collisionWidth-o-l,d="left"===e.my[0]?-e.elemWidth:"right"===e.my[0]?e.elemWidth:0,p="left"===e.at[0]?e.targetWidth:"right"===e.at[0]?-e.targetWidth:0,f=-2*e.offset[0];0>c?(i=t.left+d+p+f+e.collisionWidth-o-a,(0>i||r(c)>i)&&(t.left+=d+p+f)):u>0&&(s=t.left-e.collisionPosition.marginLeft+d+p+f-l,(s>0||u>r(s))&&(t.left+=d+p+f))},top:function(t,e){var i,s,n=e.within,a=n.offset.top+n.scrollTop,o=n.height,l=n.isWindow?n.scrollTop:n.offset.top,h=t.top-e.collisionPosition.marginTop,c=h-l,u=h+e.collisionHeight-o-l,d="top"===e.my[1],p=d?-e.elemHeight:"bottom"===e.my[1]?e.elemHeight:0,f="top"===e.at[1]?e.targetHeight:"bottom"===e.at[1]?-e.targetHeight:0,g=-2*e.offset[1];0>c?(s=t.top+p+f+g+e.collisionHeight-o-a,t.top+p+f+g>c&&(0>s||r(c)>s)&&(t.top+=p+f+g)):u>0&&(i=t.top-e.collisionPosition.marginTop+p+f+g-l,t.top+p+f+g>u&&(i>0||u>r(i))&&(t.top+=p+f+g))}},flipfit:{left:function(){t.ui.position.flip.left.apply(this,arguments),t.ui.position.fit.left.apply(this,arguments)},top:function(){t.ui.position.flip.top.apply(this,arguments),t.ui.position.fit.top.apply(this,arguments)}}},function(){var e,i,s,n,a,o=document.getElementsByTagName("body")[0],r=document.createElement("div");e=document.createElement(o?"div":"body"),s={visibility:"hidden",width:0,height:0,border:0,margin:0,background:"none"},o&&t.extend(s,{position:"absolute",left:"-1000px",top:"-1000px"});for(a in s)e.style[a]=s[a];e.appendChild(r),i=o||document.documentElement,i.insertBefore(e,i.firstChild),r.style.cssText="position: absolute; left: 10.7432222px;",n=t(r).offset().left,t.support.offsetFractions=n>10&&11>n,e.innerHTML="",i.removeChild(e)}()})(jQuery);(function(e){var t=0,i={},a={};i.height=i.paddingTop=i.paddingBottom=i.borderTopWidth=i.borderBottomWidth="hide",a.height=a.paddingTop=a.paddingBottom=a.borderTopWidth=a.borderBottomWidth="show",e.widget("ui.accordion",{version:"1.10.4",options:{active:0,animate:{},collapsible:!1,event:"click",header:"> li > :first-child,> :not(li):even",heightStyle:"auto",icons:{activeHeader:"ui-icon-triangle-1-s",header:"ui-icon-triangle-1-e"},activate:null,beforeActivate:null},_create:function(){var t=this.options;this.prevShow=this.prevHide=e(),this.element.addClass("ui-accordion ui-widget ui-helper-reset").attr("role","tablist"),t.collapsible||t.active!==!1&&null!=t.active||(t.active=0),this._processPanels(),0>t.active&&(t.active+=this.headers.length),this._refresh()},_getCreateEventData:function(){return{header:this.active,panel:this.active.length?this.active.next():e(),content:this.active.length?this.active.next():e()}},_createIcons:function(){var t=this.options.icons;t&&(e("").addClass("ui-accordion-header-icon ui-icon "+t.header).prependTo(this.headers),this.active.children(".ui-accordion-header-icon").removeClass(t.header).addClass(t.activeHeader),this.headers.addClass("ui-accordion-icons"))},_destroyIcons:function(){this.headers.removeClass("ui-accordion-icons").children(".ui-accordion-header-icon").remove()},_destroy:function(){var e;this.element.removeClass("ui-accordion ui-widget ui-helper-reset").removeAttr("role"),this.headers.removeClass("ui-accordion-header ui-accordion-header-active ui-helper-reset ui-state-default ui-corner-all ui-state-active ui-state-disabled ui-corner-top").removeAttr("role").removeAttr("aria-expanded").removeAttr("aria-selected").removeAttr("aria-controls").removeAttr("tabIndex").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),this._destroyIcons(),e=this.headers.next().css("display","").removeAttr("role").removeAttr("aria-hidden").removeAttr("aria-labelledby").removeClass("ui-helper-reset ui-widget-content ui-corner-bottom ui-accordion-content ui-accordion-content-active ui-state-disabled").each(function(){/^ui-accordion/.test(this.id)&&this.removeAttribute("id")}),"content"!==this.options.heightStyle&&e.css("height","")},_setOption:function(e,t){return"active"===e?(this._activate(t),undefined):("event"===e&&(this.options.event&&this._off(this.headers,this.options.event),this._setupEvents(t)),this._super(e,t),"collapsible"!==e||t||this.options.active!==!1||this._activate(0),"icons"===e&&(this._destroyIcons(),t&&this._createIcons()),"disabled"===e&&this.headers.add(this.headers.next()).toggleClass("ui-state-disabled",!!t),undefined)},_keydown:function(t){if(!t.altKey&&!t.ctrlKey){var i=e.ui.keyCode,a=this.headers.length,s=this.headers.index(t.target),n=!1;switch(t.keyCode){case i.RIGHT:case i.DOWN:n=this.headers[(s+1)%a];break;case i.LEFT:case i.UP:n=this.headers[(s-1+a)%a];break;case i.SPACE:case i.ENTER:this._eventHandler(t);break;case i.HOME:n=this.headers[0];break;case i.END:n=this.headers[a-1]}n&&(e(t.target).attr("tabIndex",-1),e(n).attr("tabIndex",0),n.focus(),t.preventDefault())}},_panelKeyDown:function(t){t.keyCode===e.ui.keyCode.UP&&t.ctrlKey&&e(t.currentTarget).prev().focus()},refresh:function(){var t=this.options;this._processPanels(),t.active===!1&&t.collapsible===!0||!this.headers.length?(t.active=!1,this.active=e()):t.active===!1?this._activate(0):this.active.length&&!e.contains(this.element[0],this.active[0])?this.headers.length===this.headers.find(".ui-state-disabled").length?(t.active=!1,this.active=e()):this._activate(Math.max(0,t.active-1)):t.active=this.headers.index(this.active),this._destroyIcons(),this._refresh()},_processPanels:function(){this.headers=this.element.find(this.options.header).addClass("ui-accordion-header ui-helper-reset ui-state-default ui-corner-all"),this.headers.next().addClass("ui-accordion-content ui-helper-reset ui-widget-content ui-corner-bottom").filter(":not(.ui-accordion-content-active)").hide()},_refresh:function(){var i,a=this.options,s=a.heightStyle,n=this.element.parent(),r=this.accordionId="ui-accordion-"+(this.element.attr("id")||++t);this.active=this._findActive(a.active).addClass("ui-accordion-header-active ui-state-active ui-corner-top").removeClass("ui-corner-all"),this.active.next().addClass("ui-accordion-content-active").show(),this.headers.attr("role","tab").each(function(t){var i=e(this),a=i.attr("id"),s=i.next(),n=s.attr("id");a||(a=r+"-header-"+t,i.attr("id",a)),n||(n=r+"-panel-"+t,s.attr("id",n)),i.attr("aria-controls",n),s.attr("aria-labelledby",a)}).next().attr("role","tabpanel"),this.headers.not(this.active).attr({"aria-selected":"false","aria-expanded":"false",tabIndex:-1}).next().attr({"aria-hidden":"true"}).hide(),this.active.length?this.active.attr({"aria-selected":"true","aria-expanded":"true",tabIndex:0}).next().attr({"aria-hidden":"false"}):this.headers.eq(0).attr("tabIndex",0),this._createIcons(),this._setupEvents(a.event),"fill"===s?(i=n.height(),this.element.siblings(":visible").each(function(){var t=e(this),a=t.css("position");"absolute"!==a&&"fixed"!==a&&(i-=t.outerHeight(!0))}),this.headers.each(function(){i-=e(this).outerHeight(!0)}),this.headers.next().each(function(){e(this).height(Math.max(0,i-e(this).innerHeight()+e(this).height()))}).css("overflow","auto")):"auto"===s&&(i=0,this.headers.next().each(function(){i=Math.max(i,e(this).css("height","").height())}).height(i))},_activate:function(t){var i=this._findActive(t)[0];i!==this.active[0]&&(i=i||this.active[0],this._eventHandler({target:i,currentTarget:i,preventDefault:e.noop}))},_findActive:function(t){return"number"==typeof t?this.headers.eq(t):e()},_setupEvents:function(t){var i={keydown:"_keydown"};t&&e.each(t.split(" "),function(e,t){i[t]="_eventHandler"}),this._off(this.headers.add(this.headers.next())),this._on(this.headers,i),this._on(this.headers.next(),{keydown:"_panelKeyDown"}),this._hoverable(this.headers),this._focusable(this.headers)},_eventHandler:function(t){var i=this.options,a=this.active,s=e(t.currentTarget),n=s[0]===a[0],r=n&&i.collapsible,o=r?e():s.next(),h=a.next(),d={oldHeader:a,oldPanel:h,newHeader:r?e():s,newPanel:o};t.preventDefault(),n&&!i.collapsible||this._trigger("beforeActivate",t,d)===!1||(i.active=r?!1:this.headers.index(s),this.active=n?e():s,this._toggle(d),a.removeClass("ui-accordion-header-active ui-state-active"),i.icons&&a.children(".ui-accordion-header-icon").removeClass(i.icons.activeHeader).addClass(i.icons.header),n||(s.removeClass("ui-corner-all").addClass("ui-accordion-header-active ui-state-active ui-corner-top"),i.icons&&s.children(".ui-accordion-header-icon").removeClass(i.icons.header).addClass(i.icons.activeHeader),s.next().addClass("ui-accordion-content-active")))},_toggle:function(t){var i=t.newPanel,a=this.prevShow.length?this.prevShow:t.oldPanel;this.prevShow.add(this.prevHide).stop(!0,!0),this.prevShow=i,this.prevHide=a,this.options.animate?this._animate(i,a,t):(a.hide(),i.show(),this._toggleComplete(t)),a.attr({"aria-hidden":"true"}),a.prev().attr("aria-selected","false"),i.length&&a.length?a.prev().attr({tabIndex:-1,"aria-expanded":"false"}):i.length&&this.headers.filter(function(){return 0===e(this).attr("tabIndex")}).attr("tabIndex",-1),i.attr("aria-hidden","false").prev().attr({"aria-selected":"true",tabIndex:0,"aria-expanded":"true"})},_animate:function(e,t,s){var n,r,o,h=this,d=0,c=e.length&&(!t.length||e.index()",options:{appendTo:null,autoFocus:!1,delay:300,minLength:1,position:{my:"left top",at:"left bottom",collision:"none"},source:null,change:null,close:null,focus:null,open:null,response:null,search:null,select:null},requestIndex:0,pending:0,_create:function(){var t,i,s,n=this.element[0].nodeName.toLowerCase(),a="textarea"===n,o="input"===n;this.isMultiLine=a?!0:o?!1:this.element.prop("isContentEditable"),this.valueMethod=this.element[a||o?"val":"text"],this.isNewMenu=!0,this.element.addClass("ui-autocomplete-input").attr("autocomplete","off"),this._on(this.element,{keydown:function(n){if(this.element.prop("readOnly"))return t=!0,s=!0,i=!0,undefined;t=!1,s=!1,i=!1;var a=e.ui.keyCode;switch(n.keyCode){case a.PAGE_UP:t=!0,this._move("previousPage",n);break;case a.PAGE_DOWN:t=!0,this._move("nextPage",n);break;case a.UP:t=!0,this._keyEvent("previous",n);break;case a.DOWN:t=!0,this._keyEvent("next",n);break;case a.ENTER:case a.NUMPAD_ENTER:this.menu.active&&(t=!0,n.preventDefault(),this.menu.select(n));break;case a.TAB:this.menu.active&&this.menu.select(n);break;case a.ESCAPE:this.menu.element.is(":visible")&&(this._value(this.term),this.close(n),n.preventDefault());break;default:i=!0,this._searchTimeout(n)}},keypress:function(s){if(t)return t=!1,(!this.isMultiLine||this.menu.element.is(":visible"))&&s.preventDefault(),undefined;if(!i){var n=e.ui.keyCode;switch(s.keyCode){case n.PAGE_UP:this._move("previousPage",s);break;case n.PAGE_DOWN:this._move("nextPage",s);break;case n.UP:this._keyEvent("previous",s);break;case n.DOWN:this._keyEvent("next",s)}}},input:function(e){return s?(s=!1,e.preventDefault(),undefined):(this._searchTimeout(e),undefined)},focus:function(){this.selectedItem=null,this.previous=this._value()},blur:function(e){return this.cancelBlur?(delete this.cancelBlur,undefined):(clearTimeout(this.searching),this.close(e),this._change(e),undefined)}}),this._initSource(),this.menu=e("